【Hot100】4.前缀和
这一类的核心思想是:将“子数组求和”问题转化为“两数之差”问题。
- 定义
preSum[i]为从数组开头到第i个元素的累加和。 - 任意子数组
nums[j...i]的和可以表示为:preSum[i] - preSum[j-1]。 - 如果我们要找和为
k的子数组,即寻找满足preSum[i] - preSum[j-1] = k的对数。 - 变换公式:
preSum[j-1] = preSum[i] - k。 - 核心技巧:在遍历计算
preSum[i]时,用HashMap记录之前出现过的preSum值及其次数。每次只需查询map中是否存在preSum[i] - k。
📝 本类题型总结
| 题目 | 核心转换 | 关键差异点 (vs 和为 K 的子数组) |
|---|---|---|
| 560. 和为 K | Pre[i] - Pre[j] = k |
基准:Map 存 <前缀和,次数>。查 Pre[i] - k。 |
| 974. 被 K 整除 | Pre[i] % k == Pre[j] % k |
Map 存 <余数,次数>。需处理负数余数 (a%k+k)%k。 |
| 523. 连续子数组和 | 同余 + 长度限制 | Map 存 <余数,最早索引>。判断 i - index >= 2。初始化 put(0, -1)。 |
🏆 母题:LeetCode 560. 和为 K 的子数组 (Subarray Sum Equals K)
题目内容:给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k 的连续子数组的个数。
核心逻辑:
- 初始化
map,放入(0, 1),表示前缀和为 0 出现了 1 次(处理从索引 0 开始的子数组)。 - 遍历数组,累加得到当前前缀和
preSum。 - 检查
map中是否有preSum - k。如果有,说明存在若干个子数组满足条件,累加其次数。 - 将当前
preSum存入map。
import java.util.HashMap;
import java.util.Map;
class Solution {
public int subarraySum(int[] nums, int k) {
// Key: 前缀和数值, Value: 该前缀和出现的次数
Map<Integer, Integer> map = new HashMap<>();
// ⚠️ [核心初始化] 前缀和为 0 出现 1 次
// 作用:当 preSum == k 时,preSum - k = 0,能查到这个初始值,代表从开头到当前的子数组满足条件
map.put(0, 1);
int count = 0;
int preSum = 0;
for (int num : nums) {
preSum += num;
// ⚠️ [核心逻辑] 查找是否存在前缀和为 (preSum - k) 的历史记录
if (map.containsKey(preSum - k)) {
count += map.get(preSum - k);
}
// 更新当前前缀和的出现次数
map.put(preSum, map.getOrDefault(preSum, 0) + 1);
}
return count;
}
}
🔄 变体 1:LeetCode 974. 和可被 K 整除的子数组 (Subarray Sums Divisible by K)
题目内容:给定一个整数数组 nums 和一个整数 k ,返回其中元素之和可被 k 整除的非空 子数组 的数目。
相比母题的变化:
- 数学转换:
- 母题要求
preSum[i] - preSum[j] = k。 - 本题要求
(preSum[i] - preSum[j]) % k == 0。 - 根据同余定理,这等价于
preSum[i] % k == preSum[j] % k。 - 结论:我们不再存
preSum本身,而是存preSum % k(余数)。如果两个前缀和的余数相同,它们之间的子数组和一定能被k整除。
- 母题要求
- 负数处理:Java 中
%运算对负数会返回负余数(如-5 % 3 = -2),但数学上我们需要正余数(1)。- 修正公式:
remainder = (preSum % k + k) % k。
- 修正公式:
- 统计逻辑:如果某个余数出现了
n次,那么从中任选两个都能组成合法子数组,贡献的数量是n * (n-1) / 2。或者像母题一样,每遇到一次就累加之前出现的次数。
import java.util.HashMap;
import java.util.Map;
class Solution {
public int subarraysDivByK(int[] nums, int k) {
// Key: 余数, Value: 该余数出现的次数
Map<Integer, Integer> map = new HashMap<>();
// ⚠️ [变化点 1] 初始化:余数为 0 出现 1 次
map.put(0, 1);
int count = 0;
int preSum = 0;
for (int num : nums) {
preSum += num;
// ⚠️ [变化点 2] 计算余数,重点处理负数情况
// Java 的 % 可能返回负数,例如 -5 % 3 = -2,我们需要的是 1
// 公式:(a % n + n) % n 保证结果为正
int remainder = (preSum % k + k) % k;
// ⚠️ [变化点 3] 逻辑同母题:如果之前出现过相同的余数,说明中间这段和能被 k 整除
if (map.containsKey(remainder)) {
count += map.get(remainder);
}
map.put(remainder, map.getOrDefault(remainder, 0) + 1);
}
return count;
}
}
🔄 变体 2:LeetCode 523. 连续的子数组和 (Continuous Subarray Sum)
题目内容:给你一个整数数组 nums 和一个整数 k ,如果 nums 有一个 好的子数组 返回 true ,否则返回 false 。
好的子数组定义:
- 长度 至少为 2。
- 子数组元素总和为
k的倍数。
相比母题的变化: - 返回值:返回
boolean,找到即返回true。 - 长度限制:这是最大的不同。母题不限制长度,本题要求长度
>= 2。- 实现技巧:
map中存储的不是“次数”,而是“该余数第一次出现的索引”。 - 当再次遇到相同余数时,计算
currentIndex - firstIndex。如果差值>= 2,则满足条件。
- 实现技巧:
- 初始化细节:
map.put(0, -1)。- 解释:如果前缀和本身就能被
k整除(余数为 0),且长度为 2(即索引 0 和 1),那么currentIndex (1) - firstIndex (-1) = 2,满足条件。所以初始索引设为-1。
- 解释:如果前缀和本身就能被
import java.util.HashMap;
import java.util.Map;
class Solution {
public boolean checkSubarraySum(int[] nums, int k) {
// Key: 余数, Value: 该余数第一次出现的索引
Map<Integer, Integer> map = new HashMap<>();
// ⚠️ [变化点 1] 初始化:余数 0 对应的索引为 -1
// 目的:处理从索引 0 开始的子数组,且满足长度 >= 2 的情况
map.put(0, -1);
int preSum = 0;
for (int i = 0; i < nums.length; i++) {
preSum += nums[i];
int remainder = (preSum % k + k) % k; // 处理负数
if (map.containsKey(remainder)) {
// ⚠️ [变化点 2] 检查长度限制
// 获取该余数第一次出现的索引
int firstIndex = map.get(remainder);
// 如果当前索引 - 第一次出现的索引 >= 2,说明子数组长度至少为 2
if (i - firstIndex >= 2) {
return true;
}
// 注意:如果长度不够,我们【不更新】map 中的索引,保留最早的索引以便后续匹配更长的子数组
} else {
// 只有当余数没出现过时,才记录索引(记录最早的)
map.put(remainder, i);
}
}
return false;
}
}

浙公网安备 33010602011771号