327.区间和的个数
给定一个整数数组 nums,返回区间和在 [lower, upper] 之间的个数,包含 lower 和 upper。
区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。
说明:
最直观的算法复杂度是 O(N²) ,请在此基础上优化你的算法。
示例:
输入: nums = [-2,5,-1], lower = -2, upper = 2,
输出: 3
解释: 3个区间分别是: [0,0], [2,2], [0,2],它们表示的和分别为: -2, -1, 2。
一句话:
给一个数组和一个区间,求数组中有多少个【连续的元素之和】在这个区间内。
解题思路:
第一时间想到暴力法,但是题目明确说明不能使用暴力法,暴力法的时间复杂度为O(N²)。暴力法代码在文章最后。刷题最重要是先想到解决方案,然后考虑优化~
此解法参考官方解答:前缀和 + 归并排序
- 根据数组算出前缀和数组 preSums,问题等价求 preSum[j] - preSum[i] 在区间内的个数
- 将 preSums 平分为2段 lefts 和 rights,求得 rights[j] - lefts[i] 在区间内的个数。然后按照此方法继续分解lefts 和 rights,直到分解不动。累加每一次的结果就是最后的结果
- 假设 lefts 和 rights 都是有序的,问题变得简单很多。定义3个指针 i ,l,r。i 指向 lefts[0],l 和 r 指向 rights[0]。移动 l 找出第一个 rights[l] - lefts[i] 在区间内的点,再移动 r 找出最后一个 rights[r] - lefts[i] 在区间内的点,「r - l + 1 」即为本次的个数。然后移动 i 指向lefts[i+1],再次移动 l 和 r 找出满足区间的最小点和最大点,因为数组是递增的,l 和 r只能向右移动,使得问题简单很多。
- 利用归并排序合并时特点,每次合并的2个数组都是有序的
假设下图为归并排序某一次合并的状态。
在合并前我们利用两2个数组的有序性先求得满足区间的个数。
归并排序:https://www.runoob.com/w3cnote/merge-sort.html
方法一:前缀和 + 归并排序
先写一个归并排序,在每次合并前先求满足区间的个数
将代码中【统计代码】去掉,即为归并排序
public int countRangeSum(int[] nums, int lower, int upper) {
// 前缀和数组
long[] preSums = new long[nums.length + 1];
for (int i = 0; i < nums.length; i++) {
preSums[i + 1] = preSums[i] + nums[i];
}
return mergeSort(preSums, 0, preSums.length - 1, lower, upper);
}
/**
* 归并排序
*/
public int mergeSort(long[] preSums, int left, int right, int lower, int upper) {
if (left == right) {
return 0;
}
// 分解
int mid = (left + right) / 2;
int retL = mergeSort(preSums, left, mid, lower, upper);
int retR = mergeSort(preSums, mid + 1, right, lower, upper);
int ret = retL + retR;
// 在合并数组前统计2个升序数组满足的角标对
// 【统计】开始
int i = left;
int l = mid + 1;
int r = mid + 1;
while (i <= mid) {
// 移动左指针找出满足区间的最小角标
while (l <= right && preSums[l] - preSums[i] < lower) {
l++;
}
// 移动右指针找出满足区间的最大角标 + 1
while (r <= right && preSums[r] - preSums[i] <= upper) {
r++;
}
ret += (r - l);
i++;
}
// 【统计】结束
// 合并
merge(preSums, left, mid, right);
return ret;
}
public void merge(long[] arr, int left, int mid, int right) {
long[] temp = new long[right - left + 1];
int i = 0;
int p1 = left;
int p2 = mid + 1;
// 比较两个数组,依次将小的元素放入temp,直到遍历完其中一个数组
while (p1 <= mid && p2 <= right) {
temp[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
// 将未遍历完的数组放入temp
while (p1 <= mid) {
temp[i++] = arr[p1++];
}
while (p2 <= right) {
temp[i++] = arr[p2++];
}
// 改变原数组
for (int j = 0; j < temp.length; j++) {
arr[left + j] = temp[j];
}
}
执行结果:
时间复杂度: 和归并排序一致 O(NlogN)
空间复杂度: 和归并排序一致 O(N)
附暴力法:
public int countRangeSum1(int[] nums, int lower, int upper) {
long[] temp = new long[nums.length];
for (int i = 0; i < nums.length; i++) {
temp[i] = nums[i] + 0L;
}
int ans = 0;
for (int i = 0; i < temp.length; i++) {
long c = 0;
for (int j = i; j < temp.length; j++) {
c += temp[j];
if (c >= lower && c <= upper) {
ans++;
}
}
}
return ans;
}