实例:leet560
本题的关键在于如何快速得到某个子数组的和,比如说给你一个数组nums
,让你实现一个接口sum(i, j)
,这个接口要返回nums[i..j]
的和,而且会被多次调用,怎么实现这个接口?因为接口要被多次调用,显然不能每次都去遍历nums[i..j]
,有没有一种快速的方法在 O(1) 时间内算出nums[i..j]
呢?这就需要前缀和技巧了。
前缀和的思路是这样的,对于一个给定的数组nums
,我们额外开辟一个前缀和数组进行预处理:
int n = nums.length;
// 前缀和数组
int[] preSum = new int[n + 1];
preSum[0] = 0;
for (int i = 0; i < n; i++)
preSum[i + 1] = preSum[i] + nums[i];
这个前缀和数组preSum
的含义也很好理解,preSum[i]
就是nums[0..i-1]
的和。那么如果我们想求nums[i..j]
的和,只需要一步操作preSum[j+1]-preSum[i]
即可,而不需要重新去遍历数组了。
回到这个子数组问题,我们想求有多少个子数组的和为 k,借助前缀和技巧很容易写出一个解法:
class Solution {
public int subarraySum(int[] nums, int k) {
int n=nums.length;
int[] presum=new int[n+1];
presum[0]=0;
for(int i=0;i<n;i++){
presum[i+1]=nums[i]+presum[i];
}
int ansNum=0;
for(int i=1;i<=n;i++){
for(int j=0;j<i;j++){
if(presum[i]-presum[j]==k){
ansNum++;
}
}
}
return ansNum;
}
}
当然,这个解法还是可以继续优化的:
上面虽然使用了前缀和,但是后面用了双重遍历,即每遍历到一个值,都要与之前的 每个值 做相减的操作,这样时间复杂度非常高,对于这种“遍历数组来看 某个值 是否存在”的问题可以用 哈希表来优化。
对前缀和数组进行哈希表存值,然后对前缀和数组进行一个遍历,每到一个值,就查表看 presum[i] - target是否存在以及有几个。当然之前的遍历以生成前缀和数组的过程也可以整合进来,即整个过程变为:遍历原数组,每遇到一个新的值,就去生成前缀和的哈希表,同时看此时的sum - target 是否存在。
class Solution {
public int subarraySum(int[] nums, int k) {
int n=nums.length;
HashMap<Integer,Integer> presum = new HashMap<>();
presum.put(0,1);
int ansNum=0;
int sum_i=0;
for(int i=0;i<n;i++){
sum_i=sum_i+nums[i];
int sum_j=sum_i-k;
if(presum.containsKey(sum_j)){
ansNum=ansNum+presum.get(sum_j);
}
presum.put(sum_i,presum.getOrDefault(sum_i,0)+1);
}
return ansNum;
}
}
前缀和不难,却很有用,主要用于处理数组区间的问题。
比如说,让你统计班上同学考试成绩在不同分数段的百分比,也可以利用前缀和技巧:
int[] scores; // 存储着所有同学的分数
// 试卷满分 150 分
int[] count = new int[150 + 1]
// 记录每个分数有几个同学
for (int score : scores)
count[score]++
// 构造前缀和
for (int i = 1; i < count.length; i++)
count[i] = count[i] + count[i-1];
这样,给你任何一个分数段,你都能通过前缀和相减快速计算出这个分数段的人数,百分比也就很容易计算了。
但是,稍微复杂一些的算法问题,不止考察简单的前缀和技巧。比如本文探讨的这道题目,就需要借助前缀和的思路做进一步的优化,借助哈希表记录额外的信息。可见对题目的理解和细节的分析能力对于算法的优化是至关重要的。