上一篇文章我们通过两个例子,体会到了“归并排序”的套路。只需要对经典 递归 代码稍加修改,就会 有奇效 !我们再来回顾一下(还没看上篇文章的赶快 点我 查看哦!)
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int l, int r) {
if (l == r) {
return;
}
int m = l + ((r - l) >> 1);
process(arr, l, m);
process(arr, m + 1, r);
merge(arr, l, m, r);
}
public static void 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;
while (p1 <= m && p2 <= r) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
相信大家对这段经典 递归代码 已经很熟悉了,通过上一篇的学习,我们总结出了以下 经验套路 :
只要题目可以归结为:寻找一个元素左侧或右侧 所有符合要求 的元素个数,都可以考虑使用 归并排序 。
下面我们再来练习两道 LeetCode Hard 级别的题目
170. 交易逆序对的总数
在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录 record,返回其中存在的「交易逆序对」总数。
输入:record = [9, 7, 5, 4, 6]
输出:8
解释:交易中的逆序对为 (9, 7), (9, 5), (9, 4), (9, 6), (7, 5), (7, 4), (7, 6), (5, 4)。
思路:
本题和 上篇文章 的问题 1 类似,继续利用两个子数组 已经有序 的特点,去寻找本题需要的 右侧比该数小 的数。
思考一下,右侧子数组的小数只可能在左半部分出现,要想计算出左半部分的个数,可以从右往左进行数组拷贝。
比粉色 9 小的数是蓝色部分的所有数字。
由上面这张图我们可以考虑如下几个情况:
- 当左右两侧数相等
左arr[7]==右arr[7]时,应该优先拷贝右侧的数组,右侧指针减一,这样才能找到右侧小于该数的最右侧下标。 - 当左侧 > 右侧时,计算右组个数并左侧指针减一。
- 当左侧 < 右侧时,右侧指针指针减一。
合并一下,只有当 左侧 > 右侧 时,才对当前蓝色部分个数 p2 - m 进行累加。
搞懂思路之后,代码迎刃而解:
public static int reversePairs(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 m = l + ((r - l) >> 1);
return process(arr, l, m) + process(arr, m + 1, r) + merge(arr, l, m, r);
}
public static int merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int p1 = m;
int p2 = r;
int i = help.length - 1;
int ans = 0;
while (p1 >= l && p2 >= m + 1) {
ans += arr[p1] > arr[p2] ? (p2 - m) : 0;
help[i--] = arr[p1] > arr[p2] ? arr[p1--] : arr[p2--];
}
while (p1 >= l) {
help[i--] = arr[p1--];
}
while (p2 >= m + 1) {
help[i--] = arr[p2--];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
return ans;
}
327. 区间和的个数
给你一个整数数组 nums 以及两个整数 lower 和 upper 。求数组中,值位于范围 [lower, upper] (包含 lower 和 upper)之内的区间和的个数 。
输入:nums = [-2,5,-1], lower = -2, upper = 2
输出:3
解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2 。
该题难度较大,我们先看代码再解释。
public static int countRangeSum(int[] nums, int lower, int upper) {
if (nums == null || nums.length == 0) {
return 0;
}
long[] sum = new long[nums.length];
sum[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
sum[i] = sum[i - 1] + nums[i];
}
return process(sum, 0, sum.length - 1, lower, upper);
}
public static int process(long[] sum, int L, int R, int lower, int upper) {
if (L == R) {
return sum[L] >= lower && sum[L] <= upper ? 1 : 0;
}
int M = L + ((R - L) >> 1);
return process(sum, L, M, lower, upper) + process(sum, M + 1, R, lower, upper) + merge(sum, L, M, R, lower, upper);
}
public static int merge(long[] arr, int L, int M, int R, int lower, int upper) {
int ans = 0;
int winL = L;
int winR = L;
for (int i = M + 1; i <= R; i++) {
long min = arr[i] - upper;
long max = arr[i] - lower;
while (winR <= M && arr[winR] <= max) {
winR++;
}
while (winL <= M && arr[winL] < min) {
winL++;
}
ans += winR - winL;
}
long[] help = new long[R - L + 1];
int i = 0;
int p1 = L;
int p2 = M + 1;
while (p1 <= M && p2 <= R) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= M) {
help[i++] = arr[p1++];
}
while (p2 <= R) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
return ans;
}
题目思路
转化1:
本题的关键在于给定一个区间,要求出该 区间和 是否满足要求。因此我们能够想到一定与 前缀和 有关。因此,首先求出该数组的 前缀和 sum[i]。
关键代码:
sum[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
sum[i] = sum[i - 1] + nums[i];
}
转化2:
再来思考,因为前缀和表示的是 某个数及前面所有数字之和 。因此,我们考虑能否将问题也转化为与 某个数及前面数字 有关的问题呢?答案是 可以的 。
我们以区间 [L,R] 的右端点 R 为基准,进而问题就转化为了:看符合 区间和 要求的左端点都可以取哪些值。即 L 取哪些值时, [L,R] 范围区间和落在了区间 [lower, upper] 上。
转化3:
要想求出 [L,R] 范围上的区间和是否符合要求,即 [0,R]-[0,L] = sum[R] - sum[L] 的结果是否落在了区间 [lower, upper] 上。
由图中的进一步转化知,即求 L 取哪些值时,前缀和 sum[L] 的范围落在区间 [sum[R] - upper , sum[R] - lower] 上。
至此,本问题又转化为了:寻找某个数左侧范围上符合要求的 所有数的个数问题。那么就可以考虑使用我们的 归并套路 进行解题了!
代码思路
之前的几道题目符合条件的要么是子数组左侧全部数据要么是子数组右侧全部数据,因此一个指针即可求出个数。
但本题是一个 动态区间,因此设置左右 两个窗口指针 winL,winR,规定窗口取值为左闭右开 [L,R) 。右侧数组升序有序性,因此 [sum[R] - upper , sum[R] - lower] 为单调递增的区间,又可采用 单调区间不回退 的思想。
以右子数组中的每一个为基准,在保证左子数组区间不越界的同时,每轮检查 arr[winR] 和 arr[winL] 是否满足区间要求。符合要求时,计算当前的区间长度 winR - winL 。
因此就有了本题最核心的代码段
int winL = L;
int winR = L;
for (int i = M + 1; i <= R; i++) {
long min = arr[i] - upper;
long max = arr[i] - lower;
while (winR <= M && arr[winR] <= max) {
winR++;
}
while (winL <= M && arr[winL] < min) {
winL++;
}
ans += winR - winL;
}
总结
通过 上篇文章 和本文的学习,我们总结出了 归并排序 的解题套路:
寻找一个元素左侧或右侧 所有符合要求 的元素个数。
同时要考虑清楚以下几个问题:
- 是要以 左侧子数组 为基准 还是 以 右侧子数组 为基准?
- 寻找比当前基准 大 还是 小 的数,或是其他要求?
- 怎样寻找区间更合适,从 左到右 遍历 / 从 右到左 遍历?
- 符合要求的区间怎样表示,单指针 / 双指针 ?
- 左右两侧子数组当前值 相等时 应该先拷贝 左侧 还是 右侧 ?
相信大家心里也都有自己的思考了,你学会了么!