
✨ 把代码写进星轨,用逻辑丈量宇宙。
| 导航 | 链接 |
|---|---|
| 个人主页 | 🏠 星轨初途 |
| 基础语言专栏 | 💻 C语言、📚 数据结构 |
| 刷题实战专栏 | 🚀 算法及编程题分享、📝 LeetCode 每日一题 |
LeetCode 560:和为 K 的子数组
-
- 题目描述
- 从哪里开始想?
- [方法一:前缀和 + 枚举区间](#方法一:前缀和 + 枚举区间)
- [方法二:前缀和 + 哈希表](#方法二:前缀和 + 哈希表)
-
- 用示例走一遍
- [为什么要先写 `mp0 = 1`?](#为什么要先写
mp[0] = 1?) - 代码实现
- 为什么要先查找,再记录当前前缀和?
- 还能再简化一点
- 复杂度分析
- 为什么这道题不适合滑动窗口?
- 两种方法对比
- [总结 `(๑•̀ㅂ•́)و✧`](#总结
(๑•̀ㅂ•́)و✧)

🚀 每日一题继续!今天碰到的是一道前缀和经典题。
题目让我们统计"和恰好等于 k 的连续子数组个数"。看到连续区间求和,前缀和很容易想到;真正有意思的地方在于,怎样把枚举区间的 O(n²) 再压到 O(n)。
这次哈希表又要登场啦 ( •̀ ω •́ )✧
文章目录
-
- 题目描述
- 从哪里开始想?
- [方法一:前缀和 + 枚举区间](#方法一:前缀和 + 枚举区间)
- [方法二:前缀和 + 哈希表](#方法二:前缀和 + 哈希表)
-
- 用示例走一遍
- [为什么要先写 `mp0 = 1`?](#为什么要先写
mp[0] = 1?) - 代码实现
- 为什么要先查找,再记录当前前缀和?
- 还能再简化一点
- 复杂度分析
- 为什么这道题不适合滑动窗口?
- 两种方法对比
- [总结 `(๑•̀ㅂ•́)و✧`](#总结
(๑•̀ㅂ•́)و✧)
题目描述
给定一个整数数组 nums 和一个整数 k,统计数组中和为 k 的连续非空子数组个数。
示例一:
text
输入:nums = [1,1,1], k = 2
输出:2
满足条件的两个子数组分别是:
text
nums[0...1] = [1,1]
nums[1...2] = [1,1]
示例二:
text
输入:nums = [1,2,3], k = 3
输出:2
对应的子数组是 [1,2] 和 [3]。
从哪里开始想?
最直接的办法是枚举子数组的左右端点,再计算这段区间的和。
如果每次都重新累加区间元素,时间复杂度会达到 O(n³)。这个做法显然有点笨,因为大量区间存在重叠,同一批数字会被反复计算。
前缀和可以解决"重复求区间和"的问题。
设:
text
pre[i] 表示 nums[0...i-1] 的元素之和
于是区间 [left, right] 的和为:
text
pre[right + 1] - pre[left]
查询一次区间和只需要 O(1),总复杂度便从 O(n³) 降到了 O(n²)。
方法一:前缀和 + 枚举区间
先求出前缀和数组,再枚举子数组的左右端点。
cpp
class Solution
{
public:
int subarraySum(vector<int>& nums, int k)
{
int n = nums.size();
vector<int> pre(n + 1, 0);
for (int i = 0; i < n; ++i)
{
pre[i + 1] = pre[i] + nums[i];
}
int ans = 0;
for (int left = 0; left < n; ++left)
{
for (int right = left; right < n; ++right)
{
if (pre[right + 1] - pre[left] == k)
{
++ans;
}
}
}
return ans;
}
};
复杂度分析
- 时间复杂度:
O(n²); - 空间复杂度:
O(n)。
前缀和已经帮我们快速算出了区间和,但左右端点仍然需要两层循环。还能不能继续优化?
可以。把公式稍微变一下,左端点就不用枚举了。
方法二:前缀和 + 哈希表
假设当前遍历到位置 i,当前前缀和为 pre[i]。如果前面存在一个前缀和 pre[j],满足:
text
pre[i] - pre[j] = k
移项可得:
text
pre[j] = pre[i] - k
问题一下就变了:
遍历到当前前缀和时,前面出现过多少次
pre[i] - k?
我们只需要用哈希表记录每个前缀和出现的次数。每得到一个新的前缀和 sum,就去查找 sum - k:
text
找到一次,说明有一个子数组的和为 k;
找到多次,说明有多个不同起点都能组成答案。
因此哈希表保存的不是"某个前缀和是否出现",而是"它出现了几次"。
用示例走一遍
对于:
text
nums = [1,1,1], k = 2
初始化:
text
mp[0] = 1
遍历过程如下:
| 当前数字 | 当前前缀和 sum |
查找 sum-k |
出现次数 | 答案 cnt |
|---|---|---|---|---|
| 1 | 1 | -1 | 0 | 0 |
| 1 | 2 | 0 | 1 | 1 |
| 1 | 3 | 1 | 1 | 2 |
最终答案为 2。
为什么要先写 mp[0] = 1?
0 表示还没有取任何元素时的前缀和。
如果当前前缀和本身就等于 k:
text
sum - k = 0
这说明从数组下标 0 开始到当前位置的子数组符合要求。提前放入一次前缀和 0,就能把这种情况自然地统计进去。
例如:
text
nums = [1,2], k = 3
遍历到 2 时,当前前缀和为 3。我们查找 3 - 3 = 0,正好在哈希表中找到一次,于是 [1,2] 被计入答案。
代码实现
下面是本次打卡使用的写法,使用 brr 保存所有前缀和:
cpp
class Solution
{
public:
int subarraySum(vector<int>& nums, int k)
{
vector<int> brr(nums.size() + 1, 0);
unordered_map<int, int> mp;
// 空前缀出现一次,用来统计从下标 0 开始的合法子数组
mp[0] = 1;
int cnt = 0;
for (int i = 0; i < nums.size(); ++i)
{
// 计算当前位置的前缀和
brr[i + 1] = brr[i] + nums[i];
// 如果前面出现过 brr[i + 1] - k,
// 那么这些位置到当前下标都能组成和为 k 的子数组
if (mp.find(brr[i + 1] - k) != mp.end())
{
cnt += mp[brr[i + 1] - k];
}
// 当前前缀和加入哈希表,供后面的元素使用
mp[brr[i + 1]]++;
}
return cnt;
}
};
为什么要先查找,再记录当前前缀和?
子数组必须非空。
当 k = 0 时,如果先执行:
cpp
mp[sum]++;
再查找 sum - k,当前前缀和会把自己也统计进去,相当于计算了一个长度为 0 的空区间。
所以顺序不能换:
text
先寻找以前的前缀和
再记录当前前缀和
还能再简化一点
哈希表只会查询以前出现过的前缀和,并不需要通过下标访问整个 brr 数组。用一个变量 sum 维护当前前缀和就够了:
cpp
class Solution
{
public:
int subarraySum(vector<int>& nums, int k)
{
unordered_map<int, int> mp;
mp[0] = 1;
int sum = 0;
int ans = 0;
for (int num : nums)
{
sum += num;
if (mp.find(sum - k) != mp.end())
{
ans += mp[sum - k];
}
mp[sum]++;
}
return ans;
}
};
这两份代码的时间、空间复杂度相同。第二份只是省掉了前缀和数组,常数空间更小,写起来也更利落。
复杂度分析
- 时间复杂度:平均
O(n),每个元素只遍历一次,哈希表查询和插入平均为O(1); - 空间复杂度:
O(n),最坏情况下每个前缀和都不同。
为什么这道题不适合滑动窗口?
题目中的 nums[i] 可能是负数。
如果数组全是正数,窗口扩大后区间和只会增加,缩小后只会减少,我们可以根据和的大小移动左右指针。但负数加入窗口后,区间和可能变大,也可能变小,滑动窗口失去了明确的移动方向。
例如:
text
nums = [2,-1,2], k = 3
加入 -1 后窗口和反而减小,仅根据当前和与 k 的关系,无法判断应该移动哪一侧。
前缀和配合哈希表不依赖单调性,因此可以正常处理正数、负数和 0。
两种方法对比
| 方法 | 做法 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 前缀和 + 枚举区间 | 枚举左右端点,O(1) 查询区间和 |
O(n²) |
O(n) |
| 前缀和 + 哈希表 | 统计 sum-k 以前出现的次数 |
O(n) |
O(n) |
总结 (๑•̀ㅂ•́)و✧
这道题的突破口藏在前缀和公式里:
text
pre[i] - pre[j] = k
把它变形成:
text
pre[j] = pre[i] - k
原本需要枚举的左端点,就变成了哈希表里一次平均 O(1) 的查询。
还有两个细节别忘啦:mp[0] = 1 负责接住从下标 0 开始的子数组,查询必须放在记录当前前缀和之前,避免把空区间算进答案。
🎉 前缀和与哈希表成功会师!以后再遇到"连续子数组 + 区间和 + 统计个数",就可以往这个方向想一想。每日一题继续出发,下一题见!