文章目录
1. 题目描述
给你一个整数数组 nums
和一个整数 k
,请你统计并返回该数组中和为 k
的连续子数组的个数。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
解释:此时有两个和为 2 的连续子数组:[1,1] 与 [1,1]
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
解释:此时有两个和为 3 的连续子数组:[1,2] 与 [3]
提示:
1 <= nums.length <= 2 * 10^4
-1000 <= nums[i] <= 1000
-10^7 <= k <= 10^7
2. 理解题目
这道题要求我们找出数组中所有和为k的连续子数组的数量。需要注意以下几点:
- 子数组必须是连续的,不能跳过元素
- 我们需要统计的是子数组的个数,而不是具体的子数组
- 数组元素可能包含负数,这意味着子数组的和可能增加也可能减少
- 同一位置的元素可以在不同的子数组中使用
解释一下示例:
-
对于
nums = [1,1,1], k = 2
:- 子数组 [1,1](索引 0 和 1)的和为 2
- 子数组 [1,1](索引 1 和 2)的和为 2
- 所以总共有 2 个和为 2 的子数组
-
对于
nums = [1,2,3], k = 3
:- 子数组 [1,2](索引 0 和 1)的和为 3
- 子数组 [3](索引 2)的和为 3
- 所以总共有 2 个和为 3 的子数组
3. 解法一:暴力枚举法
3.1 思路
最直观的方法是通过两层嵌套循环,枚举所有可能的子数组,然后计算它们的和。如果子数组的和等于 k,则计数器加 1。
具体步骤:
- 初始化计数器
count = 0
- 外层循环遍历所有可能的子数组起点
- 内层循环遍历所有可能的子数组终点
- 计算从起点到终点的子数组的和
- 如果和等于 k,则
count++
- 返回
count
3.2 Java代码实现
public class Solution {
public int subarraySum(int[] nums, int k) {
int count = 0;
int n = nums.length;
// 枚举所有可能的子数组
for (int i = 0; i < n; i++) {
int sum = 0;
for (int j = i; j < n; j++) {
// 计算子数组 nums[i...j] 的和
sum += nums[j];
// 如果子数组的和等于k,计数器加1
if (sum == k) {
count++;
}
}
}
return count;
}
}
3.3 代码详解
- 外层循环从索引 0 开始,遍历到 n-1,表示子数组的起始位置
- 内层循环从索引 i 开始,遍历到 n-1,表示子数组的结束位置
- 在内层循环中,我们累加
nums[j]
得到子数组nums[i...j]
的和 - 如果累加和等于 k,则将计数器
count
加 1 - 最后返回计数器的值
3.4 复杂度分析
- 时间复杂度:O(n²),其中 n 是数组长度。外层循环执行 n 次,内层循环平均执行 n/2 次,总体为 O(n²)。
- 空间复杂度:O(1),只需要常数级的额外空间来存储计数器和求和变量。
3.5 适用场景
暴力法适用于数组长度较小的情况,但当数组长度较大时,O(n²) 的时间复杂度可能导致超时。
4. 解法二:前缀和解法
4.1 思路
暴力法的一个主要问题是重复计算了很多子数组的和。为了优化这一点,我们可以使用前缀和技术,这样可以在 O(1) 时间内得到任意子数组的和。
前缀和数组的定义为:preSum[i]
表示 nums[0] + nums[1] + ... + nums[i-1]
的和。
利用前缀和,我们可以在 O(1) 时间内计算子数组 nums[i...j]
的和:sum = preSum[j+1] - preSum[i]
。
具体步骤:
- 计算前缀和数组
preSum
- 使用两层循环枚举所有可能的子数组
- 使用前缀和公式计算子数组的和
- 如果和等于 k,则计数器加 1
4.2 Java代码实现
public class Solution {
public int subarraySum(int[] nums, int k) {
int count = 0;
int n = nums.length;
// 计算前缀和数组
int[] preSum = new int[n + 1];
for (int i = 0; i < n; i++) {
preSum[i + 1] = preSum[i] + nums[i];
}
// 枚举所有可能的子数组
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
// 使用前缀和计算子数组 nums[i...j] 的和
int sum = preSum[j + 1] - preSum[i];
if (sum == k) {
count++;
}
}
}
return count;
}
}
4.3 代码详解
- 创建一个长度为 n+1 的前缀和数组
preSum
,其中preSum[0] = 0
- 计算前缀和数组:
preSum[i] = preSum[i-1] + nums[i-1]
- 使用两层循环枚举所有可能的子数组:
- 外层循环表示子数组的起始位置
- 内层循环表示子数组的结束位置
- 使用前缀和公式计算子数组的和:
sum = preSum[j+1] - preSum[i]
- 如果
sum == k
,则将计数器count
加 1 - 返回最终的计数器值
4.4 复杂度分析
- 时间复杂度:O(n²),其中 n 是数组长度。虽然使用了前缀和,但我们仍然需要两层循环来枚举所有可能的子数组。
- 空间复杂度:O(n),需要额外的 O(n) 空间来存储前缀和数组。
4.5 适用场景
前缀和解法相比暴力法的优势不在于时间复杂度(两者都是 O(n²)),而在于通过预计算消除了重复计算子数组和的操作,使得内层循环的常数因子更小。
5. 解法三:前缀和 + 哈希表解法
5.1 思路
前面两种解法的时间复杂度都是 O(n²),对于较大的数组可能会超时。我们可以借助哈希表进一步优化,将时间复杂度降低到 O(n)。
关键思路:
- 对于前缀和
preSum[j]
,如果存在某个前缀和preSum[i]
使得preSum[j] - preSum[i] = k
,那么子数组nums[i...j-1]
的和就等于 k - 我们可以转换思路:对于当前前缀和
curr_sum
,我们需要查找之前是否出现过前缀和为curr_sum - k
的位置 - 使用哈希表记录每个前缀和出现的次数,可以在 O(1) 时间内完成查找
具体步骤:
- 创建一个哈希表
map
,用于存储每个前缀和出现的次数 - 初始化
map[0] = 1
(表示空子数组,前缀和为 0,出现 1 次) - 初始化
curr_sum = 0
(当前前缀和)和count = 0
(满足条件的子数组数量) - 遍历数组
nums
:- 累加当前元素到
curr_sum
- 判断
map
中是否存在键curr_sum - k
- 如果存在,则将其对应的值(出现次数)加到
count
中 - 更新哈希表
map[curr_sum]++
- 累加当前元素到
- 返回
count
5.2 Java代码实现
import java.util.HashMap;
import java.util.Map;
public class Solution {
public int subarraySum(int[] nums, int k) {
// 哈希表:前缀和 -> 出现次数
Map<Integer, Integer> prefixSumCount = new HashMap<>();
// 初始化:空子数组的前缀和为0,出现次数为1