题目描述
给整数数组 nums 和整数 k,求连续子数组的和等于 k 的个数(数组可能含负数)。
思路解析
把每个起点到每个终点的段都加一遍能做,可数据量一大就吃力,而且没利用上「前缀和之差」这个巧劲。
转折:记 pre 为从头加到当前的累加和,那么一段 (i, j] 的和 = pre[j] − pre[i]。要它 = k,就是找前面有多少个 pre[i] 正好等于 pre[j] − k。于是边走边把出现过的前缀和计数存哈希表,到每个位置查一下 pre−k 出现过几次即可。初始放 {0:1} 代表「啥都没加」。
初始化:开局先塞 {0:1}:代表「啥都没加」的前缀和 0。这是为了能数到「从下标 0 开头」那种段。pre=0、count=0。
i=0 值1 · 算 pre:加上 nums[0]=1,前缀和 pre=1。先别急着记,先拿它去查。
i=0 · 查 pre−k 命中 + 记:查 pre−k = 0,表里有 1 个 0!对应子数组就是开头的 [1],count=1。然后把 pre=1 记进表。先查后记,才不会把自己算进去。
i=1 值−1 · 算 pre(负数):加上 nums[1]=−1,pre 从 1 变回 0。负数让前缀和下降——这就是为什么不能用滑动窗口,只能靠前缀和+哈希。
i=1 · 查 pre−k 落空 + 记:查 pre−k = −1,表里没有,这一步落空,count 不加(负例)。再记 pre=0:seen[0] 从 1 变成 2——前缀和 0 已经出现过两次了,这很关键。
i=2 值1 · 算 pre:加上 nums[2]=1,pre 又回到 1。去查表。
i=2 · 查命中 2 次 → count += 2:查 pre−k = 0,表里有 2 个 0!所以一次加 2,对应整段 [1,−1,1] 和末尾 [1]。这就是存「次数」而非「有没有」的意义:同一个前缀和出现几次,就有几段以这里结尾。
记 pre=1 · 扫描结束:别忘了照例把当前 pre=1 记进表。数组扫完,count = 3:[1]、[1,−1,1]、末尾 [1] 三段,全程只扫了一遍。
「连续子数组求和/计数」的通杀套路:把区间和转成前缀和之差,再用哈希表 O(1) 查有没有需要的那个前缀和。
参考代码(Python)
1def subarraySum(nums, k):
2 from collections import defaultdict
3 seen = defaultdict(int); seen[0] = 1 # 关键:和为 0 出现 1 次
4 pre = 0; count = 0
5 for x in nums:
6 pre += x
7 count += seen[pre - k] # 先查:有几个前缀和 = pre-k
8 seen[pre] += 1 # 后记:把当前前缀和计数
9 return count复杂度分析
- 时间复杂度:O(n) —— 一遍扫描
- 空间复杂度:O(n) —— 哈希表存前缀和
套路模板
骨架就这几行。两个铁律:哈希表初始化 {0:1}、先查 pre−k 再记 pre。顺序反了答案就错。
1seen = {0: 1} # 别忘了这一项
2pre = count = 0
3for x in nums:
4 pre += x
5 count += seen.get(pre - k, 0) # 先查
6 seen[pre] = seen.get(pre, 0) + 1 # 后记易错点
- 错误写法:忘了初始化 seen = {0: 1} → 正确写法:一开始就放 {0: 1}(少了它,「从下标 0 开头、整段和为 k」的情况会被漏掉)
- 错误写法:查 k − pre → 正确写法:查 pre − k(区间和 = pre[j] − pre[i] = k,所以要找的是 pre[i] = pre[j] − k)
- 错误写法:先记 pre 再查(顺序反了) → 正确写法:先查 pre−k,再记 pre(反了会把当前这个前缀和也算进候选,凭空多算)
以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握。