今天为大家介绍一种数组中常用的算法----前缀和。
前缀和的解题思想
前缀和的题目解题思维比较固定,即当我们循环数组到下标N时,需要用到数组前N-1项的计算的结果(这里不一定非要是和,也可能是积等),此时我们就该考虑是否应该通过计算数组循环过程中的累计值的方式简化解题,如此便有了前缀和的解题思想。
了解了思想,接下来下来就该考虑,这个累计的结果我们该通过什么方式保存起来呢?
- 题目明确要求不允许使用额外空间的,直接原地修改数组。
- 不限制空间复杂度时,最好额外开辟空间计算,避免数据污染。
- 计算时如果每次只需要获取前一次的累计结果,可以通过数组的方式存储每次获取数组末尾元素的值。
- 如果每次计算需要获取前几次或更多次的结果进行对比时,推荐哈希表的方式,这样可以压缩时间复杂度。
让我们先来通过一道题目,熟悉下前缀和的思维,并且了解下关于前缀和边界的这个易错点。
题目
给定一个整数数组和一个整数 k ,请找到该数组中和为 k 的连续子数组的个数。
提示:
- 1 <= nums.length <= 2 * 10 ^ 4
- -1000 <= nums[i] <= 1000
- -10 ^ 7 <= k <= 10 ^ 7
示例
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2
解释: 此题 [1,1] 与 [1,1] 为两种不同的情况示例 2 :
输入:nums = [1,2,3], k = 3
输出: 2。
分析
这道题目非常简洁,就是求数组中何为整数k的连续子数组个数。
如果这道题的取值没有负数,那就是标准的滑窗问题,但因为有了负数,滑窗思想不能用了。
通过分析,这道题应该属于我们上面列举四种情况的最后一种。具体思路如下:
- 初始化一个空的哈希表和pre_sum=0的前缀和变量
- 设置返回值ret = 0,用于记录满足题意的子数组数量
- 循环数组的过程中,通过原地修改数组的方式,计算数组的累加和
- 将当前累加和减去整数K的结果,在哈希表中查找是否存在
- 如果存在该key值,证明以数组某一点为起点到当前位置满足题意,ret加等于将该key值对应的value
- 判断当前的累加和是否在哈希表中,若存在value+1,若不存在value=1
- 最终返回ret即可
但在这里要注意刚才说到的前缀和边界问题。
我们在计算这种场景时,需要考虑如果以数组nums[0]为开头的连续子数组就满足题意呢?
此时候我们的哈希表还是空的,没办法计算前缀和!所以遇到这类题目,都需要在哈希表中默认插入一个{0:1}的键值对,
用于解决从数组开头的连续子数组满足题意的特殊场景。
解题
class Solution {
public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>(); // 前缀 -> 出现次数
map.put(0, 1); // 以防nums[0]就是k
// 记录出每一个前缀和就相当于记录了所有子数组的和,
// 因为两个前缀和相减就是它们之间子数组的和
int pre = 0, cnt = 0;
for (int n : nums) {
pre += n;//1
// 已知当前的前缀pre和之前某个前缀X,且要求pre-X=k,那么X=pre-k。X的出现次数就是此时以n结尾的子数组的合法次数
cnt += map.getOrDefault(pre - k, 0);
map.put(pre, map.getOrDefault(pre, 0) + 1);
}
return cnt;
}
}