题目描述
给你一个整数数组
nums和一个整数k,请你统计并返回 该数组中和为k的子数组的个数。子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2 输出:2示例 2:
输入:nums = [1,2,3], k = 3 输出:2提示:
1 <= nums.length <= 2 * 104-1000 <= nums[i] <= 1000-107 <= k <= 107
方法一:暴力
暴力枚举
先使用两层for循环遍历,然后根据变量sum和count分别累积和以及计算符合要求的子串的个数,外层循环每遍历一次都将sum置零,然后内层循环挨个遍历,计算子串个数,当sum和k相等的时候,count就+1,说明当前子串的和为k,具体代码如下:
java
class Solution {
public int subarraySum(int[] nums, int k) {
int count = 0;
for(int i=0;i<nums.length;i++){
int sum = 0;
for(int j = i;j<nums.length;j++){
sum+=nums[j];
if(sum == k ){
count++;
}
}
}
return count;
}
}
方法二:前缀和
1、暴力法的问题在哪?
暴力法枚举所有起点 i 和终点 j,计算子数组和,时间复杂度 O(n²)。
当 n = 10⁵ 时,O(n²) 就是 100 亿次操作,超时!
所以我们需要一个 线性时间 的方法。
2、引入"前缀和"思想
什么是前缀和?
前缀和
prefixSum[i]表示从nums[0]到nums[i-1]的和(也可以定义为到i,看习惯)。我们用一个变量
sum动态记录从开头到当前位置的累计和。比如:
javanums = [1, 2, 3, -2, 5] 前缀和依次为: i=0: sum = 1 i=1: sum = 3 i=2: sum = 6 i=3: sum = 4 i=4: sum = 9
关键观察:
如果存在两个位置
j < i,使得:
prefixSum[i] - prefixSum[j] == k那么从
j+1到i的子数组和就是k!
换句话说:
我们要找有多少个 j < i,使得 prefixSum[j] == prefixSum[i] - k
这就把问题转化成了:在遍历过程中,快速查找"之前出现过多少次某个前缀和值"。
3、用哈希表加速查找
我们可以用一个 HashMap<Integer, Integer> 来记录:
-
key:某个前缀和的值
-
value:这个前缀和出现的次数
这样,每到一个新位置 i,我们:
-
计算当前前缀和
sum -
查看
sum - k在哈希表中出现了多少次 → 这就是以i结尾、和为k的子数组个数 -
把当前
sum加入哈希表(计数 +1)
4、举个例子理解
nums = [1, 2, 3], k = 3
我们希望找到和为 3 的子数组:
-
1,2
-
3\] → 共 2 个
初始化:
map = {0: 1} // 重要!表示前缀和为 0 出现了 1 次(空数组)
sum = 0
count = 0
为什么要有
map.put(0, 1)? 因为如果某个前缀和正好等于k,比如sum = 3, k = 3,那么sum - k = 0,我们需要知道"前缀和 0 出现过",才能算这个子数组。所以初始放一个 0。
i = 0, num = 1
-
sum = 0 + 1 = 1 -
sum - k = 1 - 3 = -2 -
map中没有 -2 → count += 0 -
把
sum=1加入 map → map = {0:1, 1:1}
i = 1, num = 2
-
sum = 1 + 2 = 3 -
sum - k = 3 - 3 = 0 -
map.get(0) = 1→ count += 1 ✅(对应子数组 [1,2]) -
把
sum=3加入 map → map = {0:1, 1:1, 3:1}
i = 2, num = 3
-
sum = 3 + 3 = 6 -
sum - k = 6 - 3 = 3 -
map.get(3) = 1→ count += 1 ✅(对应子数组 [3]) -
把
sum=6加入 map → map = {0:1, 1:1, 3:1, 6:1}
最终 count = 2
5、写出代码(Java)
java
import java.util.HashMap;
import java.util.Map;
class Solution {
public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> prefixSumCount = new HashMap<>();
prefixSumCount.put(0, 1); // 关键!处理从开头开始的子数组
int sum = 0;
int count = 0;
for (int num : nums) {
sum += num; // 当前前缀和
// 查找是否存在前缀和 = sum - k
if (prefixSumCount.containsKey(sum - k)) {
count += prefixSumCount.get(sum - k);
}
// 更新当前前缀和的出现次数
prefixSumCount.put(sum, prefixSumCount.getOrDefault(sum, 0) + 1);
}
return count;
}
}
6、涉及的 Java 知识点
| 知识点 | 说明 |
|---|---|
HashMap<K, V> |
用于 O(1) 时间查找和更新前缀和出现次数 |
getOrDefault(key, default) |
如果 key 不存在,返回默认值,避免 null |
增强 for 循环 for (int num : nums) |
简洁遍历数组 |
| 自动装箱/拆箱 | int 和 Integer 自动转换 |
7、算法思想总结
| 思想 | 说明 |
|---|---|
| 前缀和 | 将子数组和问题转化为两个前缀和的差 |
| 哈希表优化 | 用空间换时间,避免重复计算 |
| 数学转化 | sum[i] - sum[j] = k ⇨ sum[j] = sum[i] - k |
| 边界处理 | 初始放入 0:1 是关键技巧 |
8、复杂度分析
-
时间复杂度:O(n) ------ 只遍历一次数组,每次哈希操作 O(1)
-
空间复杂度:O(n) ------ 哈希表最多存 n 个不同前缀和
9、总结
| 方法 | 时间 | 空间 | 是否推荐 |
|---|---|---|---|
| 暴力双循环 | O(n²) | O(1) | 小数据可用 |
| 前缀和 + 哈希表 | O(n) | O(n) | 强烈推荐!面试常考 |