这段代码是解决 LeetCode 560. 和为 K 的子数组 问题的经典前缀和 + 哈希表优化解法,核心目标是高效统计数组中和为 k 的连续子数组的个数,时间复杂度优化到 O (n),空间复杂度 O (n)。下面从问题理解→核心思路→代码逐行解析→实例演示四个维度详细讲解:
一、问题理解
问题要求
给定一个整数数组 nums 和一个整数 k,请你统计并返回该数组中和为 k 的连续子数组的个数。
- 子数组:连续的元素序列(必须相邻,如
nums[1..3]是子数组,nums[1]和nums[3]不相邻则不是)。 - 示例:输入
nums = [1,1,1],k = 2,输出2(子数组[0,1]和[1,2]的和均为 2);输入nums = [1,2,3],k = 3,输出2(子数组[0,0]之和 1+2=3?不,实际是[0,1]之和 3,[2,2]之和 3,共 2 个)。
二、核心思路:前缀和 + 哈希表计数
暴力解法(枚举所有子数组并计算和)时间复杂度为 O (n²),效率极低。该解法通过前缀和转化问题,结合哈希表记录前缀和出现次数,实现线性时间求解,核心思路:
-
前缀和定义:设
preSum[i]表示数组前i个元素的和(即preSum[i] = nums[0] + nums[1] + ... + nums[i-1])。例如:nums = [a,b,c],则preSum[0] = 0,preSum[1] = a,preSum[2] = a+b,preSum[3] = a+b+c。 -
子数组和与前缀和的关系:子数组
nums[j..i-1](从索引j到i-1)的和 =preSum[i] - preSum[j]。题目要求子数组和为k,即preSum[i] - preSum[j] = k→ 等价于preSum[j] = preSum[i] - k。 -
哈希表计数:用哈希表
preSumCount记录每个前缀和preSum出现的次数。对于当前前缀和preSum[i],若preSum[i] - k存在于哈希表中,则说明有preSumCount[preSum[i]-k]个j满足条件,即有这么多个子数组和为k,累加至结果ans。 -
初始化处理:初始时
preSumCount存入(0, 1),原因是:当preSum[i] = k时,preSum[i] - k = 0,此时需要统计到preSum[0] = 0这一情况(对应子数组从索引 0 开始到i-1)。
三、代码逐行解析
java
运行
import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
class Solution {
public int subarraySum(int[] nums, int k) {
// 1. 记录和为k的子数组总数(结果)
int ans = 0;
// 2. 记录当前前缀和(preSum[i] = nums[0] + ... + nums[i-1])
int preSum = 0;
// 3. 哈希表:key=前缀和,value=该前缀和出现的次数
Map<Integer, Integer> preSumCount = new HashMap<>();
// 4. 初始化:前缀和为0的情况出现1次(对应子数组从0开始的场景)
preSumCount.put(0, 1);
// 5. 遍历数组,更新前缀和并统计结果
for (int num : nums) {
// 5.1 更新当前前缀和(加上当前元素)
preSum += num;
// 5.2 核心:若preSum - k存在于哈希表中,说明有对应子数组和为k
if (preSumCount.containsKey(preSum - k)) {
ans += preSumCount.get(preSum - k);
}
// 5.3 将当前前缀和加入哈希表(次数+1,若已存在则更新)
preSumCount.put(preSum, preSumCount.getOrDefault(preSum, 0) + 1);
}
// 6. 返回结果
return ans;
}
}
四、实例演示(直观理解过程)
以测试用例 nums = [1,1,1],k = 2 为例,演示前缀和、哈希表和结果的变化:
| 步骤 | 遍历元素 num | 当前 preSum(累加后) | preSum - k | preSumCount 中是否存在 preSum - k | 本次累加 ans(加上对应次数) | preSumCount 更新(key=preSum,value = 次数) | ans 累计 |
|---|---|---|---|---|---|---|---|
| 初始 | - | 0 | - | - | - | {(0,1)} | 0 |
| 1 | 1 | 0 + 1 = 1 | 1 - 2 = -1 | 否(哈希表中无 - 1) | +0 | 加入 (1,1) → {(0,1), (1,1)} | 0 |
| 2 | 1 | 1 + 1 = 2 | 2 - 2 = 0 | 是(0 的次数为 1) | +1 | 加入 (2,1) → {(0,1), (1,1), (2,1)} | 1 |
| 3 | 1 | 2 + 1 = 3 | 3 - 2 = 1 | 是(1 的次数为 1) | +1 | 加入 (3,1) → {(0,1), (1,1), (2,1), (3,1)} | 2 |
最终结果:ans=2,与预期一致(对应子数组 [0,1] 和 [1,2])。
另一个测试用例 nums = [1,2,3],k=3:
| 步骤 | num | preSum | preSum -k | 是否存在 | 累加 ans | preSumCount 更新 | ans 累计 |
|---|---|---|---|---|---|---|---|
| 初始 | - | 0 | - | - | - | {(0,1)} | 0 |
| 1 | 1 | 1 | 1-3=-2 | 否 | +0 | {(0,1), (1,1)} | 0 |
| 2 | 2 | 3 | 3-3=0 | 是(次数 1) | +1 | {(0,1), (1,1), (3,1)} | 1 |
| 3 | 3 | 6 | 6-3=3 | 是(次数 1) | +1 | {(0,1), (1,1), (3,1), (6,1)} | 2 |
最终结果:ans=2(对应子数组 [0,1](1+2=3)和 [2,2](3=3))。
五、关键细节解析
1. 为什么初始化preSumCount.put(0, 1)?
这是为了处理子数组从索引 0 开始的情况。例如:当 preSum = k 时(如上述第二个测试用例中 preSum=3,k=3),preSum - k = 0,此时哈希表中 0 的次数为 1,会将这一情况统计到 ans 中,即子数组 [0..i-1] 是有效的。若不初始化,这种情况会被遗漏。
2. 哈希表的作用:避免重复计算前缀和次数
哈希表记录了每个前缀和出现的次数,例如:若 preSum = 5 出现过 3 次,当后续 preSum[i] - k = 5 时,说明有 3 个不同的 j 满足条件,即有 3 个和为 k 的子数组,直接累加 3 即可,无需重新计算。
3. 为什么能统计所有连续子数组?
因为遍历过程中,preSum 是 “累加当前元素” 的过程,覆盖了所有可能的前缀和 preSum[i];而 preSum[i] - k 的查询,覆盖了所有可能的 preSum[j],因此所有满足 preSum[i] - preSum[j] = k 的子数组 [j..i-1] 都会被统计。
六、复杂度分析
-
时间复杂度:O(n)。仅遍历数组一次(O (n)),哈希表的
put和get操作均为 O (1)(平均情况),因此总时间复杂度为线性级别。 -
空间复杂度:O(n)。哈希表
preSumCount最多存储n+1个不同的前缀和(数组长度为 n 时,前缀和最多有 n+1 个:preSum[0]到preSum[n]),因此空间复杂度为 O (n)。
七、总结
该解法的核心是 **“前缀和转化问题 + 哈希表记录频率”:通过前缀和将 “子数组和为 k” 转化为 “两个前缀和的差值为 k”,再用哈希表高效统计符合条件的前缀和出现次数,最终在 O (n) 时间内解决问题。这种 “前缀和 + 哈希表” 的思路是解决子数组和问题 ** 的经典范式,可迁移到 “和为 0 的子数组”“子数组和的范围” 等类似问题中。

被折叠的 条评论
为什么被折叠?



