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 的时候顺序反了结果会偏大
  • 数组里有负数也没事,前缀和照样工作,别被吓到

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

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

相关推荐
我命由我123452 小时前
CesiumJS 笔记 - 获取容器中心点、Cartesian3 clone 方法、修改 Cartesian3 对象的高度
前端·javascript·css·前端框架·html·html5·js
大明者省3 小时前
WSL2 Ubuntu22.04 GPU训练环境配置指南
人工智能·算法·计算机视觉
抱抱宝3 小时前
Agent-study项目教程(03):手写 Mini-ReAct Agent(不依赖框架)
javascript·人工智能·gpt·react.js·prompt·agent
用户938515635074 小时前
Next.js 笔记系统(二):Redis 数据服务与侧边栏组件拆分实战
javascript·全栈
白狐_7985 小时前
408数据结构第8章:排序②——性质对比秒杀、场景选择与外部排序
java·数据结构·算法
zander2585 小时前
LeetCode 84:柱状图中的最大矩形——单调栈如何确定左右边界
java·数据结构·算法
Shell运维手记6 小时前
Linux 常用基础命令学习笔记
linux·运维·笔记·学习·算法·github
杨航 AI6 小时前
O(n log n):线性对数原理拆解 这个是排序算法的黄金复杂度之一。
算法·排序算法
船厂电气自动化ai大模型7 小时前
AI大模型与数学 第32课 函数凹凸性与二阶导数:拐点求解、凹凸区间计算(10道二阶导数计算题)
数据结构·人工智能·python·深度学习·算法
旖旎夜光7 小时前
LeetCode 3:无重复字符的最长子串(滑动窗口) —— 题解
数据结构·c++·算法·leetcode·滑动窗口