高级农民
积分 4022
大米 颗
鳄梨 个
水井 尺
蓝莓 颗
萝卜 根
小米 粒
学分 个
注册时间 2017-1-22
最后登录 1970-1-1
560. Subarray Sum Equals Kclass Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
"""
preSum = [0] * (len(nums) + 1)
for i in range(len(nums)): preSum[i+1] = preSum[i] + nums[i]
ans = 0
for i in range(0, len(nums)+1):
for j in range(i+1, len(nums)+1):
if preSum[j] - preSum[i] == k: ans += 1
return ans
"""
currSum = 0
preSum = collections.defaultdict(int)
preSum[0] = 1
ans = 0
for num in nums:
currSum += num
if currSum - k in preSum:
ans += preSum[currSum - k]
preSum[currSum] += 1
return ans
复制代码 好,这一题是 **“Subarray Sum Equals K(和为 K 的子数组个数)”** 的经典解法。
我用 **直觉 → 数学关系 → 哈希表怎么用 → 走一遍例子** 来给你彻底讲清楚。
---
## 一、问题一句话版
给你一个数组 `nums` 和一个整数 `k`,
**问:有多少个连续子数组的和等于 `k`?**
---
## 二、核心直觉(这一步最重要)
### 定义前缀和
令:
```
sum[i] = nums[0] + nums[1] + ... + nums[i]
```
那么区间 `(j+1 ... i)` 的和是:
```
sum[i] - sum[j]
```
---
## 三、关键数学结论(题解里的那段话)
### 1️⃣ 为什么「前缀和相等 → 中间和为 0」
如果:
```
sum[i] == sum[j]
```
那么:
```
sum[i] - sum[j] = 0
```
👉 `j+1` 到 `i` 的子数组和为 **0**
---
### 2️⃣ 推广到一般情况(真正用到的)
如果:
```
sum[i] - sum[j] = k
```
等价于:
```
sum[j] = sum[i] - k
```
👉 **只要之前出现过 `sum[i] - k`,就说明存在一个子数组和为 `k`,并且以 `i` 结尾**
---
## 四、HashMap 在这里干什么?
我们用一个 hashmap(字典):
```
map[前缀和] = 出现次数
```
它的作用是:
> 快速告诉我:
> **“到目前为止,有多少个前缀和等于某个值”**
---
## 五、算法流程(一步一步)
### 初始化
```python
count = 0
curr_sum = 0
map = {0: 1}
```
⚠️ `0:1` 很关键:
* 表示「在数组开始前,有一个前缀和为 0」
* 这样可以正确处理 **从下标 0 开始的子数组**
---
### 遍历数组
对每个 `num`:
1. 更新前缀和
```python
curr_sum += num
```
2. 看有没有 `curr_sum - k`
```python
if curr_sum - k in map:
count += map[curr_sum - k]
```
3. 把当前前缀和加入 hashmap
```python
map[curr_sum] += 1
```
---
## 六、为什么要「出现次数」,而不是 True / False?
因为:
> **同一个前缀和可能出现多次**
每出现一次:
* 都代表一个不同的起点
* 都能形成一个新的子数组
---
## 七、完整走一遍例子(非常关键)
### 示例
```python
nums = [1, 2, 3]
k = 3
```
---
### 初始状态
```
map = {0:1}
curr_sum = 0
count = 0
```
---
### i = 0, num = 1
```
curr_sum = 1
curr_sum - k = -2 → 不存在
map = {0:1, 1:1}
```
---
### i = 1, num = 2
```
curr_sum = 3
curr_sum - k = 0 → map[0] = 1
count += 1
```
子数组:
```
[1,2]
```
更新 map:
```
map = {0:1, 1:1, 3:1}
```
---
### i = 2, num = 3
```
curr_sum = 6
curr_sum - k = 3 → map[3] = 1
count += 1
```
子数组:
```
[3]
```
---
### 最终结果
```
count = 2
```
---
## 八、为什么这是 O(n)?
* 每个元素 **只遍历一次**
* hashmap 查询是 **O(1)**
相比暴力解法:
| 方法 | 时间复杂度 |
| ------------- | -------- |
| 三重循环 | O(n³) |
| 前缀和 + 双循环 | O(n²) |
| 前缀和 + HashMap | **O(n)** |
---
## 九、一句话总结(面试必背)
> 通过前缀和,把「子数组和为 k」转化为
> 查找是否存在 `sum[i] - k`,
> 使用 HashMap 记录前缀和出现次数,
> 从而在 O(n) 时间内完成统计。
---
这个问题问得**非常到位**,基本就是这道题的“分水岭理解点”。我直接给你一个**清晰对比 + 直觉解释**。
---
## 一、先说结论(一句话版)
> **前缀和 + HashMap 能处理负数,是因为它不依赖“区间和的单调性”;
> 滑动窗口不行,是因为一旦有负数,窗口和就不再单调变化。**
下面展开。
---
## 二、滑动窗口为什么在「全正数」时可行?
滑动窗口隐含了一个**关键假设**:
* 右指针右移 → 区间和 **只会变大**
* 左指针右移 → 区间和 **只会变小**
这个假设 **只在所有数 ≥ 0 时成立**。
### 典型逻辑
```text
sum < k → 扩大窗口(right++)
sum > k → 缩小窗口(left++)
```
这是一个“**可控的单调过程**”。
---
## 三、负数一出现,滑动窗口就崩了 💥
### 举个最小反例
```
nums = [1, -1, 1]
k = 1
```
---
### 滑动窗口尝试
1️⃣ `left=0, right=0`
```
sum = 1 ✔
```
2️⃣ 扩展窗口
```
right=1
sum = 0 (变小了!)
```
3️⃣ 再扩展
```
right=2
sum = 1 ✔
```
现在问题来了:
* `sum < k` 时你 **不知道** 是该扩右还是缩左
* `sum > k` 时缩左 **也可能让 sum 变大**
👉 **决策方向不再可靠**
---
### 本质问题
> 有负数时,
> **移动指针 ≠ 可预测地改变区间和**
滑动窗口失去了“导航能力”。
---
## 四、前缀和 + HashMap 为什么不怕负数?
### 它根本不「移动窗口」
它做的是:
> **固定右端点 i,回头看所有可能的左端点 j**
数学关系:
```
sum[i] - sum[j] = k
```
⇔
```
sum[j] = sum[i] - k
```
---
### 负数完全不影响这个等式
无论 nums 里是:
```
+5, -3, +10, -8, ...
```
这个关系 **永远成立**。
---
## 五、一个负数示例完整走一遍
```
nums = [1, -1, 1]
k = 1
```
### 过程
| i | num | curr_sum | curr_sum - k | map | 新增子数组 |
| - | --- | -------- | ------------ | --------- | -------- |
| - | - | 0 | - | {0:1} | - |
| 0 | 1 | 1 | 0 | {0:1} | [1] |
| 1 | -1 | 0 | -1 | {0:1,1:1} | - |
| 2 | 1 | 1 | 0 | {0:2,1:1} | [1,-1,1] |
👉 完全不需要任何“回退”或“调整窗口”。
---
## 六、对比总结(核心差异)
| 维度 | 滑动窗口 | 前缀和 + HashMap |
| ------- | ---- | ------------- |
| 是否依赖单调性 | 是 | 否 |
| 能否处理负数 | ❌ | ✅ |
| 思维方式 | 移动区间 | 数学等式 |
| 本质 | 贪心 | 计数 / 统计 |
---
## 七、什么时候该用哪种?
### 用滑动窗口的前提
* 数组元素 **全是非负**
* 或满足某种 **单调性质**
👉 例如:
* 最小子数组和 ≥ k
* 不重复字符的最长子串
---
### 用前缀和 + HashMap 的信号
* 出现 **负数**
* 问的是 **子数组个数**
* 和、差、等于某个值
👉 **90% 都是前缀和 + 哈希**
---
## 八、一句话终极总结(面试杀手锏)
> 滑动窗口依赖区间和的单调变化,一旦有负数就失效;
> 前缀和方法基于恒等式 `sum[i] - sum[j] = k`,与元素正负无关,因此能稳定处理负数。