题目:
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
核心思路:
子数组的和 = 当前前缀和 – 之前某个前缀和。
什么是前缀和?
前缀和就是从数组开头一直累加到当前位置的和:
nums = [1, 1, 2, 3]
前缀和: [1, 2, 4, 7]
前缀和[0] = 1
前缀和[1] = 1+1 = 2
前缀和[2] = 1+1+2 = 4
前缀和[3] = 1+1+2+3 = 7
前缀和的用处
任何一个连续子数组的和,都可以用两个前缀和相减得到:
nums = [1, 1, 2, 3]
前缀和: [1, 2, 4, 7]
想求 [1, 2] 的和?
不需要重新累加,直接:前缀和[2] – 前缀和[0] = 4 – 1 = 3
想求 [2, 3] 的和?
前缀和[3] – 前缀和[1] = 7 – 2 = 5
可以画成这样:
nums = [1, 1, 2, 3]
├───┤
前缀和[0]=1
├───────┤
前缀和[2]=4
[1, 2] 的和 = 前缀和[2] – 前缀和[0] = 4 – 1 = 3
怎么找和等于 k 的子数组?
既然 子数组的和 = 当前前缀和 – 之前某个前缀和,那么:
子数组的和 = k
↓
当前前缀和 – 之前某个前缀和 = k
↓
之前某个前缀和 = 当前前缀和 – k
所以每走到一个位置,只需要看之前有没有出现 sum – k 这个前缀和就够了。
完整演示
nums = [1, 1, 2], k = 2
用 map 记录之前出现过的前缀和及其次数
初始: map = {0: 1} (下面解释为什么)
i=0: sum = 1
找 sum-k = 1-2 = -1, map 里没有 ✗
map = {0:1, 1:1}
i=1: sum = 2
找 sum-k = 2-2 = 0, map 里有 0 ✓ count=1
→ 对应子数组 [1,1],和 = 2-0 = 2
map = {0:1, 1:1, 2:1}
i=2: sum = 4
找 sum-k = 4-2 = 2, map 里有 2 ✓ count=2
→ 对应子数组 [2],和 = 4-2 = 2
map = {0:1, 1:1, 2:1, 4:1}
结果: 2
题解:
class Solution {
public int subarraySum(int[] nums, int k) {
int count = 0;
int sum = 0;
// key: 前缀和, value: 出现次数
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0, 1); // 初始化:和为0出现一次
for (int i = 0; i < nums.length; i++) {
sum += nums[i]; // 当前前缀和
if (map.containsKey(sum – k)) { // 检查是否存在 sum-k
count += map.get(sum – k); // 累加次数
}
map.put(sum, map.getOrDefault(sum, 0) + 1); // 记录当前前缀和
}
return count;
}
}
```
## 核心思路
核心在于一个公式:**`子数组和 = 当前前缀和 – 之前某个前缀和`**
前缀和就是从头到当前位置的累加和:
```
nums = [1, 1, 2]
前缀和: [1, 2, 4]
↑ ↑ ↑
s[0] s[1] s[2]
```
如果要求 `[1, 2]` 这个子数组的和,不需要重新累加,直接用前缀和相减:
```
sum[1,2] = s[2] – s[0] = 4 – 1 = 3
```
所以当我们走到位置 `i`,当前前缀和是 `sum`,只需要看**之前有没有出现 `sum – k` 的前缀和**,有的话就说明中间那段子数组和刚好等于 `k`:
```
nums = [1, 1, 2], k = 2
前缀和: [1, 2, 4]
i=0: sum=1, sum-k=1-2=-1, map里没有 -1 ✗
i=1: sum=2, sum-k=2-2=0, map里有 0(初始化的) ✓ count=1
→ 对应子数组 [1,1],和 = 2-0 = 2
i=2: sum=4, sum-k=4-2=2, map里有 2 ✓ count=2
→ 对应子数组 [2],和 = 4-2 = 2
为什么要初始化 ?map.put(0, 1)
当前缀和本身就等于 时,如果 map 里没有 ,就会漏掉从数组开头开始的子数组:ksum – k = 00
<span style="color:#abb2bf"><code>nums = [2], k = 2
i=0: sum = 2
找 sum-k = 2-2 = 0
如果 map 里没有 0 → 找不到,漏掉了!
如果 map 里有 0 → 找到了,对应子数组 [2]</code></span>
map.put(0, 1)相当于在数组开头放一个"虚拟的前缀和 0"。
本质
比较粗暴的方法是双重循环枚举所有子数组,O(n²)。前缀和的本质就是把“求子数组的和”这件事转化为“在 map 里找一个数”,每个位置只做一次查找,所以降到了 O(n)。
