力扣解题-560. 和为 K 的子数组
给你一个整数数组 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
第一次解答
解题思路
核心方法:前缀和 + 哈希表统计频次,将暴力枚举子数组的O(n²)时间复杂度优化至O(n),利用前缀和的数学性质快速定位和为k的子数组,是该问题的最优解法。
核心原理铺垫
前缀和定义:prefixSum[i] 表示数组前i个元素的和(prefixSum[0] = 0,prefixSum[1] = nums[0],prefixSum[2] = nums[0]+nums[1],以此类推)。
子数组和公式:对于子数组nums[start...end](从下标start到end),其和 = prefixSum[end+1] - prefixSum[start]。
问题转化:要找和为k的子数组,即满足 prefixSum[end+1] - prefixSum[start] = k → prefixSum[start] = prefixSum[end+1] - k。
因此,遍历到end位置时,只需统计之前出现过的prefixSum[start] = (当前前缀和 - k)的次数,即可得到以end为结尾的、和为k的子数组个数。
具体步骤
- 初始化关键变量:
prefixCount:HashMap<Long, Integer>,键为前缀和,值为该前缀和出现的次数(解决重复前缀和的统计问题);- 初始化
prefixCount.put(0L, 1):表示“前缀和为0”的情况出现1次(对应“空前缀”,即数组开始之前的虚拟前缀,用于处理从下标0开始的子数组); currSum:long类型的当前前缀和(避免int溢出,因为nums元素可正可负,累加后可能超出int范围),初始值为0;count:统计和为k的子数组总数,初始值为0。
- 遍历数组计算前缀和:
- 遍历每个元素
num,将num累加到currSum,得到当前前缀和; - 计算目标前缀和
target = currSum - k:若存在该前缀和,说明有子数组以当前位置结尾且和为k; - 若
prefixCount包含target,则将count加上prefixCount.get(target)(即符合条件的子数组个数); - 将当前前缀和
currSum存入prefixCount:若已存在则次数+1,不存在则初始化为1(prefixCount.getOrDefault(currSum, 0)+1)。
- 遍历每个元素
- 返回结果:遍历完成后,
count即为和为k的子数组总数,直接返回。
核心优化逻辑说明
- 时间复杂度优化:暴力解法需枚举所有子数组(O(n²)),无法适配
n=2×10⁴的规模;前缀和+哈希表仅需一次遍历(O(n)),哈希表的查找/插入均为O(1),整体时间复杂度为O(n),完全满足题目性能要求。 - 数据类型选择:使用
long存储前缀和,避免因nums元素累加(如2×10⁴个1000相加)导致int溢出,保证计算准确性。 - 性能表现说明:
- 耗时30ms击败25.84%的用户:该解法是本题的标准最优解,多数提交均采用此逻辑,耗时差异主要来自评测机环境(如哈希表的哈希冲突、JVM优化等);
- 内存消耗48.3MB击败23.38%的用户:核心原因是哈希表存储了所有前缀和的频次,空间复杂度为O(n)(最坏情况下所有前缀和唯一),属于合理的空间开销。
执行耗时:30 ms,击败了25.84% 的Java用户 内存消耗:48.3 MB,击败了23.38% 的Java用户
public int subarraySum(int[] nums, int k) {
Map<Long,Integer> prefixCount=new HashMap<>();
prefixCount.put(0L,1);
long currSum=0;
int count=0;
for(int num:nums){
currSum=currSum+num;
long target=currSum-k;
if(prefixCount.containsKey(target)){
count=count+prefixCount.get(target);
}
prefixCount.put(currSum,prefixCount.getOrDefault(currSum,0)+1);
}
return count;
}
扩展1:输出符合条件的子数组
扩展原理
在基础解法的哈希表中,不再仅存储前缀和的出现次数,而是存储该前缀和对应的所有索引(即前缀和结束的位置)。遍历过程中,找到目标前缀和后,通过索引还原出具体的子数组元素。
具体逻辑
prefixMap:Map<Long, List<Integer>>,键为前缀和,值为该前缀和出现的所有下标(如前缀和0对应下标-1,前缀和1对应下标0等);- 初始化
prefixMap.put(0L, new ArrayList<>(Arrays.asList(-1))):空前缀的下标为-1,用于还原从0开始的子数组; - 遍历到下标
i时,若找到目标前缀和target,则遍历prefixMap.get(target)中的所有起始前缀下标startIndex:- 子数组的实际起始下标为
startIndex + 1(因为startIndex是前缀和的结束下标,子数组从下一个位置开始); - 子数组的结束下标为
i,遍历[startIndex+1, i]的元素,封装为列表加入结果;
- 子数组的实际起始下标为
- 每次遍历后,将当前下标
i加入prefixMap中对应前缀和的列表,记录该前缀和的出现位置。
public List<List<Integer>> subarraysWithSumK(int[] nums, int k) {
List<List<Integer>> result = new ArrayList<>();
// Map<前缀和, 所有对应的前缀索引(即前缀和结束的位置)>
Map<Long, List<Integer>> prefixMap = new HashMap<>();
// 初始化:空前缀,前缀和=0,对应索引=-1(表示在数组开始之前)
prefixMap.put(0L, new ArrayList<>(Arrays.asList(-1)));
long currSum = 0;
for (int i = 0; i < nums.length; i++) {
currSum += nums[i];
long target = currSum - k;
// 如果存在 target,说明有子数组以 i 结尾且和为 k
if (prefixMap.containsKey(target)) {
for (int startIndex : prefixMap.get(target)) {
// 子数组从 startIndex + 1 到 i
List<Integer> subarray = new ArrayList<>();
for (int j = startIndex + 1; j <= i; j++) {
subarray.add(nums[j]);
}
result.add(subarray);
}
}
// 将当前前缀和对应的索引 i 加入 map
prefixMap.computeIfAbsent(currSum, key -> new ArrayList<>()).add(i);
}
return result;
}
扩展2:输出符合条件的子数组的索引
扩展原理
与“输出子数组元素”的逻辑一致,仅将“收集元素”改为“收集子数组的起始/结束下标”,无需遍历元素,直接通过索引计算得到子数组的区间[start, end],效率更高。
具体逻辑
- 结果列表
result存储int[],每个数组包含两个元素:子数组的起始下标和结束下标; - 找到目标前缀和
target后,遍历其对应的起始前缀下标startPrefix:- 子数组起始下标 =
startPrefix + 1(空前缀下标-1对应起始下标0); - 子数组结束下标 = 当前遍历的下标
i; - 将
new int[]{start, end}加入结果列表;
- 子数组起始下标 =
- 其余逻辑与“输出子数组”完全一致,仅结果形式不同。
public List<int[]> subarrayIndices(int[] nums, int k) {
List<int[]> result = new ArrayList<>();
Map<Long, List<Integer>> prefixMap = new HashMap<>();
prefixMap.put(0L, Arrays.asList(-1));
long currSum = 0;
for (int i = 0; i < nums.length; i++) {
currSum += nums[i];
long target = currSum - k;
if (prefixMap.containsKey(target)) {
for (int startPrefix : prefixMap.get(target)) {
int start = startPrefix + 1;
int end = i;
result.add(new int[]{start, end});
}
}
prefixMap.computeIfAbsent(currSum, x -> new ArrayList<>()).add(i);
}
return result;
}
总结
- 基础解法的核心是前缀和+哈希表:利用数学公式将子数组和问题转化为前缀和的差值问题,通过哈希表统计前缀和频次,将时间复杂度优化至O(n);
- 扩展解法的核心是存储前缀和的索引:在基础解法的哈希表中,将“频次”改为“索引列表”,通过索引还原子数组的区间或元素,实现从“统计个数”到“还原具体子数组”的扩展;
- 本题的关键优化点:
- 用
long存储前缀和避免溢出; - 初始化前缀和0对应下标-1,处理从数组起始位置开始的子数组;
- 哈希表的核心作用是快速查找目标前缀和,避免重复计算。
- 用