算法学习:归并排序和衍生问题(前端JS实现)(小和问题,逆序对问题)

493 阅读3分钟

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)
}

总结

  1. 归并排序的算法的时间复杂度是:O(n*logn) (递归)
  2. 归并排序算法的空间复杂度是:O(n) 用到了一个辅助数组
  3. 归并排序为什么能做到logN的时间复杂度,因为没有浪费每一次比大小的信息,有序信息传递给向上的递归回朔过程

image.png

归并排序的扩展问题:

小和问题

描述 在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。

例子
[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…