首页 > 技术文章 > 【算法】归并排序的扩展

hzyuan 2022-01-04 17:16 原文

左程云算法与数据结构课 https://www.bilibili.com/video/BV13g41157hK?p=2&spm_id_from=pageDriver

小和问题

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

例如:[1,3,4,2,5],1左边比1小的数,没有;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。

题解

小和问题也可以按如下方法求,看每个数右边有多少个比它大的数,那这个数就累加几次。

例如:[1,3,4,2,5],1右边比1大的数有4个;3右边比3大的数有两个;4右边比4大的数有1个;2右边比2大的数有1个;5右边没有比5大的数。所以小和为1*4 + 3*2 + 4*1 + 2*1 =16。

可以通过归并排序的思想求小和问题。设变量res=0,

image-20220104145023744

1 和 3 比较。1 比 3 小,所以 res =res+1,{1}。 归并结果为{1,3}。

image-20220104145750885

{1,3} 和 4 比较。1 比 4 小,所以 res=res+1,{1};3 比 4 小,所以res =res+3,{1,3}。归并结果为{1,3,4}。

image-20220104150033683

2 和 5 比较。2 比 5 小,所以 res=res+2,{2}。归并结果为{2,5}。

image-20220104150200268

{1,3,4} 和 {2,5} 比较。1 比 2小,因为右部份是有序的,所以1 比 右部分所有数(2,5两个数)都小,所以 res = res + 1*2,{1};3 比 2 大,{1,2};3 比 5 小,所以 res=res+3,{1,2,3};4 比 5 小,所以res=res+4,{1,2,3,4}。归并结果为{1,2,3,4,5}。

最后res的值即为数组的小和。

代码实现

public class SmallSum {
    //求数组小和
    public static int smallSum(int[] arr) {
        if (arr == null || arr.length < 2) {
            return 0;
        }
        return process(arr, 0 , arr.length - 1);
    }
    //用归并排序的思想求数组小和
    private static int process(int[] arr, int l, int r) {
        if (l == r) { //只有一个元素即小和为0
            return 0;
        }
        int mid = l + ((r - l) >> 1);
        //小和等于左部分小和+右部分小和+merge过程小和
        return process(arr, l, mid) + process(arr, mid + 1, r) + merge(arr, l, mid, r);
    }
    private static int merge(int[] arr, int l, int mid, int r) {
        int[] help = new int[r - l + 1]; //辅助数组
        int i = 0;
        int p1 = l;
        int p2 = mid + 1;
        int res = 0; //小和
        while (p1 <= mid && p2 <= r) {
            //当左侧数比右侧数小时,左侧数是所有右侧数的小和需要累加的值
            res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
            //与经典merge的区别是当两数相等时取右侧数,否则会导致小和不正确
            help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }
        while (p1 <= mid) {
            help[i++] = arr[p1++];
        }
        while (p2 <= r) {
            help[i++] = arr[p2++];
        }
        for (i = 0; i < help.length; i++) {
            arr[l + i] = help[i];
        }
        return res;
    }

    //for test
    public static void main(String[] args) {
        int[] arr = {1,3,4,2,5};
        System.out.println(smallSum(arr));
    }
}

逆序对问题

在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有的逆序对。

例如:[3,2,4,5,0] 的全部逆序对是 (3,2)、(3,0)、(2,0)、(4,0)、(5,0)

题解

经过小和问题的熏陶,我们很容易想到逆序对问题也可以通过归并排序的思想求得。只不过小和问题是左侧数比右侧小时进行相应处理,而逆序对问题是左侧数比右侧数大时进行相应处理。

代码

public class InversionPair {

    public static void inversionPair(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process(arr, 0, arr.length - 1);
    }

    private static void process(int[] arr, int l, int r) {
        if (l == r) {
            return;
        }
        int mid = l + ((r - l) >> 1);
        process(arr, l, mid);
        process(arr, mid + 1, r);
        merge(arr, l, mid, r);
    }

    private static void merge(int[] arr, int l, int mid, int r) {
        int[] help = new int[r -l + 1];
        int i = 0;
        int p1 = l;
        int p2 = mid + 1;
        while (p1 <= mid && p2 <= r) {
            if (arr[p1] > arr[p2]) { //左部分有序,若其比右侧值大,则其后所有值均比右侧值大
                int temp = 0;
                while (temp < mid - p1 + 1) { //输出逆序对
                    System.out.println("(" + arr[p1 + temp] + "," + arr[p2] +")");
                    temp++;
                }
            }
            //注意此处是<=,即两数相等时取左侧数,若取右侧数,则当左部分有比右侧数大的值时,就会遗漏逆序对
            //与小和问题的此处 < 对比思考
            help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        }
        while (p1 <= mid) {
            help[i++] = arr[p1++];
        }
        while (p2 <= r) {
            help[i++] = arr[p2++];
        }
        for (i = 0; i < help.length; i++) {
            arr[l + i] = help[i];
        }
    }

    //for test
    public static void main(String[] args) {
        int[] arr = {3,2,4,3,0};
        inversionPair(arr);
    }
}

推荐阅读