算法之归并排序的应用[极其详细] !
前言
如果你对归并排序还不了解,可以移步到我的另外一篇博客 排序算法4 - 归并排序
应用1 —— 小和问题
1. 题目
问题描述
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
举例说明
比如数组 [1, 3, 4, 2, 5]
1 左边比 1 小的数,没有
3 左边比 3 小的数,1
4 左边比 4 小的数,1、3
2 左边比 2 小的数,1
5 左边比 5 小的数,1、3、2、4
所以小和为 1 + 1 + 3 + 1 + 1 + 3 + 4 + 2 = 16
2. 思路
暴力解法
最暴力的做法就是在每个位置,依次遍历,得到左边比该位置小的数,时间复杂度为 O(N²) ,因为每一个位置,都需要遍历一次
归并排序
我们转换一个思路,求每一个数左边比当前小的数,也就等同于求每个数右边比当前大的数的个数
仍然以数组 [1, 3, 4, 2, 5] 举例说明如下:
1 它的右边有 4 个数比它大
3 它的右边有 2 个数比它大
4 它的右边有 1 个数比它大
2 它的右边有 1 个数比它大
5 它的右边没有数比它大
小和为 1 * 4 + 3 * 2 + 4 * 1 + 2 * 1 = 16
那么怎么用归并排序来解决这个问题呢?
归并排序的思想是分而治之,先分再合并,其中分是和归并排序一样的
分完之后就是合并,合并详细过程如下:
3. 代码
通过一步一步过程的说明,相信你已经大致了解了求解小和的过程,现在上代码 [如果会写归并排序,下面代码会很好理解]
public class smallSum {
public static void main(String[] args) {
int[] arr = new int[]{1, 3, 4, 2, 5};
System.out.println(smallSum(arr));
}
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);
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;
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;
}
}
运行结果正如我们之前计算出来的一样
4. 总结
① 通过分而治之的思想,时间复杂度从暴力的 O(N²) 变成了 O(N logN) ,并且我要说明一点,当左组指针所指的数小于右组指针所指的数时,可以通过右组指针与右组最后一个元素的下标之差来判断共有几个数大于左组指针所指的数,而不用遍历右组数组,相关代码如下:res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
② 小和问题的解决思路和归并排序几乎一样,不同的一点是它们对左、右两组遇到值相同的数时的处理,小和问题是右组指针++,归并排序是左组指针++;因为小和问题若不右组指针++,就无法得到大于左组指针所指的数的个数(相当于如果左组指针++,就忽略了计算这个左组指针所指的数对应的小和),代码中体现的差异如下:
tips: 归并排序遇到值相同的数时选择先将左组指针指向的元素放入临时数组中,这样可以使得排序是稳定的
应用2 —— 逆序对问题
1. 题目
问题描述
在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序对。
举例说明
比如数组 [1, 3, 4, 2, 5]
1 没有逆序对
3 存在一对逆序对 [3, 2]
4 存在一对逆序对 [4, 2]
2 没有逆序对
5 没有逆序对
所以逆序对的个数为 1 + 1 = 2
2. 思路
暴力解法
暴力解法指的是枚举法,即每个位置都进行遍历,得到逆序对的个数
归并排序
求逆序对的个数实际就是求一个数右边比它小的数的个数,即求一个数左边比它大的数的个数,与上面的小和问题思路类似,这里不再赘诉,有困惑的小伙伴可以看代码
3. 代码
我相信明白了上面的小和问题,逆序对问题就相对轻松些啦,代码如下:
public class InvertedOrderNum {
public static void main(String[] args) {
int[] arr = new int[]{3, 5, 2, 1, 0, 4, 9};
System.out.println(invertedOrderNum(arr));
}
public static int invertedOrderNum(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);
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] ? 0 : (mid - p1 + 1);
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;
}
}
4. 总结
该代码与小和问题主要的不同有两小点
① 小和问题中,得到小和是通过判断左组指针所指的数是否小于右组的,若小于,则根据右组指针的位置可以得到有几个数大于左组指针所指的数
逆序对问题中,得到逆序对个数是通过判断左组指针是否小于右组的,若大于,则根据左组指针的位置可以得到有几个数大于右组指针所指的数
② 对于遇到左右两组值相同的数的处理,
小和问题是通过右指针++,原因在小和问题应用的总结中有所说明;
逆序对问题是通过左指针++,原因和小和问题相仿:若不左组指针++,就无法得到大于右组指针所指的数的个数(相当于如果右组指针++,就忽略了计算这个右组指针所指的数对应的逆序对)
欢迎来逛逛我的博客 mmimo技术小栈