题目
标题和出处
标题:区间和的个数
出处:327. 区间和的个数
难度
8 级
题目描述
要求
给定一个整数数组 nums \texttt{nums} nums 以及两个整数 lower \texttt{lower} lower 和 upper \texttt{upper} upper,返回数组中的值位于范围 [lower, upper] \texttt{[lower, upper]} [lower, upper](包含 lower \texttt{lower} lower 和 upper \texttt{upper} upper)内的区间和的个数。
区间和 S(i, j) \texttt{S(i, j)} S(i, j) 表示 nums \texttt{nums} nums 从下标 i \texttt{i} i 到 j \texttt{j} j 的元素之和(包含 i \texttt{i} i 和 j \texttt{j} j, i ≤ j \texttt{i} \le \texttt{j} i≤j)。
示例
示例 1:
输入:
nums
=
[-2,5,-1],
lower
=
-2,
upper
=
2
\texttt{nums = [-2,5,-1], lower = -2, upper = 2}
nums = [-2,5,-1], lower = -2, upper = 2
输出:
3
\texttt{3}
3
解释:三个区间是
[0,0]
\texttt{[0,0]}
[0,0]、
[2,2]
\texttt{[2,2]}
[2,2] 和
[0,2]
\texttt{[0,2]}
[0,2],对应的区间和分别是
-2
\texttt{-2}
-2、
-1
\texttt{-1}
-1、
2
\texttt{2}
2。
示例 2:
输入:
nums
=
[0],
lower
=
0,
upper
=
0
\texttt{nums = [0], lower = 0, upper = 0}
nums = [0], lower = 0, upper = 0
输出:
1
\texttt{1}
1
数据范围
- 1 ≤ nums.length ≤ 10 5 \texttt{1} \le \texttt{nums.length} \le \texttt{10}^\texttt{5} 1≤nums.length≤105
- -2 31 ≤ nums[i] ≤ 2 31 − 1 \texttt{-2}^\texttt{31} \le \texttt{nums[i]} \le \texttt{2}^\texttt{31} - \texttt{1} -231≤nums[i]≤231−1
- -10 5 ≤ lower ≤ upper ≤ 10 5 \texttt{-10}^\texttt{5} \le \texttt{lower} \le \texttt{upper} \le \texttt{10}^\texttt{5} -105≤lower≤upper≤105
- 保证答案是一个 32 \texttt{32} 32 位的整数
解法一
预备知识
这道题的解法涉及到前缀和。对于长度为 n n n 的数组 nums \textit{nums} nums 计算前缀和,需要创建长度为 n + 1 n + 1 n+1 的数组 sums \textit{sums} sums,对于 1 ≤ i ≤ n 1 \le i \le n 1≤i≤n 有 sums [ i ] = ∑ k = 0 i − 1 nums [ k ] \textit{sums}[i] = \sum_{k = 0}^{i - 1} \textit{nums}[k] sums[i]=∑k=0i−1nums[k],即 sums [ i ] \textit{sums}[i] sums[i] 表示数组 nums \textit{nums} nums 的前 i i i 个元素之和, sums [ 0 ] = 0 \textit{sums}[0] = 0 sums[0]=0。
对于 0 ≤ i < n 0 \le i < n 0≤i<n, sums [ i + 1 ] − sums [ i ] = nums [ i ] \textit{sums}[i + 1] - \textit{sums}[i] = \textit{nums}[i] sums[i+1]−sums[i]=nums[i],因此 sums [ i + 1 ] = sums [ i ] + nums [ i ] \textit{sums}[i + 1] = \textit{sums}[i] + \textit{nums}[i] sums[i+1]=sums[i]+nums[i],对于 sums \textit{sums} sums 中的每个元素都可以在 O ( 1 ) O(1) O(1) 的时间内计算得到,整个数组 sums \textit{sums} sums 的计算时间是 O ( n ) O(n) O(n)。
得到前缀和数组 sums \textit{sums} sums 之后,原数组 nums \textit{nums} nums 的任何子数组的和都可以在 O ( 1 ) O(1) O(1) 的时间内计算得到。对于 nums \textit{nums} nums 的下标范围 [ j , k ] [j, k] [j,k] 的子数组,其中 0 ≤ j ≤ k < n 0 \le j \le k < n 0≤j≤k<n,该子数组的和为 sums [ k + 1 ] − sums [ j ] \textit{sums}[k + 1] - \textit{sums}[j] sums[k+1]−sums[j]。
思路和算法
对于长度为 n n n 的数组,不同的区间个数是 n ( n + 1 ) 2 \dfrac{n(n + 1)}{2} 2n(n+1)。如果直接计算每个区间的区间和,则时间复杂度至少是 O ( n 2 ) O(n^2) O(n2)。由于数组 nums \textit{nums} nums 的长度最大为 1 0 5 10^5 105,因此 O ( n 2 ) O(n^2) O(n2) 的时间复杂度过高,必须使用时间复杂度更低的方法。
首先计算数组 nums \textit{nums} nums 的前缀和数组 sums \textit{sums} sums,然后根据 sums \textit{sums} sums 计算原数组的区间和。对于 0 ≤ start ≤ end ≤ n 0 \le \textit{start} \le \textit{end} \le n 0≤start≤end≤n,考虑数组 nums \textit{nums} nums 的下标范围 [ start , end − 1 ] [\textit{start}, \textit{end} - 1] [start,end−1] 的区间,该区间的长度是 end − start \textit{end} - \textit{start} end−start,区间和是 sums [ end ] − sums [ start ] \textit{sums}[\textit{end}] - \textit{sums}[\textit{start}] sums[end]−sums[start]。
当 start ≥ end \textit{start} \ge \textit{end} start≥end 时,区间的长度是 0 0 0,此时不存在区间和位于范围 [ lower , upper ] [\textit{lower}, \textit{upper}] [lower,upper] 内。当 start < end \textit{start} < \textit{end} start<end 时,区间的长度大于等于 1 1 1,将 sums \textit{sums} sums 的下标范围 [ start , end ] [\textit{start}, \textit{end}] [start,end] 的子数组分成两个更短的子数组,对两个更短的子数组内的前缀和排序之后合并,同时计算范围 [ lower , upper ] [\textit{lower}, \textit{upper}] [lower,upper] 内的区间和的个数(以下简称「区间和的个数」)。具体做法如下。
-
取 start \textit{start} start 和 end \textit{end} end 的平均值 mid \textit{mid} mid,将 sums \textit{sums} sums 的下标范围 [ start , end ] [\textit{start}, \textit{end}] [start,end] 的子数组分成下标范围 [ start , mid ] [\textit{start}, \textit{mid}] [start,mid] 和下标范围 [ mid + 1 , end ] [\textit{mid} + 1, \textit{end}] [mid+1,end] 的两个子数组。
-
对两个子数组分别排序并计算区间和的个数,得到两个升序的子数组和两个子数组分别包含的区间和的个数。
-
计算两个升序子数组之间的区间和的个数。定义指针 i i i 在左边的升序子数组中从左到右移动,即 i i i 在范围 [ start , mid ] [\textit{start}, \textit{mid}] [start,mid] 中从左到右移动,对于每个 i i i,需要在右边的升序子数组中找到相应的区间 [ left , right ) [\textit{left}, \textit{right}) [left,right),使得对于任何 left ≤ j < right \textit{left} \le j < \textit{right} left≤j<right,都有 i < j i < j i<j 且 lower ≤ sums [ j ] − sums [ i ] ≤ upper \textit{lower} \le \textit{sums}[j] - \textit{sums}[i] \le \textit{upper} lower≤sums[j]−sums[i]≤upper,则以 i i i 为区间开始位置的区间和的个数是 right − left \textit{right} - \textit{left} right−left。
-
计算两个升序子数组之间的区间和的个数之后,合并两个升序子数组。
当整个数组排序结束时即可得到整个数组的区间和个数。
计算两个升序子数组之间的区间和的个数的过程中,当 i i i 增加时,对应的 left \textit{left} left 和 right \textit{right} right 也一定增加,因此 i i i、 left \textit{left} left 和 right \textit{right} right 都是按照从小到大的顺序变化。计算两个升序子数组之间的区间和的个数与合并两个升序子数组的时间复杂度相同,计算区间和的个数并没有提升总时间复杂度,总时间复杂度和原始归并排序一样是 O ( n log n ) O(n \log n) O(nlogn)。
归并排序可以使用自顶向下的方式递归实现,也可以使用自底向上的方式迭代实现。对于同一个数组,使用自顶向下和自底向上两种方式实现的中间过程可能有所区别,但是都能得到正确的结果。
代码
以下代码为归并排序的自顶向下实现。
class Solution {
public int countRangeSum(int[] nums, int lower, int upper) {
int length = nums.length;
long[] sums = new long[length + 1];
for (int i = 0; i < length; i++) {
sums[i + 1] = sums[i] + nums[i];
}
return mergeSortAndCount(sums, lower, upper, 0, sums.length - 1);
}
public int mergeSortAndCount(long[] sums, int lower, int upper, int start, int end) {
if (start >= end) {
return 0;
}
int mid = start + (end - start) / 2;
int count = mergeSortAndCount(sums, lower, upper, start, mid) + mergeSortAndCount(sums, lower, upper, mid + 1, end);
int left = mid + 1, right = mid + 1;
for (int i = start; i <= mid; i++) {
while (left <= end && sums[left] - sums[i] < lower) {
left++;
}
while (right <= end && sums[right] - sums[i] <= upper) {
right++;
}
count += right - left;
}
merge(sums, start, mid, end);
return count;
}
public void merge(long[] sums, int start, int mid, int end) {
int currLength = end - start + 1;
long[] temp = new long[currLength];
int i = start, j = mid + 1, k = 0;
while (i <= mid && j <= end) {
if (sums[i] <= sums[j]) {
temp[k++] = sums[i++];
} else {
temp[k++] = sums[j++];
}
}
while (i <= mid) {
temp[k++] = sums[i++];
}
while (j <= end) {
temp[k++] = sums[j++];
}
System.arraycopy(temp, 0, sums, start, currLength);
}
}
以下代码为归并排序的自底向上实现。
class Solution {
public int countRangeSum(int[] nums, int lower, int upper) {
int count = 0;
int length = nums.length;
long[] sums = new long[length + 1];
for (int i = 0; i < length; i++) {
sums[i + 1] = sums[i] + nums[i];
}
int sumLength = sums.length;
for (int halfLength = 1, currLength = 2; halfLength < sumLength; halfLength *= 2, currLength *= 2) {
for (int start = 0; start < sumLength - halfLength; start += currLength) {
int mid = start + halfLength - 1;
int end = Math.min(start + currLength - 1, sumLength - 1);
int left = mid + 1, right = mid + 1;
for (int i = start; i <= mid; i++) {
while (left <= end && sums[left] - sums[i] < lower) {
left++;
}
while (right <= end && sums[right] - sums[i] <= upper) {
right++;
}
count += right - left;
}
merge(sums, start, mid, end);
}
}
return count;
}
public void merge(long[] sums, int start, int mid, int end) {
int currLength = end - start + 1;
long[] temp = new long[currLength];
int i = start, j = mid + 1, k = 0;
while (i <= mid && j <= end) {
if (sums[i] <= sums[j]) {
temp[k++] = sums[i++];
} else {
temp[k++] = sums[j++];
}
}
while (i <= mid) {
temp[k++] = sums[i++];
}
while (j <= end) {
temp[k++] = sums[j++];
}
System.arraycopy(temp, 0, sums, start, currLength);
}
}
复杂度分析
-
时间复杂度: O ( n log n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度。计算前缀和需要 O ( n ) O(n) O(n) 的时间,归并排序需要 O ( n log n ) O(n \log n) O(nlogn) 的时间,时间复杂度是 O ( n log n ) O(n \log n) O(nlogn)。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。前缀和需要 O ( n ) O(n) O(n) 的空间,自顶向下实现时需要递归调用栈的空间是 O ( log n ) O(\log n) O(logn),自底向上实现时可以省略递归调用栈的空间,无论是自顶向下实现还是自底向上实现,归并过程需要 O ( n ) O(n) O(n) 的辅助空间。
解法二
预备知识
该解法涉及到前缀和与线段树。
前缀和为数组的前缀子数组的元素和,数组的每个下标对应一个前缀和,根据前缀和可以快速得到任意一个子数组的元素和。
线段树是一种二叉搜索树,将一个区间划分成两个更短的区间。线段树中的每个叶结点都是长度为 1 1 1 的区间,称为单元区间。
线段树支持区间的快速查询和修改。对于长度为 n n n 的区间,使用线段树查询特定子区间的元素个数以及修改特定子区间内的元素个数的时间是 O ( log n ) O(\log n) O(logn)。
有时,为了降低线段树的空间复杂度,需要使用离散化。
思路和算法
为了计算区间和的个数,可以首先计算原数组的前缀和数组。对于前缀和 sum \textit{sum} sum,如果存在一个下标更小的前缀和 sum ′ \textit{sum}' sum′ 满足 lower ≤ sum − sum ′ ≤ upper \textit{lower} \le \textit{sum} - \textit{sum}' \le \textit{upper} lower≤sum−sum′≤upper,则原数组中存在一个区间和为 sum − sum ′ \textit{sum} - \textit{sum}' sum−sum′,该区间和在范围 [ lower , upper ] [\textit{lower}, \textit{upper}] [lower,upper] 中。
由于 lower ≤ sum − sum ′ ≤ upper \textit{lower} \le \textit{sum} - \textit{sum}' \le \textit{upper} lower≤sum−sum′≤upper 等价于 sum − upper ≤ sum ′ ≤ sum − lower \textit{sum} - \textit{upper} \le \textit{sum}' \le \textit{sum} - \textit{lower} sum−upper≤sum′≤sum−lower,因此对于前缀和 sum \textit{sum} sum,需要计算下标更小的前缀和中有多少个前缀和在范围 [ sum − upper , sum − lower ] [\textit{sum} - \textit{upper}, \textit{sum} - \textit{lower}] [sum−upper,sum−lower] 中,得到区间和的个数。
由于前缀和的取值范围很大,因此需要使用离散化限定范围。使用哈希集合存储所有可能的前缀和,对于前缀和 sum \textit{sum} sum,将 sum \textit{sum} sum、 sum − upper \textit{sum} - \textit{upper} sum−upper 和 sum − lower \textit{sum} - \textit{lower} sum−lower 添加到哈希集合中。然后使用列表存储哈希集合中的所有元素,将列表升序排序,并记录每个元素在列表中的名次,即每个元素在列表中的下标。
创建线段树,用于存储列表中的每个元素的名次。从左到右遍历前缀和数组,对于每个前缀和 sum \textit{sum} sum,其左边的前缀和已经遍历过,对于其左边的前缀和 sum ′ \textit{sum}' sum′,存在一个区间和 sum − sum ′ \textit{sum} - \textit{sum}' sum−sum′,如果 sum − upper ≤ sum ′ ≤ sum − lower \textit{sum} - \textit{upper} \le \textit{sum}' \le \textit{sum} - \textit{lower} sum−upper≤sum′≤sum−lower,则区间和 sum − sum ′ \textit{sum} - \textit{sum}' sum−sum′ 在范围 [ lower , upper ] [\textit{lower}, \textit{upper}] [lower,upper] 中。得到 sum \textit{sum} sum 的名次 rank \textit{rank} rank,以及 sum − upper \textit{sum} - \textit{upper} sum−upper 和 sum − lower \textit{sum} - \textit{lower} sum−lower 的名次 start \textit{start} start 和 end \textit{end} end,计算线段树的子区间 [ start , end ] [\textit{start}, \textit{end}] [start,end] 中的元素个数,该元素个数即为已经遍历过的在范围 [ sum − upper , sum − lower ] [\textit{sum} - \textit{upper}, \textit{sum} - \textit{lower}] [sum−upper,sum−lower] 中的前缀和个数,将区间和的个数增加该前缀和个数。更新区间和的个数之后,将 rank \textit{rank} rank 添加到线段树中,继续遍历后面的前缀和并更新区间和的个数。遍历结束之后,即可得到在范围 [ lower , upper ] [\textit{lower}, \textit{upper}] [lower,upper] 中的区间和的个数。
代码
class Solution {
public int countRangeSum(int[] nums, int lower, int upper) {
int count = 0;
int length = nums.length;
long[] sums = new long[length + 1];
for (int i = 0; i < length; i++) {
sums[i + 1] = sums[i] + nums[i];
}
Set<Long> set = new HashSet<Long>();
for (int i = 0; i <= length; i++) {
long sum = sums[i];
set.add(sum);
set.add(sum - upper);
set.add(sum - lower);
}
List<Long> sumsList = new ArrayList<Long>(set);
Collections.sort(sumsList);
Map<Long, Integer> ranks = new HashMap<Long, Integer>();
int size = sumsList.size();
for (int i = 0; i < size; i++) {
ranks.put(sumsList.get(i), i);
}
SegmentTree st = new SegmentTree(size);
for (int i = 0; i <= length; i++) {
long sum = sums[i];
int rank = ranks.get(sum);
long minSum = sum - upper, maxSum = sum - lower;
int start = ranks.get(minSum), end = ranks.get(maxSum);
count += st.getCount(start, end);
st.add(rank);
}
return count;
}
}
class SegmentTree {
int length;
int[] tree;
public SegmentTree(int length) {
this.length = length;
this.tree = new int[length * 4];
}
public int getCount(int start, int end) {
return getCount(start, end, 0, 0, length - 1);
}
public void add(int rank) {
add(rank, 0, 0, length - 1);
}
private int getCount(int rangeStart, int rangeEnd, int index, int treeStart, int treeEnd) {
if (rangeStart > rangeEnd) {
return 0;
}
if (rangeStart == treeStart && rangeEnd == treeEnd) {
return tree[index];
}
int mid = treeStart + (treeEnd - treeStart) / 2;
if (rangeEnd <= mid) {
return getCount(rangeStart, rangeEnd, index * 2 + 1, treeStart, mid);
} else if (rangeStart > mid) {
return getCount(rangeStart, rangeEnd, index * 2 + 2, mid + 1, treeEnd);
} else {
return getCount(rangeStart, mid, index * 2 + 1, treeStart, mid) + getCount(mid + 1, rangeEnd, index * 2 + 2, mid + 1, treeEnd);
}
}
private void add(int rank, int index, int start, int end) {
if (start == end) {
tree[index]++;
return;
}
int mid = start + (end - start) / 2;
if (rank <= mid) {
add(rank, index * 2 + 1, start, mid);
} else {
add(rank, index * 2 + 2, mid + 1, end);
}
tree[index] = tree[index * 2 + 1] + tree[index * 2 + 2];
}
}
复杂度分析
-
时间复杂度: O ( n log n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度。可能的前缀和个数是 O ( n ) O(n) O(n),每个前缀和在线段树中的查询和更新操作都需要 O ( log n ) O(\log n) O(logn) 的时间,时间复杂度是 O ( n log n ) O(n \log n) O(nlogn)。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。创建前缀和数组、前缀和列表与线段树需要 O ( n ) O(n) O(n) 的空间。
解法三
预备知识
该解法涉及到树状数组。
前缀和为数组的前缀子数组的元素和,数组的每个下标对应一个前缀和,根据前缀和可以快速得到任意一个子数组的元素和。
树状数组也称二叉索引树,由 Peter M. Fenwick 发明,因此又称 Fenwick 树。树状数组支持快速计算数组的前缀和与区间和,以及快速修改。对于长度为 n n n 的区间,使用树状数组查询特定子区间的区间和以及修改特定子区间内的元素值的时间是 O ( log n ) O(\log n) O(logn)。
有时,为了降低树状数组的空间复杂度,需要使用离散化。
思路和算法
计算区间和的个数也可以使用树状数组实现。
首先计算原数组的前缀和数组并将所有前缀和离散化,然后创建树状数组,用于存储每个可能的前缀和的名次。从左到右遍历前缀和数组,对于每个前缀和 sum \textit{sum} sum,得到 sum \textit{sum} sum 的名次 rank \textit{rank} rank,以及 sum − upper \textit{sum} - \textit{upper} sum−upper 和 sum − lower \textit{sum} - \textit{lower} sum−lower 的名次 start \textit{start} start 和 end \textit{end} end,计算树状数组的子区间 [ start , end ] [\textit{start}, \textit{end}] [start,end] 中的元素个数,该元素个数即为已经遍历过的在范围 [ sum − upper , sum − lower ] [\textit{sum} - \textit{upper}, \textit{sum} - \textit{lower}] [sum−upper,sum−lower] 中的前缀和个数,将区间和的个数增加该前缀和个数。更新区间和的个数之后,将 rank \textit{rank} rank 添加到树状数组中,继续遍历后面的前缀和并更新区间和的个数。遍历结束之后,即可得到在范围 [ lower , upper ] [\textit{lower}, \textit{upper}] [lower,upper] 中的区间和的个数。
代码
class Solution {
public int countRangeSum(int[] nums, int lower, int upper) {
int count = 0;
int length = nums.length;
long[] sums = new long[length + 1];
for (int i = 0; i < length; i++) {
sums[i + 1] = sums[i] + nums[i];
}
Set<Long> set = new HashSet<Long>();
for (int i = 0; i <= length; i++) {
long sum = sums[i];
set.add(sum);
set.add(sum - upper);
set.add(sum - lower);
}
List<Long> sumsList = new ArrayList<Long>(set);
Collections.sort(sumsList);
Map<Long, Integer> ranks = new HashMap<Long, Integer>();
int size = sumsList.size();
for (int i = 0; i < size; i++) {
ranks.put(sumsList.get(i), i);
}
BinaryIndexedTree bit = new BinaryIndexedTree(size);
for (int i = 0; i <= length; i++) {
long sum = sums[i];
int rank = ranks.get(sum);
long minSum = sum - upper, maxSum = sum - lower;
int start = ranks.get(minSum), end = ranks.get(maxSum);
count += bit.getCount(start, end);
bit.add(rank);
}
return count;
}
}
class BinaryIndexedTree {
int length;
int[] tree;
public BinaryIndexedTree(int length) {
this.length = length;
this.tree = new int[length + 1];
}
public int getCount(int start, int end) {
return getPrefixSum(end + 1) - getPrefixSum(start);
}
public void add(int index) {
index++;
while (index <= length) {
tree[index]++;
index += lowbit(index);
}
}
private int getPrefixSum(int index) {
int sum = 0;
while (index > 0) {
sum += tree[index];
index -= lowbit(index);
}
return sum;
}
private static int lowbit(int x) {
return x & (-x);
}
}
复杂度分析
-
时间复杂度: O ( n log n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度。可能的前缀和个数是 O ( n ) O(n) O(n),每个前缀和在树状数组中的查询和更新操作都需要 O ( log n ) O(\log n) O(logn) 的时间,时间复杂度是 O ( n log n ) O(n \log n) O(nlogn)。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。创建前缀和数组、前缀和列表与树状数组需要 O ( n ) O(n) O(n) 的空间。