说实话,这题我第一次看到的时候,心里想的是:不就是找子数组和等于 k 吗?两层 for 循环不就完事了?
然后我写了,提交了,超时了。
改了又改,还是超时。最后才发现,这题的精髓根本不是暴力枚举,而是一个叫 "前缀和" 的东西配上哈希表 ------ 速度直接从 O (n²) 干到 O (n)。
今天就聊聊我是怎么从超时一步步爬到 beat 90% 的。
题目说的啥
给你一个整数数组 nums 和一个整数 k,让你找出数组中和为 k 的连续子数组的个数。
就这么简单。比如 nums = 1,1,1,k = 2,答案是 2,因为 1,1 出现了两次。
我一开始的憨憨写法
拿到题第一反应,穷举呗。
固定左端点,右端点往右滑,累加求和,等于 k 就计数加一。
js
var subarraySum = function(nums, k) {
let count = 0;
for (let i = 0; i < nums.length; i++) {
let sum = 0;
for (let j = i; j < nums.length; j++) {
sum += nums[j];
if (sum === k) count++;
}
}
return count;
};
逻辑没问题,但数组一大就凉。O (n²) 的时间复杂度,LeetCode 直接给我甩个 Time Limit Exceeded。
我心想:行吧,得想办法优化。
前缀和是个啥
前缀和这个概念,说白了就是:用一个数组 pre,pre i 表示 nums 前 i 个元素的和。
比如 nums = 1, 2, 3,那 pre = 0, 1, 3, 6。pre 0 = 0 是个哨兵,方便计算。
有了前缀和,任意子数组 nums j...i-1 的和就等于 pre i - pre j。
我们要找的就是 pre i - pre j = k 的情况,也就是 pre j = pre i - k。
换句话说,遍历到 pre i 的时候,我只需要知道:之前出现过多少次 pre i - k?
这个 "之前出现过多少次",用哈希表存就完事了。
结合代码走一遍
拿 nums = 1, 1, 1,k = 2 来举例。
前缀和数组 pre = 0, 1, 2, 3。
- 遍历 pre 0 = 0:查 map 里有没有 0 - 2 = -2,没有,ans 不变。把 0 存进 map,map = {0: 1}
- 遍历 pre 1 = 1:查有没有 1 - 2 = -1,没有。把 1 存进去,map = {0:1, 1:1}
- 遍历 pre 2 = 2:查有没有 2 - 2 = 0,有!出现了 1 次,ans += 1。把 2 存进去
- 遍历 pre 3 = 3:查有没有 3 - 2 = 1,有!出现了 1 次,ans += 1。最终 ans = 2
完美,和预期一致。
这里有个细节要注意:先查 map 再存当前值。不然如果 k = 0 的话,会把自己也算进去,就错了。这个坑我踩过,调试了半天才发现。
最终代码
js
var subarraySum = function(nums, k) {
let count = 0;
const n = nums.length;
// prefixSum[i] 表示 nums 前 i 个元素的和,prefixSum[0] = 0 是哨兵
const prefixSum = new Array(n + 1).fill(0);
for (let i = 1; i <= n; i++) {
prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
}
// 哈希表:key 是前缀和的值,value 是该值出现的次数
const sumCountMap = new Map();
for (const currentSum of prefixSum) {
// 我们需要找的是:之前有多少个前缀和等于 currentSum - k
const target = currentSum - k;
count += sumCountMap.get(target) ?? 0;
// 注意:先查再存,避免 k=0 时把自己算进去
sumCountMap.set(currentSum, (sumCountMap.get(currentSum) ?? 0) + 1);
}
return count;
};
其实还可以再精简一下,不用单独建前缀和数组,边遍历边算前缀和就行:
js
var subarraySum = function(nums, k) {
let count = 0;
let currentSum = 0;
// key: 前缀和的值,value: 出现次数。初始放入 0:1 对应哨兵
const sumCountMap = new Map();
sumCountMap.set(0, 1);
for (const num of nums) {
currentSum += num;
// 查之前出现过多少次 currentSum - k
const target = currentSum - k;
count += sumCountMap.get(target) ?? 0;
// 先查后存,别搞反了
sumCountMap.set(currentSum, (sumCountMap.get(currentSum) ?? 0) + 1);
}
return count;
};
这个版本空间也是 O (n),但少了一个数组,更干净。
复杂度
时间 O (n),一趟遍历搞定。空间 O (n),哈希表最多存 n 个不同的前缀和。
比暴力的 O (n²) 香多了。
写在最后
这题的核心考点就是前缀和 + 哈希表的组合拳。说实话这个套路学会了,很多子数组求和的问题都能套。
我自己踩过的坑提醒一下:
- 哈希表初始化要放
{0: 1},不然从数组开头开始的子数组会漏掉 - 一定是先查再存,k = 0 的时候顺序反了结果会偏大
- 数组里有负数也没事,前缀和照样工作,别被吓到
你有没有在这道题上卡过?或者你有更骚的写法?评论区聊聊,我会回来看的。
如果觉得有用,点个赞让更多正在刷题的朋友看到吧~