力扣刷题#10:LeetCode560 和为K的子数组_刷题笔记

560. 和为 K 的子数组 | 刷题笔记

从完全懵懂到独立 AC 的完整思考过程


题目回顾

给你一个整数数组 nums 和一个整数 k,请你统计并返回该数组中和为 k 的子数组的个数

注意:子数组是连续的非空序列。

示例:

输入:nums = [1, 1, 1], k = 2
输出:2
解释:有两个子数组的和为 2:[1, 1](下标 0~1)和 [1, 1](下标 1~2)

第一步:先写暴力解法(谁都能想到)

先不管优化,用最直觉的方式写:

枚举所有子数组,看和是不是 k

// 暴力思路(O(n²))
int ans = 0;
for (int i = 0; i < n; i++) {
    int sum = 0;
    for (int j = i; j >= 0; j--) {
        sum += nums[j];
        if (sum == k) ans++;
    }
}

对每个起始位置 i,往回累加,每遇到 sum == k 就计数 +1。

问题:时间复杂度 O(n²),数据量大了会超时。

面试官一定会追问:能不能优化到 O(n)?


第二步:理解前缀和——从「修路记工分」说起

什么是前缀和?

nums = [3, 1, -2, 4]

前缀和:从第 1 个数累加到当前位置的总和
prefix[0] = 0           (还没开始,总分 0)
prefix[1] = 3            (前 1 个数)
prefix[2] = 3 + 1 = 4   (前 2 个数)
prefix[3] = 4 - 2 = 2   (前 3 个数)
prefix[4] = 2 + 4 = 6   (前 4 个数)

关键性质:子数组的和 = 前缀和之差

nums[j..i] 的和 = prefix[i+1] - prefix[j]

用生活例子理解:

你 1 月存了 100 元,3 月存了 250 元。
那 2~3 月存了多少?250 - 100 = 150 元。
前缀和就是「到某月为止的总存款」。


第三步:从暴力解法「逼出」哈希表

暴力解法的瓶颈在哪?

for (int j = i; j >= 0; j--) {   // ← 对每个 i,要往回找 O(i) 次
    sum += nums[j];
    if (sum == k) ans++;
}

关键问题:能不能不用往回找,一瞬间就知道「前面有没有和为 k 的子数组以 i 结尾」?

数学转化

我们要找:nums[j..i] 的和 == k
即:prefix[i+1] - prefix[j] == k
移项:prefix[j] == prefix[i+1] - k

翻译一下:
「以 i 结尾、和为 k 的子数组有几个?」
= 「前面有多少个位置 j,使得 prefix[j] == prefix[i+1] - k?」

现在需求明确了

我需要一个数据结构,能告诉我:「前缀和 = X 的位置出现过几次?」

需要支持的操作 用什么数据结构
问「X 出现过几次?」 数组?不行,X 可能是负数、可能很大
哈希表! key = X,value = 出现次数

所以哈希表不是「拍脑袋想出来的」,是从暴力解法的瓶颈「逼出来的」。


第四步:ans += map[target] 到底在加什么?

这是这道题最绕的地方。很多人(包括我)第一反应是:

找到了 target 就说明有一个子数组符合条件,直接 ans++ 不行吗?

答案是:不一定只找到一个!

具体例子

nums = [1, -1, 1, 1, 1], k = 2

我们一步步走,重点关注 map 的状态:

步骤 i curr(当前前缀和) target = curr - k map 里 target 出现了几次 ans 累加
初始 0 0
1 0 1 -1 0 次 0
2 1 0 -2 0 次 0
3 2 1 -1 0 次 0
4 3 2 0 map[0] = 2 次 → ans += 2 2
5 4 3 1 map[1] = 2 次 → ans += 2 4

第 4 步为什么 ans 要 +2?

因为前缀和 = 0 的位置出现了两次

prefix[0] = 0   ← 初始空前缀
prefix[2] = 0   ← nums[0] + nums[1] = 1 + (-1) = 0,又出现了!

这两个位置分别对应两个不同的子数组:

① 从 prefix[0] 到 i=3:nums[0..3] = [1,-1,1,1],sum = 2 ✅
② 从 prefix[2] 到 i=3:nums[2..3] = [1,1],      sum = 2 ✅

同一个前缀和可能在不同位置重复出现,每次出现都可能构成一个新的、以当前位置结尾的、和为 k 的子数组。

所以代码逻辑是:

ans += map[target];   // 不是 ans++,而是加上出现的次数!

第五步:踩过的坑与修正

坑 1:unordered_map::insert 的用法错了

a.insert(0, nums[0]);   // 编译错误!

unordered_map::insert 接受的是 pair<key, value>,正确写法:

a[0] = nums[0];           // 用 [] 赋值(推荐)
// 或者
a.insert({0, nums[0]});   // insert 接受 pair

坑 2:找到前缀和后,忘了把当前前缀和加入 map

// 错误:只查不存
if (a.count(target)) {
    sum += a[target];
}
// ← 少了 a[cur]++ !

当前位置的前缀和,对后面的位置来说就是「前面的前缀和」,必须记录下来,后面的位置才能查到它。

坑 3:len == 1 的特殊处理是多余的

if (len == 1) {
    if (nums[0] == k) return 1;
    return 0;
}

这段逻辑其实已经被主循环覆盖了,不需要特殊处理。去掉它,代码更简洁。


最终 AC 代码

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int, int> a;
        // key   = 前缀和的值
        // value = 这个前缀和出现了几次

        a[0] = 1;   // 初始:空前缀的和为 0,出现 1 次
        int cur = 0, sum = 0;   // cur = 当前前缀和,sum = 答案计数器

        for (int i = 0; i < nums.size(); i++) {
            cur += nums[i];            // 更新当前前缀和
            int target = cur - k;      // 要找的前缀和

            if (a.count(target)) {     // 前面有前缀和 = target
                sum += a[target];      // 出现几次就加几次
            }

            a[cur]++;                 // ★ 把当前前缀和记录进 map
        }
        return sum;
    }
};

复杂度分析

复杂度 说明
时间复杂度 O(n) 只遍历一次,哈希表查找/插入平均 O(1)
空间复杂度 O(n) 哈希表最多存 n 个不同的前缀和

关键思路总结

1. 这道题的本质

前缀和 + 哈希表查值

这是一个非常经典的组合,在不少题目中都会反复出现(比如「连续数组」、「和可被 K 整除的子数组」等)。

2. map 的 key 和 value 为什么这么设计?

不是拍脑袋想出来的,是从需求推出来的:

需求:对每个位置 i,快速知道「前面前缀和 = X 的位置有几个?」
  ↓
数据结构:key = 前缀和的值,value = 出现次数
  ↓
工具:哈希表(O(1) 查询)

3. ans += map[target] 而不是 ans++

同一个前缀和可能在多个位置出现,每个位置都对应一个合法的子数组,所以要加「次数」而不是加 1。

4. 这道题的思考路径

暴力 O(n²) → 发现瓶颈(往回找太慢)
          ↓
          问:能不能 O(1) 查到「前面有没有符合条件的位置」?
          ↓
          前缀和之差 = k → 查表:前面有没有 prefix = curr-k?
          ↓
          用哈希表存「每个前缀和出现了几次」
          ↓
          一次遍历,O(n) 完成

模板视角:与「两数之和」的哈希表模板对比

在完成这道题之后,有一个非常有价值的上位视角:这道题本质上就是「两数之和」的哈希表模板的变形!

只要把「要找什么」定义清楚,两道题的代码模板几乎一模一样。

一一对应关系

两数之和 和为 K 的子数组
遍历的对象 数组元素 nums[i] 前缀和 cur
要找的目标 complement = target - nums[i] target = cur - k
哈希表 key 元素的值 → 下标 前缀和的值 → 出现次数
找到了怎么办 返回两个下标 ans += 出现次数
找不到怎么办 存入 (nums[i], i) map[cur]++(记录/更新出现次数)

代码模板对比

// ====== 两数之和 ======
unordered_map<int, int> hash;
for (int i = 0; i < n; i++) {
    int complement = target - nums[i];
    if (hash.count(complement)) {
        return {hash[complement], i};   // 找到一组,直接返回
    }
    hash[nums[i]] = i;                 // 没找到,把当前值存进去
}

// ====== 和为 K 的子数组 ======
unordered_map<int, int> map;
map[0] = 1;                           // 初始化
for (int i = 0; i < n; i++) {
    cur += nums[i];
    int target = cur - k;
    if (map.count(target)) {
        ans += map[target];            // 找到了,累加出现次数
    }
    map[cur]++;                        // 把当前前缀和记录/更新进 map
}

为什么能对应上?

两道题的核心公式长得一模一样:

两数之和:   nums[i] + complement = target
             →  complement = target - nums[i]

子数组求和: prefix[j] + k = prefix[i]
             →  prefix[j] = prefix[i] - k
             →  即 target = cur - k

两者都是在问:「我要找的那个值,之前有没有出现过?」

唯一的区别:

  • 两数之和:找到一组就结束了(直接 return
  • 和为 K 的子数组:同一个前缀和可能出现多次,所以要累加次数ans += map[target])而不是直接返回

这个视角的价值

以后遇到类似题目,思维路径可以是:

看到「找两个东西满足某种关系」→ 第一反应:能不能用「两数之和」的哈希表模板?

你会在后面遇到更多同模板的变形题,比如:

  • 连续数组(和为 0 的最长子数组)→ 同样是前缀和 + map
  • 和可被 K 整除的子数组 → 同样是前缀和 + map,只是判断条件变了(prefix % K 代替 prefix

它们全都是同一个模板的不同变形。 吃透这一道,后面几道会快很多。


一句话总结

哈希表不是「想出来的」,是从暴力解法的瓶颈「逼出来的」。
只要记住:当你需要「快速查询某个值出现过没有/出现过几次」时,第一时间想到哈希表。

这道题初见几乎没人能独立想到,见多了、复盘多了,慢慢就有「嗅觉」了 💪


本文档由 AI 辅助生成,作者提供问题和思路,综合获得以上内容。

posted @ 2026-06-07 19:48  STA_running  阅读(13)  评论(0)    收藏  举报