发现很多利用归并思想的做法其实都是基于归并排序的,而归并排序的核心在于merge的过程是基于两个有序数组的操作,基于多个有序数组的操作往往都能做到每个数组的指针不回溯,这就是复杂度降低的原因。
而归并解法能降低时间复杂度的原因也在于利用了已经存在的有序,所以可以说:这些解法就像是利用了归并排序在不同时期提供的钩子函数,依此完成统计工作。
例一:经典归并排序
public static int[] HELP;
//help数组的作用是:在merge时需要一个新的空间,存放从两个有序部分合并起来的部分
//可以在方法中每次merge时new一个新的数组来用,但很耗空间.
public static void mergeSort(int[] ints,int l,int r){
HELP = new int[ints.length];
if(l == r){
return;
}
/*
这里为什么只需要在 l == r 退出即可?不用再在 r - l = 1退出?
考虑在哪里退出就是为了防止出现死循环,当r和l差一个时代入方法任能向下分治,只是mid并不是中间元素罢了
*/
int mid = l + ((r-l)>>1);
mergeSort(ints,l,mid);
mergeSort(ints,mid+1,r);
merge(ints,l,mid,r);
}
public static void merge(int[] ints,int l,int m,int r){
int a = l; //第一部分的指针
int b = m+1; //第二部分的指针
int index = l; //汇总到新空间的指针
//此while循环是当两个指针都未越界时的
while (a<=m && b<=r){
if(ints[a]<=ints[b]){
HELP[index] = ints[a];
index++;
a++;
}else {
HELP[index] = ints[b];
index++;
b++;
}
}
//此while是当b越界,将a剩余部分全部写入
while (a<=m){
HELP[index] = ints[a];
index++;
a++;
}
//此while是当a越界,将b剩余部分全部写入,两个while最多只会有一个执行
while (b<=r){
HELP[index] = ints[b];
index++;
b++;
}
//复制HELP回原数组,即用merge后有序的数组替代
for (int i = l; i <= r; i++) {
ints[i] = HELP[i];
}
}
例二:LeetCode 493
怎么判断哪些问题可以用归并解决?
以下是两个指标:
- 原问题可以分为左边部分,右边部分,左对右的部分
- 什么是左对右的部分?就是无论左右部分是否有序,单独考虑左边每一个元素对于右边元素的效果(例题中更直白)
- 左右两部分变有序可以优化原问题求解
第一个是递归往下分治调用的基础,第二个则是归并能降低复杂度的原因
在这个例题中,对于[1,3,2,3,1],要找出其中所有反转对(就是前面比后面大很多的数对),我们可以分为求前一半中的反转对,后一半中的反转对,以及由前一部分的一个元素和后一部分的一个元素组成的反转对。
这就是归并吗?优点在哪里?
例如我们已经完成了底层的代码,现在在执行最上层的统计反转对的工作,也就是已经归并出了a = [1,2,3]和b = [1,3],接下来要找依次对比a和b的元素,看看是否满足题目条件。归并就是利用了两个数组有序,如果a中前面的数比b中某个数大,那a后面的元素一定也比b中那个数大;如果a中某个元素确定比b中某个元素大,那一定比b中那个数之前的元素大。
也就是说:如果用两个指针遍历两个数组,则这两个指针不会回溯就遍历完成并找出所有匹配元素。
具体流程如下:
基本思路是找到每个i索引下能够满足条件的j索引
一开始ij指针位于起始位置,在这个位置j对应的数已经大到不满足条件了,所以无法移动j,要移动i
此时的i下j满足,就可以移动j,再来判断是否符合条件。而假如i的后面还有元素的话,移动j能够保证下一个i也一定和前面的j符合关系
听起来归并使用有一个条件————就是两个数组之间元素一定是比大小的关系。(后面有题目不是这个关系,LeetCode官方题解是先转化成这个关系)
代码:
public static int MAXN = 5001;
public static int[] help = new int[MAXN];
public int reversePairs(int[] nums) {
return counts(nums, 0, nums.length - 1);
}
public static int counts(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int m = (l + r) / 2;
return counts(arr, l, m) + counts(arr, m + 1, r) + merge(arr, l, m, r);
}
public static int merge(int[] arr, int l, int m, int r) {
int ans = 0;
int j = m + 1;
/*
核心在于,归并时,怎么利用两个数组有序,双指针不回溯地,进行统计 —— 实则类似抽象的归并排序 —— 双指针的移动利用了有序的规则
*/
for (int i = l; i <= m; i++) {
while (j <= r) {
if ((long) arr[i] > (long) arr[j] * 2) {
j++;
}else {
break;
}
}
ans += j - (m + 1);
}
int i = l;
int a = l;
int b = m+1;
while (a<=m && b<=r){
if(arr[a]<arr[b]){
help[i] = arr[a];
a++;
i++;
}else {
help[i] = arr[b];
b++;
i++;
}
}
while (a<=m){
help[i] = arr[a];
a++;
i++;
}
while (b<=r){
help[i] = arr[b];
b++;
i++;
}
for (int k = l; k <= r; k++) {
arr[k] = help[k];
}
return ans;
}
基本框架和归并排序没有区别了,唯一一点是在merge时完成统计
- 另外有一点:其实merge时只是计算了我们上面所说的左边对右边的部分中产生的结果,但其实基本上是所有的结果,因为最底层的merge是两个元素之间的嘛,再往上一层层merge并起来;也就是说,其实之前说的统计左边部分,右边部分,左边对右边的部分本质上都是在计算左边对右边的部分。
例三:LeetCode 315
可以用归并吗?明显可以,因为可以划分出左右,而且也可以通过排序优化比大小的逻辑。
代码:(我的)
public List<Integer> countSmaller(int[] nums) {
//问题是:排序导致加错位置了
// Map<Integer,Integer> map = new HashMap<>();
// for (int i = 0; i < nums.length; i++) {
// map.put(nums[i],i);
// }
int[] indexs = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
indexs[i] = i;
}
int[] counts = counts(nums, 0, nums.length - 1,indexs);
List<Integer> list = new ArrayList<>();
for (int i = 0; i < counts.length; i++) {
list.add(counts[i]);
}
return list;
}
public static int[] counts(int[] nums, int l, int r,int[] indexs) {
int[] help = new int[nums.length];
if (l == r) {
return help;
}
int m = (l + r) / 2;
int[] counts = counts(nums, l, m,indexs);
int[] counts2 = counts(nums, m + 1, r,indexs);
int[] counts3 = merge(nums, l, m, r,indexs);
for (int i = 0; i < nums.length; i++) {
counts[i] = counts[i] + counts2[i] + counts3[i];
}
return counts;
}
public static int[] merge(int[] nums, int l, int m, int r,int[] indexs) {
int[] ans = new int[nums.length];
int j = m + 1;
for (int i = l; i <= m; i++) {
while (j <= r && nums[i] > nums[j]) {
j++;
}
// ans[i]+=j-m-1;
ans[indexs[i]]+=j-m-1;
}
int i = l;
int a = l;
int b = m+1;
int[] help = new int[nums.length];
int[] indexHelp = new int[nums.length];
while (a<=m && b<=r) {
if(nums[a]<nums[b]){
help[i] = nums[a];
indexHelp[i] = indexs[a];
i++;
a++;
}else {
help[i] = nums[b];
indexHelp[i] = indexs[b];
i++;
b++;
}
}
while (a<=m){
help[i] = nums[a];
indexHelp[i] = indexs[a];
i++;
a++;
}
while (b<=r){
help[i] = nums[b];
indexHelp[i] = indexs[b];
i++;
b++;
}
for (int k = l; k <= r; k++) {
nums[k] = help[k];
indexs[k] = indexHelp[k];
}
return ans;
}
这里面有一个陷阱,但并不和归并的思路冲突
陷阱是:每当我们获取到左边/右边得到的结果(一个对应每个元素的数组,是基于两部分还未排序的顺序的),在这一层merge时,是基于新的顺序得到的结果数组,这导致每个merge产生的结果数组顺序都不同,导致加错位了。
解决方法就是用一个同等大小的数组标记该索引元素的实际位置,每次merge排序时同步移动此数组的元素。
例四:LeetCode 327
这个题可以用归并吗?看似可以哦:分别考虑左边数组的子范围,右边数组的子范围,再考虑左边跨右边数组的子范围。
但其实有一个问题 ———— 左边的元素和右边的元素并不是比较关系,导致无法有效利用有序的信息
本题中是判断区间内元素的和是否在范围内,这样的话无论怎么移动哪个指针都不会有明确的结果。
那怎么办?————想想办法转化成左右比大小的类型————将原数组转化成前序和数组,那么原数组区间内的和就是新数组两个元素的差值。
代码:
public int countRangeSum(int[] nums, int lower, int upper) {
long s = 0;
long[] sum = new long[nums.length + 1];
for (int i = 0; i < nums.length; ++i) {
s += nums[i];
sum[i + 1] = s;
}
return countRangeSumRecursive(sum, lower, upper, 0, sum.length - 1);
}
public int countRangeSumRecursive(long[] sum, int lower, int upper, int left, int right) {
if (left == right) {
return 0;
} else {
int mid = (left + right) / 2;
int n1 = countRangeSumRecursive(sum, lower, upper, left, mid);
int n2 = countRangeSumRecursive(sum, lower, upper, mid + 1, right);
int ret = n1 + n2;
// 首先统计下标对的数量
int i = left;
int l = mid + 1;
int r = mid + 1;
while (i <= mid) {
while (l <= right && sum[l] - sum[i] < lower) {
l++;
}
while (r <= right && sum[r] - sum[i] <= upper) {
r++;
}
ret += r - l;
i++;
}
// 随后合并两个排序数组
long[] sorted = new long[right - left + 1];
int p1 = left, p2 = mid + 1;
int p = 0;
while (p1 <= mid || p2 <= right) {
if (p1 > mid) {
sorted[p++] = sum[p2++];
} else if (p2 > right) {
sorted[p++] = sum[p1++];
} else {
if (sum[p1] < sum[p2]) {
sorted[p++] = sum[p1++];
} else {
sorted[p++] = sum[p2++];
}
}
}
for (int j = 0; j < sorted.length; j++) {
sum[left + j] = sorted[j];
}
return ret;
}
}
还是有难度的哎