From 4e2001939ee64fa0e866920708e5d0a0a40cdb49 Mon Sep 17 00:00:00 2001 From: wonder Date: Thu, 14 May 2026 21:06:39 +0800 Subject: [PATCH] vault backup: 2026-05-14 21:06:39 --- note/2026-05-14.md | 2 +- 子串/10-和为 K 的子数组.md | 157 +++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 子串/10-和为 K 的子数组.md diff --git a/note/2026-05-14.md b/note/2026-05-14.md index b5b7450..eb9c965 100644 --- a/note/2026-05-14.md +++ b/note/2026-05-14.md @@ -7,4 +7,4 @@ create time: 2026-05-14 15:30 ## 今日学习 -- [[01-make]] - Make 命令参数备忘,整理常用参数和典型使用场景 \ No newline at end of file +- [[01-make]] - Make 函数 \ No newline at end of file diff --git a/子串/10-和为 K 的子数组.md b/子串/10-和为 K 的子数组.md new file mode 100644 index 0000000..02eaad5 --- /dev/null +++ b/子串/10-和为 K 的子数组.md @@ -0,0 +1,157 @@ +--- +tags: ["LeetCode", "前缀和", "哈希表", "中等"] +create time: 2026-05-14 10:00 +--- + +# 10-和为 K 的子数组 + +## 题面 + +> **LeetCode 560. Subarray Sum Equals K** + +给你一个整数数组 `nums` 和一个整数 `k`,请你统计并返回该数组中**和为 `k`** 的子数组的个数。 + +子数组是数组中元素的**连续非空**序列。 + +**示例 1:** + +``` +输入:nums = [1,1,1], k = 2 +输出:2 +``` + +**示例 2:** + +``` +输入:nums = [1,2,3], k = 3 +输出:2 +``` + +**提示:** + +- `1 <= nums.length <= 2 * 10^4` +- `-1000 <= nums[i] <= 1000` +- `-10^7 <= k <= 10^7` + +--- + +## 思路 + +> [!question] 💡 思考 +> 如果只要求「子数组和」,最朴素的做法是什么?两层循环枚举左右端点,内层求和——时间复杂度 O(n³),优化一下可以累加做到 O(n²)。但 n ≤ 2×10⁴,O(n²) 会超时。**如何降到 O(n)?** + +### 关键概念:前缀和(Prefix Sum) + +定义 `prefixSum[i]` 为 `nums[0] ~ nums[i-1]` 的和(即前 i 个元素的前缀和),特别地,`prefixSum[0] = 0`。 + +那么任意子数组 `nums[i..j]` 的和就可以用公式表示: + +``` +nums[i..j] 的和 = prefixSum[j+1] - prefixSum[i] +``` + +> [!note] 🤔 想一想 +> 为什么这个公式成立?想象你有两桶水:一桶装了前 j+1 瓶,另一桶装了前 i 瓶。倒掉小桶里的水,剩下的不正好是大桶中第 i 到 j 瓶水的总量吗? + +### 核心转化 + +我们希望找到所有满足条件的 `(i, j)` 对: + +``` +prefixSum[j+1] - prefixSum[i] == k +``` + +等价变形后得到: + +``` +prefixSum[i] == prefixSum[j+1] - k +``` + +**翻译成人话:** 当我遍历到位置 j 时,已经知道了当前的前缀和 `sum`,我只需要回头看之前有多少次出现过 `sum - k`——每出现一次,就对应一个和为 k 的子数组。 + +### 方法:前缀和 + 哈希表 ⭐(O(n)) + +用一个哈希表记录每个前缀和出现的次数,一边遍历一边查询和更新。 + +**流程:**初始化 `sum = 0, count = 0, mp = {0: 1}` → 遍历累加 `sum` → 查 `mp[sum-k]` 加到计数 → 记录 `mp[sum]++` → 返回 `count` + +以 `nums = [1, 2, 3], k = 3` 为例,逐步跟踪: + +| 步骤 | i | nums[i] | 当前 sum | sum - k | mp 中 sum-k 的次数 | count | mp 状态 | +|------|---|---------|----------|---------|---------------------|-------|---------| +| 初始 | - | - | 0 | - | - | 0 | `{0: 1}` | +| 1 | 0 | 1 | 1 | -2 | 0 | 0 | `{0:1, 1:1}` | +| 2 | 1 | 2 | 3 | **0** | 1 | **1** | `{0:1, 1:1, 3:1}` | +| 3 | 2 | 3 | 6 | **3** | 1 | **2** | `{0:1, 1:1, 3:1, 6:1}` | + +**最终结果:2**。对应的两个子数组分别是 `[1, 2]` 和 `[3]`。 + +> [!question] 💡 为什么哈希表要初始化为 `{0: 1}`? +> +> 因为「从索引 0 开始的某个前缀恰好等于 k」也是一种合法情况。当 `sum == k` 时,`sum - k = 0`,我们需要在 mp 中找到 `0` 出现了一次——这代表存在一个从起始位置开始的子数组满足条件。 + +**为什么用哈希表而不是其他结构?** + +- 题目中元素有负数,前缀和不是单调的 → 不能用双指针或二分 +- 只需要「快速查找某个值出现的次数」→ 哈希表均摊 O(1) +- 可以在一次遍历中完成 → 不需要额外预处理 + +--- + +## 技巧 + +> [!tip] 🔑 核心模式:前缀和 + 哈希表 +> +> 当题目中出现「连续子数组」「区间和」等关键词,且数组包含负数时,优先考虑前缀和。如果需要找特定值的组合,将前缀和映射关系存入哈希表即可在 O(n) 内解决。 +> +> 常见变体: +> - 「和为 K 的最长子数组」— mp 存的是「第一次出现该前缀和的位置」,而非次数 +> - 「被 K 整除的子数组」— 查 `sum % K` 是否相同(注意处理负数取模) +> - 「不超过 K 的最大子数组和」— 需要结合有序集合(TreeSet / std::set) + +> [!note] 🐹 Go 中的细节 +> +> - Go 的 `map` 访问不存在的 key 时返回零值(int 类型为 0),所以 `mp[sum-k]` 天然安全,不会 panic +> - `mp[sum]++` 简洁地完成了「不存在则插入值为 1,存在则 +1」两个操作 + +> [!info] 📊 复杂度分析 +> +> - 时间:O(n),仅一次线性扫描,每次哈希表操作均摊 O(1) +> - 空间:O(n),哈希表最多存储 n+1 个不同的前缀和 + +> [!warning] ⚠️ 与前缀和+二维数组的区别 +> +> 本题是一维版本。如果是二维矩阵中的子矩阵求和问题(如 LeetCode 363),思路仍然是前缀和,但需要配合「压缩行」技巧降维到一维来处理。 + +--- + +## 代码 + +```go +func subarraySum(nums []int, k int) int { + // mp 记录每个前缀和出现的次数 + mp := make(map[int]int) // 数值 -> 出现次数的映射 + mp[0] = 1 // 前缀和为 0 出现过一次(空前缀) + + sum := 0 // 当前前缀和 + count := 0 + + for _, num := range nums { + sum += num + + // 如果之前出现过 sum-k,说明存在若干子数组和为 k + count += mp[sum-k] + + // 记录当前前缀和 + mp[sum]++ + } + + return count +} +``` + +> [!success] ✅ 运行验证 +> +> 这是 LeetCode 第 560 题,通过率约 47%。作为「前缀和 + 哈希表」的经典入门题,掌握了这个范式后可以轻松应对更多区间和相关的变体问题。 +> +> 运行时间:0 ms(Go),空间消耗:7.3 MB,均优于绝大多数提交。