前缀和属于较为常见的算法,用于求取数据集合中某个范围里所有元素的和,先来一道标准的前缀和题目。
从题目要求看到要求的是数组中某个范围所有元素的和,再联想前缀和的使用场景,可以发现这要求就是为前缀和量身定制的。
前缀和顾名思义,就是指定下标的元素前面所有元素值的和,不同元素的前缀和都不一样,比如说原数组(题目所给数组)下标0的元素前面没有元素,那么其前缀和就是0,原数组下标3的前缀和就是下标由0到2所有元素值的和,由于题目需要求取多个范围的和,需要用一个长度为n+1的数组来存储前缀和(假设原数组长度为n),将其命名为前缀和数组,使用长度为n+1的数组的原因是需要一个额外空间来存储全部元素值的和。
那么当求由i到j范围的数据值和,我们只需要用前缀和数组中下标j+1的元素减去下标为i的元素即可,为什么这里用下标j+1的元素减?这是因为下标j+1的元素存储着的是原数组中下标由0到j的所有元素的和,而下标i的元素存储着下标由0到i-1的所有元素的和,
说完计算区域和,再来说说如何计算前缀和,由于下标0的元素前缀和为0,所以可以直接跳过,从下标1开始遍历,那么此刻遍历的元素前缀和就等于上一个元素的前缀和加上一个元素的值。
最终代码如下所示。
class NumArray {
int[] preSum;
public NumArray(int[] nums) {
int len=nums.length;
preSum=new int[len+1];
for(int i=1;i<preSum.length;i++){
preSum[i]=preSum[i-1]+nums[i-1];
}
}
public int sumRange(int left, int right) {
return preSum[right+1]-preSum[left];
}
}
再来看看一道稍微复杂的题目。
如上图红框所示,题目的关键是和为k和连续子数组,换句话说,是求数组中某个范围的元素值的总和。 那么就可以采用前缀和的方式,由于前缀和方法的时间复杂度为O(n),空间复杂度也为O(n),所以这里不太需要担心意题目中元素数量的问题。
这里更需要注意的是题目指定了这个范围和为k,设前缀和数组为preSum,范围由下标i到下标j,那么i到j的范围和就等于preSum[j+1]-preSum[i],我们所要求的就是preSum[j+1]-preSum[i]==k,变换一下,preSum[i]=preSum[j+1]-k,我们就把问题转换为求preSum[i]的数量了。
下标j是范围右边界,此时正在进行遍历,获取preSum[j+1]不存在问题,k为题目所给,也没有问题,下标i为范围左边界,是之前遍历的,如何判断之前是否有符合要求的preSum[i]存在呢?此外,由于前缀和可能会出现相同值的情况,所以还需要存储该前缀和出现的次数,也就是前缀和到出现次数的映射。
小结一下,我们的需求是判断preSum[i]是否存在,且要建立preSum[i]到出现次数的映射,简单的数组已经不能满足我们的需求了,HashMap就很好地符合了我们的要求。
确定好需要使用的数据结构后,就可以确定算法的大致流程了,首先遍历数组,计算此刻遍历元素的前缀和preSum,判定大小为preSum-k的值是否存在,如果存在,则用res加preSum-k的出现次数,后更新在HashMap中preSum出现的次数,代码如下所示。
class Solution {
public int subarraySum(int[] nums, int k) {
HashMap<Integer,Integer> preFrequent=new HashMap<Integer,Integer>();
preFrequent.put(0,1);
int preSum=0;
int count=0;
int len=nums.length;
for(int i=0;i<len;i++){
preSum+=nums[i];
count+=preFrequent.getOrDefault(preSum-k,0);
preFrequent.put(preSum,preFrequent.getOrDefault(preSum,0)+1);
}
return count;
}
}