560. 和为 K 的子数组
这道题目是 “和为 K 的子数组” 。它在面试中非常经典,难点在于:如果用暴力法(嵌套循环)去数有多少种组合,时间复杂度是 ,面对海量数据会超时。
你提供的代码使用了一个极其聪明的组合技巧:前缀和 + 哈希表(Map) 。
🏠 生活案例:公交车的计费器
想象你坐一辆公交车,车上有一个累积计费器(记录从起点站开始一共收了多少钱)。
-
题目要求:找出有多少段连续的区间,票价刚好是 元。
-
你的逻辑:
- 在 A 站,计费器显示一共收了 10 元。
- 在 B 站,计费器显示一共收了 25 元。
- 如果你想要找一段票价为 15 元 () 的路程。你会发现:。
- 结论:这意味着从 A 站到 B 站这段路,乘客们一共交了 15 元。
所以,我们不需要去一段一段数,只需要记录计费器曾经出现过哪些数字,然后看现在的数字减去 ,那个差值以前有没有出现过。
💻 代码实现与生活化注释
JavaScript
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var subarraySum = function(nums, k) {
let sumNums = 0; // “计费器”:从开头到当前的累积和
let count = 0; // “计数器”:发现满足条件的区间个数
let map = new Map(); // “小账本”:记录每个累积和出现的次数
// 1. 初始化小账本:
// 还没出发时,累积和为 0,这种情况已经出现过 1 次。
// (这是为了处理:如果某一段路刚好从开头加起来就等于 k 的情况)
map.set(0, 1);
for(let num of nums){
// 2. 计费器跳动:加上当前的站点的金额
sumNums = sumNums + num;
/**
* 3. 核心逻辑:查账
* 看看 (当前总和 - k) 这个数字以前有没有在账本里出现过?
* 如果出现过,说明从“那个时候”到“现在”这一段路,和刚好就是 k!
*/
if(map.has(sumNums - k)){
// 以前出现过几次,就代表有几种切分方法能凑出 k
count = count + map.get(sumNums - k);
}
/**
* 4. 记账:
* 把当前的累积和记录到账本里。
* 如果以前有过这个和,次数+1;没有的话,记为 1 次。
*/
map.set(sumNums, (map.get(sumNums) || 0) + 1);
}
return count;
};
🔍 为什么这个方法是 ?
- 只走一次:我们只用一个
for循环把数组从头到尾走一遍。 - 空间换时间:我们额外用了一个
Map(账本)来存以前的数据。在Map里查找数字的速度是极快的。 - 数学原理:子数组 的和等于 。我们通过寻找 来反向定位符合条件的子数组。
避坑指南:
代码里 map.set(0, 1) 这一行非常关键。如果没有它,当数组里第一项刚好等于 时,sumNums - k 会等于 0,如果你没预存 0,程序就会漏掉这个结果。这就好比你必须承认:在出发之前,你的兜里确实有 0 元。