归并排序扩展-小和问题-逆序对问题

 一、小和问题

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

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

  这个问题,当然可以采用逐个比较相加的方法进行求解,但是这种方法的时间复杂度为 O(N^2),想要进一步降低时间复杂度可以采用 归并排序的思想进行求解。

  将数组中的每一个数的左边小于它的所有数相加,可以换一种取值的解释,统计数组中的每一个数(假设数为 a),在它的右边有多少个比它大的数(假设共有 n 个),那么在最后求小和时,就有 n 个 a被计算。

 

   为什么会想到使用归并排序的方法呢?

  因为在递归将两个数组进行合并(merge)的过程中,当 左数组下标为 p1 的值比 右数组下标为 p2 的值小时,可以通过下标简单的减法运算得到在右数组中有多少个比 p1 大的数,同时,由于归并排序时一层一层将需要排序的数组进行合并的,所以每一个数都可以和它右边的所有数进行比较。

  下面具体讲解一个例子:

[ 2, 3, 6, 7, 8, 3, 4, 9]

  (1)首先,先进行左边的递归 [ 2], [3]

  

   此时,左数组 p1 = 0,有数组 p2 = 1,p1 的值比 p2 小,因此 2 的右边有 (R-p2+1)=(1-1+1)= 1 个比 2 大的数

  将 2 写进新的数组空间后,发现左边数组越界,此时将右边所有元素拷贝进新数组中,完成这一轮的排序。

  (2)接着递归到达数组 [ 6], [7] 

  同样的,左数组 p1 = 2,有数组 p2 = 3,arr[p1] 的值比 arr[p2] 小,因此6 的右边有 (R-p2+1)=(1-1+1)= 1 个比 6 大的数

  (3)接着递归到达

[2, 3]  [6, 7]    L = 0,R = 3,M = 1
p1 = 0, p2 = 2
(1)因为 arr[p1] < arr[p2],所有在右边数组中统计小于 arr[p1] 的值的个数,有 (R-p2+1)=(3-2+1)=2,因此得到有两个小于 2 的数 p1++ => p1 = 1 (2)因为 arr[p1] < arr[p2],所有在右边数组中统计小于 arr[p1] 的值的个数,有 (R-p2+1)=(3-2+1)=2,因此得到有两个小于 3 的数 p1++ => p1 = 2 (3) 左边数组越界(p1 > M),接着将右边数组所有元素拷贝到新数组中,在将新数组拷贝回原数组对应的位置。   这一轮结束

  (4)(5) (6) 右边的数组 [  8, 3, 4, 9]  经过和上边一样的步骤得到数组中每个元素的右边比它大的值的个数,并将数组排序为 [ 3, 4, 8, 9]

[2, 3, 6, 7] [3, 4, 8, 9]   L = 0,R = 7,M = 3
p1 = 0, p2 = 41)因为 arr[p1] < arr[p2],所有在右边数组中统计小于 arr[p1] 的值的个数,有 (R-p2+1)=(7-4+1)=4,因此得到有 4 个小于 2 的数
    p1++ => p1 = 1 (arr[p1] = 3)
(2) 因为 arr[p1] = arr[p2],将 p2 对应数值拷进 新数组中
     p2++ => p2 = 5 (arr[p2] = 4)
 (3)因为 arr[p1] < arr[p2],所有在右边数组中统计小于 arr[p1] 的值的个数,有 (R-p2+1)=(7-5+1)=3,因此得到有 3 个小于 3 的数
   p1++ => p1 = 2  (arr[p1] = 6)
(4) 因为 arr[p1] > arr[p2],将 p2 对应数值拷进 新数组中
     p2++ => p2 = 6 arr[p2] = 8)
 (5)因为 arr[p1] < arr[p2],所有在右边数组中统计小于 arr[p1] 的值的个数,有 (R-p2+1)=(7-6+1)=2,因此得到有 2 个小于 6 的数
   p1++ => p1 = 3  (arr[p1] = 7)
 (6)因为 arr[p1] < arr[p2],所有在右边数组中统计小于 arr[p1] 的值的个数,有 (R-p2+1)=(7-6+1)=2,因此得到有 2 个小于 7 的数
   p1++ => p1 = 4  (左边数组越界)
(7)因为左边数组已经越界,所以将右边数组剩下的元素拷贝进新数组,再将新数组拷贝回原数组对应位置即可。

  这里还有一个值得注意的地方,在 arr[p1] == arr[p2] 时,将右边数组的值拷贝进新数组,这是与经典归并排序不同的地方。

  因为这个方法是通过计算下标的差值来得到右边有多少个数比 arr[p1] 小,将右边数组的值先拷贝更为准确。

  例如

  

   JAVA 代码实现如下:

public static int smallSum(int[] arr) {

        if (arr == null || arr.length < 2)
            return 0;
        return process(arr, 0, arr.length - 1);
    }

    public static int process(int[] arr, int L, int R) {
        // 检验排序数组区间的个数
        if (L == R)
            return 0;
        // 将数组划分为两部分
        int mid = L + ((R - L) >> 1);
        // 分别得到左右两部分数组的小和后加上该数组merge后求得的小和,返回
        return process(arr, L, mid) + process(arr, mid + 1, R) + merge(arr, L, mid, R);
    }

    public static int merge(int[] arr, int L, int M, int R) {
        // 创建一块与传进来的数组登场的空间
        int[] help = new int[R - L + 1];
        // 设置一些辅助变量
        int p1 = L;
        int p2 = M + 1;
        int i = 0;
        int res = 0;// 计算结果
        // 当左右数组都没有越界
        while (p1 <= M && p2 <= R) {
            res += arr[p1] < arr[p2] ? (R - p2 + 1) * arr[p1] : 0;
            // 与经典排序算法不同的地方,当 arr[p1] == arr[p2] 时,拷贝右边的数
            help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }
        // 左数组越界
        while (p2 <= R) {
            help[i++] = arr[p2++];
        }
        // 右数组越界
        while (p1 <= M) {
            help[i++] = arr[p1++];
        }
        // 拷贝数组
        for (i = 0; i < help.length; i++) {
            arr[L + i] = help[i];
        }
        // 返回结果
        return res;
    }

 二、逆序对问题

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

  这个问题也是采用 归并排序的方法进行解决,不同的,这个问题是右数组中比左数组中的值大的元素个数。

 

posted @ 2020-04-22 17:37  葡萄籽pp  阅读(206)  评论(0编辑  收藏  举报