目录
[1. 和为 K 的子数组](#1. 和为 K 的子数组)
[1.1 题目解析](#1.1 题目解析)
[1.2 解法](#1.2 解法)
[1.3 代码实现](#1.3 代码实现)
[2. 和可被 K 整除的子数组](#2. 和可被 K 整除的子数组)
[2.1 题目解析](#2.1 题目解析)
[2.2 解法](#2.2 解法)
[2.3 代码实现](#2.3 代码实现)
1. 和为 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
1.1 题目解析
这是一个典型的「连续区间和统计」问题,本质上就是在数组中寻找 有多少个连续子数组的和等于 k。换句话说,它是「区间和查询」的一种变形,只不过不是求和,而是数满足条件的区间个数。
常规解法
最直观的方法是用双层循环:
-
外层枚举子数组的起点 i
-
内层枚举子数组的终点 j,计算 [i, j] 的和并判断是否等于 k。
这种方法简单粗暴,但时间复杂度是 O(n²),在最坏情况下数组长度 2 * 10^4,会有 4 亿次计算,无法在合理时间内完成。
问题的核心在于:我们在计算子数组和时重复了大量运算(比如 [0, 3] 和 [0, 4],它们都需要重新计算 [0, 3] 的和)。要想高效,就必须避免重复计算。
思路转折
要想避免重复计算,就要 预处理区间和。
区间和问题最常见的优化就是 前缀和。
-
设 sum 表示 [0, i] 的元素和。
-
那么子数组的和 = sum - k,所以,只要知道之前有多少个前缀和等于 sum[i] - k,就能统计出以 i 结尾的子数组个数。
1.2 解法
用哈希表记录「某个前缀和出现了几次」。
在遍历数组、计算前缀和的过程中,随时查询 sum - k是否出现过,并累加到答案。
**i)**初始化哈希表:hash.put(0, 1),表示前缀和为 0 出现过一次(相当于前缀和数组的虚拟起点)。
**ii)**遍历数组,累计前缀和 sum。
**iii)**查询 sum - k 是否存在于哈希表中,若存在则加上对应次数。
**iiii)**更新当前前缀和 sum 在哈希表中的出现次数。
**iiiii)**遍历结束,返回答案。
1.3 代码实现
java
class Solution {
public int subarraySum(int[] nums, int k) {
// 哈希表:存前缀和 -> 出现次数
Map<Integer, Integer> hash = new HashMap<>();
hash.put(0, 1); // 模拟前缀和数组 sum[0] = 0
int sum = 0, ans = 0;
for (int x : nums) {
sum += x; // 累加前缀和
// 如果 sum - k 出现过,说明存在子数组和为 k
ans += hash.getOrDefault(sum - k, 0);
// 更新哈希表,记录当前前缀和出现次数
hash.put(sum, hash.getOrDefault(sum, 0) + 1);
}
return ans;
}
}
2. 和可被 K 整除的子数组
974. 和可被 K 整除的子数组 - 力扣(LeetCode)
给定一个整数数组 nums
和一个整数 k
,返回其中元素之和可被 k
整除的非空 子数组 的数目。
子数组 是数组中 连续 的部分。
示例 1:
输入:nums = [4,5,0,-2,-3,1], k = 5
输出:7
解释:
有 7 个子数组满足其元素之和可被 k = 5 整除:
[4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3]
示例 2:
输入: nums = [5], k = 9
输出: 0
提示:
1 <= nums.length <= 3 * 104
-104 <= nums[i] <= 104
2 <= k <= 104
2.1 题目解析
本质上,这是一个 "区间和是否满足某个整除条件" 的问题。
换句话说,我们要统计 有多少个子数组的和能被 K 整除。
常规解法
-
枚举所有子数组(两层循环),
-
计算每个子数组的和,
-
判断是否能被 K 整除。
时间复杂度:
-
枚举子数组 → O(n²)
-
每次计算子数组和 → O(1)(如果用前缀和优化),否则 O(n)
-
总体:O(n²) ~ O(n³),对于 n=3*10⁴ 肯定超时
思路转折
所以问题就转化为:统计前缀和的余数相等的情况, 要想高效 → 必须避免 O(n²) 枚举。
我们可以在遍历数组的同时:
-
维护当前前缀和对 k 的余数,
-
统计这个余数出现过多少次。
这样,每次遇到相同余数,就代表多出一些合法子数组。
2.2 解法
前缀和 + 同余定理 + 哈希表计数
**i)**初始化哈希表:hash.put(0, 1)(默认前缀和为 0 出现一次)。
**ii)**遍历数组 nums:
-
累加前缀和 sum
-
计算余数 r = (sum % k + k) % k(保证非负)
-
统计已有多少前缀和的余数也等于 r → 加入结果
-
更新哈希表:hash[r]++
**iii)**返回结果。
2.3 代码实现
java
class Solution {
public int subarraysDivByK(int[] nums, int k) {
Map<Integer, Integer> hash = new HashMap<>();
hash.put(0, 1); // 前缀和为0的余数,出现1次
int sum = 0, ret = 0;
for (int x : nums) {
sum += x;
int r = (sum % k + k) % k; // 修正负数余数
ret += hash.getOrDefault(r, 0); // 找到已有多少相同余数
hash.put(r, hash.getOrDefault(r, 0) + 1); // 更新出现次数
}
return ret;
}
}