LC 560 和为 K 的子数组:前缀和配哈希表,这对组合我是真的服了

说实话,这题我第一次看到的时候,心里想的是:不就是找子数组和等于 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 的时候顺序反了结果会偏大
  • 数组里有负数也没事,前缀和照样工作,别被吓到

你有没有在这道题上卡过?或者你有更骚的写法?评论区聊聊,我会回来看的。

如果觉得有用,点个赞让更多正在刷题的朋友看到吧~

相关推荐
Omics Pro27 分钟前
Arc研究所Cell|全新虚拟细胞官方基准评测框架
大数据·人工智能·深度学习·算法·机器学习
2601_962097481 小时前
JavaScript 异步编程
javascript·ajax·回调函数·异步编程·子线程
全栈项目管理程序猿2 小时前
ArcGIS JS 基础教程(18):SceneLayer 场景图层
javascript
leoZ2312 小时前
第 2 篇:搭建地基——Vue3 + Vite + Tailwind v4 + shadcn-vue
前端·javascript·vue.js·人工智能·目标检测·数据挖掘·语音识别
CoderYanger2 小时前
A.每日一题:1140. 石子游戏 II
java·程序人生·算法·leetcode·游戏·职场和发展·深度优先
小雨笙笙2 小时前
机器学习:评估模型与选择
人工智能·算法·机器学习
七牛开发者2 小时前
拆解 dsh:Session 的事件溯源与状态重建
javascript·github·agent
高亦真2 小时前
今天是学习嵌入式的第35天
linux·学习·算法
梨想橙汁2 小时前
JS BOM 浏览器对象:定时器与同步异步底层理解
前端·javascript
梨想橙汁2 小时前
ES6+ 必用新特性:箭头函数、解构、模板字符串、扩展运算符
前端·javascript