题目描述
给你一个整数数组 nums 和一个整数 k,请你统计并返回该数组中和为 k 的子数组的个数。
子数组是数组中元素的连续非空序列。
示例
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
提示:
- 1 <= nums.length <= 2 * 10^4
- -1000 <= nums[i] <= 1000
- -10^7 <= k <= 10^7
解题思路总览
| 方法 | 核心思想 | 时间复杂度 | 空间复杂度 | 备注 |
|---|---|---|---|---|
| 前缀和 + 哈希表 | 用哈希表记录前缀和出现次数 | O(n) | O(n) | 推荐解法 |
| 暴力枚举 | 枚举所有子数组 | O(n^2) | O(1) | 会超时 |
| 前缀和数组 | 先计算所有前缀和再统计 | O(n^2) | O(n) | 不优化 |
一、核心解法:前缀和 + 哈希表
核心思想
对于子数组问题,核心技巧是前缀和。
设 prefix[i] = nums[0] + nums[1] + ... + nums[i]
子数组 nums[i...j] 的和 = prefix[j] - prefix[i-1]
我们要找:prefix[j] - prefix[i-1] = k
即:prefix[i-1] = prefix[j] - k
所以对于每个位置 j,我们只需要知道有多少个前缀和等于 prefix[j] - k。
关键洞察
以 nums = [1,1,1], k = 2 为例:
前缀和序列:
prefix[-1] = 0 (空数组的前缀和)
prefix[0] = 1 (nums[0])
prefix[1] = 2 (nums[0] + nums[1])
prefix[2] = 3 (nums[0] + nums[1] + nums[2])
枚举每个位置作为子数组结尾:
j=0: prefix[0]=1, 找 prefix[i-1]=1-2=-1,有 0 个
j=1: prefix[1]=2, 找 prefix[i-1]=2-2=0, 有 1 个 (nums[0..1])
j=2: prefix[2]=3, 找 prefix[i-1]=3-2=1, 有 1 个 (nums[1..2])
总计:2 个
图解
nums = [1, 1, 1], k = 2
前缀和计算过程:
空数组: prefix = 0
加 1: prefix = 1
加 1: prefix = 2
加 1: prefix = 3
哈希表演化过程:
初始状态:
hash = {0: 1} // prefix[-1] = 0,出现 1 次
处理 nums[0] = 1:
prefix = 1
prefix - k = -1
hash 中没有 -1,ans += 0
hash[1]++
hash = {0: 1, 1: 1}
处理 nums[1] = 1:
prefix = 2
prefix - k = 0
hash 中有 0,出现 1 次,ans += 1 // 子数组 [0..1]
hash[2]++
hash = {0: 1, 1: 1, 2: 1}
处理 nums[2] = 1:
prefix = 3
prefix - k = 1
hash 中有 1,出现 1 次,ans += 1 // 子数组 [1..2]
hash[3]++
hash = {0: 1, 1: 1, 2: 1, 3: 1}
最终: ans = 2
二、算法流程图
输入: nums = [1, 1, 1], k = 2
初始化:
hash = {0: 1} // prefix[-1] = 0
prefix = 0
ans = 0
i=0, num=1:
prefix = 0 + 1 = 1
prefix - k = -1
hash[-1]? 不存在,ans += 0
hash[1] = 1
i=1, num=1:
prefix = 1 + 1 = 2
prefix - k = 0
hash[0] = 1,ans += 1 // 找到 1 个子数组
hash[2] = 1
i=2, num=1:
prefix = 2 + 1 = 3
prefix - k = 1
hash[1] = 1,ans += 1 // 找到 1 个子数组
hash[3] = 1
输出: ans = 2
三、完整代码实现
cpp
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> hash;
int prefix = 0; // 当前前缀和
int ans = 0; // 答案计数
// 初始化:空数组的前缀和为 0,出现 1 次
hash[0] = 1;
for (int num : nums) {
prefix += num; // 更新前缀和
// 找有多少个前缀和等于 prefix - k
// 即有多少个子数组和为 k
if (hash.find(prefix - k) != hash.end()) {
ans += hash[prefix - k];
}
// 记录当前前缀和出现的次数
hash[prefix]++;
}
return ans;
}
};
四、逐行解析
cpp
unordered_map<int, int> hash;
- 哈希表:key = 前缀和,value = 该前缀和出现的次数
cpp
int prefix = 0;
int ans = 0;
prefix:当前遍历位置的前缀和ans:累计满足条件的子数组个数
cpp
hash[0] = 1;
- 初始化:空数组的前缀和为 0,初始出现 1 次
- 这个初始化很重要,因为子数组可能从索引 0 开始
cpp
for (int num : nums) {
prefix += num;
- 遍历数组,累加当前前缀和
cpp
if (hash.find(prefix - k) != hash.end()) {
ans += hash[prefix - k];
}
- 如果存在前缀和等于
prefix - k - 说明存在子数组和为
k - 加上该前缀和出现的次数
cpp
hash[prefix]++;
- 记录当前前缀和出现的次数
五、为什么初始化 hash[0] = 1?
当子数组从索引 0 开始时:
nums = [3, 1, 2], k = 6
处理 nums[0] = 3:
prefix = 3
prefix - k = -3
hash[-3] 不存在
如果没有初始化 hash[0] = 1,会漏掉 [3, 1, 2] 这个子数组!
因为 [3, 1, 2] 的和 = prefix[2] - prefix[-1]
而 prefix[-1] = 0,hash[0] = 1
初始化 hash[0] = 1 就是为了处理从 0 开始的子数组。
六、与第 53 题(最大子数组和)对比
| 维度 | 第 53 题 最大子数组和 | 第 560 题 和为 K 的子数组 |
|---|---|---|
| 问题类型 | 求最大和 | 统计个数 |
| 方法 | 贪心/DP | 前缀和 + 哈希表 |
| 转移 | dp[i] = max(dp[i-1] + nums[i], nums[i]) | 统计 prefix - k 出现的次数 |
| 时间复杂度 | O(n) | O(n) |
| 空间复杂度 | O(1) | O(n) |
七、复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 备注 |
|---|---|---|---|
| 前缀和 + 哈希表 | O(n) | O(n) | 推荐 |
| 暴力枚举 | O(n^2) | O(1) | 会超时 |
| 前缀和数组 | O(n^2) | O(n) | 不优化 |
详细分析:
时间复杂度:
遍历一次数组:O(n)
哈希表查找/插入:平均 O(1)
总计:O(n)
空间复杂度:
哈希表最坏情况 O(n) 个不同的前缀和
输出数组不算额外空间
总计:O(n)
八、边界情况分析
| 情况 | 处理方式 |
|---|---|
| 全是正数 | 正常处理 |
| 全是负数 | 正常处理 |
| 有 0 | 正常处理,0 不影响前缀和 |
| k = 0 | 正常处理,找两段子数组和相等 |
| 空数组 | 不进入循环,返回 0 |
示例:k = 0
nums = [0, 0, 0], k = 0
处理过程:
i=0: prefix=0, prefix-k=0, hash[0]=1, ans+=1 // [0]
i=1: prefix=0, prefix-k=0, hash[0]=2, ans+=2 // [0], [0,0]
i=2: prefix=0, prefix-k=0, hash[0]=3, ans+=3 // [0], [0,0], [0,0,0]
结果:ans = 6
验证:[0] 在三个位置各出现一次,[0,0] 出现两次,[0,0,0] 出现一次
九、面试追问 FAQ
| 问题 | 回答要点 |
|---|---|
| Q: 为什么前缀和能解决这个问题? | 子数组和 = 前缀和差,我们用哈希表记录前缀和出现次数,快速查找差为 k 的情况 |
| Q: 哈希表的作用是什么? | 将查找前缀和出现次数从 O(n) 降到 O(1),实现 O(n) 时间复杂度 |
| Q: 为什么初始化 hash[0] = 1? | 处理从索引 0 开始的子数组,此时 prefix - k = 0 - k = -k |
| Q: 时间复杂度为什么是 O(n)? | 遍历一次,哈希表操作均摊 O(1) |
| Q: 空间复杂度为什么是 O(n)? | 哈希表最坏情况存储 n 个不同的前缀和 |
| Q: 能否用数组代替哈希表? | 不能,因为前缀和范围可能很大(负数到正数),哈希表更灵活 |
十、相关题目
| 题目编号 | 题目名称 | 难度 | 核心差异 |
|---|---|---|---|
| 560 | 和为 K 的子数组 | 中等 | 基础题,统计个数 |
| 53 | 最大子数组和 | 中等 | 求最大和,贪心/DP |
| 1248 | 统计「优美子数组」 | 中等 | 统计奇数个数为 K |
| 974 | 和可被 K 整除的子数组 | 中等 | 前缀和模 K |
| 剑指 Offer 42 | 连续子数组的最大和 | 简单 | 求最大和 |
| 724 | 寻找数组的中心下标 | 简单 | 前缀和基础 |
十一、总结
| 要点 | 内容 |
|---|---|
| 核心思想 | 前缀和 + 哈希表 |
| 关键公式 | 子数组和 = prefix[j] - prefix[i-1] |
| 哈希作用 | 快速查找 prefix - k 出现的次数 |
| 初始化 | hash[0] = 1,处理从 0 开始的子数组 |
| 时间复杂度 | O(n) |
| 空间复杂度 | O(n) |
| 关键洞察 | 用空间换时间,将 O(n^2) 优化到 O(n) |
和为 K 的子数组是前缀和思想的经典应用,通过哈希表快速查找前缀和差为 k 的情况,将暴力枚举的 O(n^2) 优化到 O(n)。