子串

0 阅读3分钟

560. 和为 K 的子数组

这道题目是 “和为 K 的子数组” 。它在面试中非常经典,难点在于:如果用暴力法(嵌套循环)去数有多少种组合,时间复杂度是 O(n2)O(n^2),面对海量数据会超时。

你提供的代码使用了一个极其聪明的组合技巧:前缀和 + 哈希表(Map)


🏠 生活案例:公交车的计费器

想象你坐一辆公交车,车上有一个累积计费器(记录从起点站开始一共收了多少钱)。

  • 题目要求:找出有多少段连续的区间,票价刚好是 kk 元。

  • 你的逻辑

    • A 站,计费器显示一共收了 10 元
    • B 站,计费器显示一共收了 25 元
    • 如果你想要找一段票价为 15 元 (k=15k=15) 的路程。你会发现:2510=1525 - 10 = 15
    • 结论:这意味着从 A 站到 B 站这段路,乘客们一共交了 15 元。

所以,我们不需要去一段一段数,只需要记录计费器曾经出现过哪些数字,然后看现在的数字减去 kk,那个差值以前有没有出现过。


💻 代码实现与生活化注释

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;
};

🔍 为什么这个方法是 O(n)O(n)

  • 只走一次:我们只用一个 for 循环把数组从头到尾走一遍。
  • 空间换时间:我们额外用了一个 Map(账本)来存以前的数据。在 Map 里查找数字的速度是极快的。
  • 数学原理:子数组 [i,j][i, j] 的和等于 PrefixSum[j]PrefixSum[i1]PrefixSum[j] - PrefixSum[i-1]。我们通过寻找 PrefixSum[j]k=PrefixSum[i1]PrefixSum[j] - k = PrefixSum[i-1] 来反向定位符合条件的子数组。

避坑指南:

代码里 map.set(0, 1) 这一行非常关键。如果没有它,当数组里第一项刚好等于 kk 时,sumNums - k 会等于 0,如果你没预存 0,程序就会漏掉这个结果。这就好比你必须承认:在出发之前,你的兜里确实有 0 元。