大家好,我是周一。
最近几篇算法,我们都是聊的归并排序,今天再开一篇。再聊两题。
一、大于右侧数的两倍
怕大家忘了归并排序,所以先拿一题练练手。
1、题目描述
求给定数组中,当前数 大于 右侧数两倍 的个数
2、例子
数组:[6, 7, 3, 2, 1]
当前数大于右侧数的两倍的数有(6,2),(6,1),(7,3),(7,2),(7,1),(3,1)
所有总共有6个。
3、思路
认真看过归并排序:解决小和、逆序对问题的伙伴都知道,我们在求解时,都是和merge操作放在一起的。但是此题再和merge操作放在一起求解,难度、代码复杂度就很大了。
所以,我们换个角度,很简单,我们把 求个数的操作 和 merge操作 分成两个循环单独求,瞬间就豁然开朗了。
4、详细的参考代码
只有merge操作的代码,其他和归并排序的一模一样
private static int merge(int[] arr, int l, int mid, int r) {
int num = 0;
// l...mid, mid+1...r, 目前右组寻找的范围 [mid+1, windowR)
int windowR = mid + 1;
for (int i = l; i <= mid; i++) {
while (windowR <= r && arr[i] > (arr[windowR] << 1)) {
windowR++;
}
// 此时,符合条件的个数为 (windowR - (mid+1))
// 因为此时windowR不满足要求,所以不是(windowR - (mid+1)) +1
num += windowR - mid - 1;
}
int[] help = new int[r - l + 1];
int i = 0;
int pL = l;
int pR = mid + 1;
while (pL <= mid && pR <= r) {
// 谁小拷贝谁(相等的拷贝左组的)
help[i++] = arr[pL] <= arr[pR] ? arr[pL++] : arr[pR++];
}
while (pL <= mid) {
help[i++] = arr[pL++];
}
while (pR <= r) {
help[i++] = arr[pR++];
}
for (int j = 0; j < help.length; j++) {
arr[l + j] = help[j];
}
return num;
}
OK,热身完毕。今天的重头戏当然是如何用归并排序解决LeetCode的这道Hard题。
二、LeetCode327. 区间和的个数
那么我们就看看在LeetCode的题目中,归并排序有何妙用呢。
1、题目描述
https://leetcode-cn.com/probl...
给你一个整数数组 nums 以及两个整数 lower 和 upper 。求数组中,值位于范围 [lower, upper] (包含 lower 和 upper)之内的 区间和的个数 。
区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。
2、例子
输入:nums = [-2,5,-1], lower = -2, upper = 2
输出:3
解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2 。
3、思路
首先,我们要先知道,前缀和的概念。
前缀和:preSum[ j ]所表示的 j 位置的前缀和含义是,原数组0到 j 位置的数的和。
为啥要用前缀和。因为对于频繁求数组某个范围内数的和时,使用前缀和代替这样在求原数组 i - j 范围内数的和,则等于前缀和数组中 preSum[ j ] - preSum[ i - 1]。这样就不用每次求和时都遍历数组了。
核心思路如下:
1.使用前缀和代替原数组;
2.在归并排序合并方法内,统计满足条件的累加和个数 和 合并操作分开。
3.每次合并操作,对于右组(前缀和数组)中的每一个数X[ i ],求左组(前缀和数组)所有数中有多少个范围在 [X[ i ] - upper, X[ i ] - lower]上,将满足条件的个数累加起来即为最后的结果
可能看的云里雾里,来,我们看看详细的代码
4、详细的参考代码
/**
* 描述:https://leetcode.com/problems/count-of-range-sum/。
* 中文版:(327. 区间和的个数)https://leetcode-cn.com/problems/count-of-range-sum/
*
* 给你一个整数数组 nums 以及两个整数 lower 和 upper 。
* 求数组中,值位于范围 [lower, upper] (包含 lower 和 upper)之内的 区间和的个数 。
*
* 区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。
*
* 结果直接在leetcode测试
*
*
* @author Java和算法学习:周一
*/
public class CountOfRangeSum {
public static int countRangeSum(int[] nums, int lower, int upper) {
if (nums == null || nums.length < 1) {
return 0;
}
// 求原数组的前缀和
long[] preSum = new long[nums.length];
preSum[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
preSum[i] = preSum[i - 1] + nums[i];
}
return process(preSum, 0, preSum.length - 1, lower, upper);
}
private static int process(long[] preSum, int L, int R, int lower, int upper) {
// L == R时,preSum[L] 表示原数组 [0, L]范围上的累加和
// 在merge过程中,会忽略掉左组一个数也没有的这种情况,所以在这里补充这种情况
if (L == R) {
return preSum[L] >= lower && preSum[L] <= upper ? 1 : 0;
}
int mid = L + ((R - L) >> 1);
// 返回左组和右组在合并过程中产生的满足条件的累加和个数
return process(preSum, L, mid, lower, upper)
+ process(preSum, mid + 1, R, lower, upper)
+ merge(preSum, L, mid, R, lower, upper);
}
private static int merge(long[] arr, int L, int mid, int R, int lower, int upper) {
// 累加和个数
int ans = 0;
// 左组寻找的左侧位置(肯定是从当前的L位置开始)
int windowL = L;
// 左组寻找的右侧位置(肯定也是从当前的L位置开始)
int windowR = L;
// 对于右组的每一个数X,在左组中寻找值在[X-upper, X-lower]之间的个数
for (int i = mid + 1; i <= R; i++) {
long min = arr[i] - upper;
long max = arr[i] - lower;
// 因为是在左组中寻找,所以下标不能超过mid
// 寻找当前值比max大的第一个位置(因为等于max的时候右移了一位,所以不包含此位置)
while (windowR <= mid && arr[windowR] <= max) {
windowR++;
}
// 寻找当前值大于等于min的第一个位置(因为等于min的时候没有右移,所以包含此位置)
while (windowL <= mid && arr[windowL] < min) {
windowL++;
}
// 最后满足要求的累加和个数为 [windowL, windowR),即 windowR - windowL,windowR是开区间,所以不 +1
ans += windowR - windowL;
}
// 以下是经典的merge过程
long[] help = new long[R - L + 1];
int i = 0;
int pL = L;
int pR = mid + 1;
while (pL <= mid && pR <= R) {
help[i++] = arr[pL] <= arr[pR] ? arr[pL++] : arr[pR++];
}
while (pL <= mid) {
help[i++] = arr[pL++];
}
while (pR <= R) {
help[i++] = arr[pR++];
}
for (int j = 0; j < help.length; j++) {
arr[L + j] = help[j];
}
return ans;
}
}
LeetCode测试结果:
尽管执行用时稍微差了那么点点,但毕竟是通过的第一道Hard题,一步一步来,相信自己。