O(N*logN)的排序算法:归并排序
先上一段归并排序的实现代码
function mergeSort(arr) {
// 对两边已经排好的数组进行 合并
function merge (left, mid, right) {
let index1 = left
let index2 = mid + 1
let index = 0
// 辅助数组
let help = Array(right - left + 1)
while (index1 <= mid && index2 <= right) {
if (arr[index1] <= arr[index2]) {
help[index++] = arr[index1]
index1++
} else {
help[index++] = arr[index2]
index2++
}
}
while (index1 <= mid) {
help[index++] = arr[index1]
index1++
}
while (index2 <= right) {
help[index++] = arr[index2]
index2++
}
index = 0
for (; index < help.length; index++) {
arr[left + index] = help[index]
}
}
function process(left, right) {
if (left === right) {
return
}
// 防止(left + right) /2 求和溢出
const mid = left + ((right - left) >> 1)
process(left, mid)
process(mid + 1, right)
merge(left, mid, right)
}
process(0, arr.length - 1)
}
总结
- 归并排序的算法的时间复杂度是:O(n*logn) (递归)
- 归并排序算法的空间复杂度是:O(n) 用到了一个辅助数组
- 归并排序为什么能做到logN的时间复杂度,因为没有浪费每一次比大小的信息,有序信息传递给向上的递归回朔过程
归并排序的扩展问题:
小和问题
描述 在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。
例子
[1,3,4,2,5]
求解过程:
1左边比1小的数:0
3左边比3小的数:1
4左边比4小的数:1,3
2左边比2小的数:1
5左边比5小的数:1,3,4,2
所以小和为1+1+3+1+1+3+4+2=16
小和问题,可以换个思路考虑: 求一个数组的小和,可以转化为求每个元素在小和累加过程出现的次数,然后将当前元素与出现次数相乘,累加得到小和。 假设当前元素为a,a右边比a大的元素个数,为a在小和累加过程出现的次数
对上面的归并排序代码进行改写,在merge过程中,利用左右已经拍好序的数组,求得左边数组每个项的小和。(right - index2 + 1) * arr[index1],即右边数组中有多少项(right - index2 + 1)比左边当前数组项(arr[index1])大,相乘得到该项的小和。值得注意的一点,merge过程必须要进行数组的排序,如果不做排序的情况下,无法直接得到右边数组有多少项比arr[index1]大。
代码如下:
// 归并排序 求小和问题
function merge(arr, left, mid, right) {
let index1 = left
let index2 = mid + 1
let sum = 0
let help = []
while (index1 <= mid && index2 <= right) {
if (arr[index1] < arr[index2]) {
help.push(arr[index1])
sum += (right - index2 + 1) * arr[index1]
index1++
} else {
// 如果右边比左边大,先拷贝右边的值
help.push(arr[index2])
index2++
}
}
while (index1 <= mid) {
help.push(arr[index1])
index1++
}
while (index2 <= right) {
help.push(arr[index2])
index2++
}
for (let i = 0; i < help.length; i++) {
arr[left + i] = help[i]
}
return sum
}
function process (arr, left, right) {
if (left === right) {
return 0
}
const mid = left + ((right - left) >> 1)
const leftSum = process(arr, left, mid)
const rightSum = process(arr, mid + 1, right)
return merge(arr, left, mid, right) + leftSum + rightSum
}
function getMinSum(arr) {
return process(arr, 0, arr.length - 1)
}
改写归并排序且求解小和问题,时间复杂度和空间复杂度同于归并排序(O(n*logn) 和 O(N))。
逆序对问题
逆序数:在一个排列中,如果一对数的前后位置与大小顺序相反, 即前面的数大于后面的数,那么它们就称为一个逆序。 一个排列中逆序的总数就称为这个排列的逆序数。、
对上面的归并排序代码进行改写,在merge过程中,对当前数组项(arr[index1]),有(right - index2 + 1)项比之小,即可以得到多少逆序对。
// 归并排序 求逆序对问题
function merge(arr, left, mid, right) {
let index1 = left
let index2 = mid + 1
let sum = 0
let help = []
while (index1 <= mid && index2 <= right) {
if (arr[index1] > arr[index2]) {
help.push(arr[index1])
sum += right - index2 + 1
index1++
} else {
help.push(arr[index2])
index2++
}
}
while (index1 <= mid) {
help.push(arr[index1])
index1++
}
while (index2 <= right) {
help.push(arr[index2])
index2++
}
for (let i = 0; i < help.length; i++) {
arr[left + i] = help[i]
}
return sum
}
function process (arr, left, right) {
if (left === right) {
return 0
}
const mid = left + ((right - left) >> 1)
const leftSum = process(arr, left, mid)
const rightSum = process(arr, mid + 1, right)
return merge(arr, left, mid, right) + leftSum + rightSum
}
function getReverseSets(arr) {
return process(arr, 0, arr.length - 1)
}
改写归并排序且逆序对问题,时间复杂度和空间复杂度同于归并排序(O(n*logn) 和 O(N))。
文章来源,在左神的算法视频内容基础上写的。 推荐一下左神的算法视频,讲的很棒,地址: www.bilibili.com/video/BV13g…