560.和为K的子数组
560.1 题目
给你一个整数数组 nums 和一个整数 k,请你统计并返回该数组中和为 k 的子数组的个数。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
提示:
1 <= nums.length <= 2 * 10^4-1000 <= nums[i] <= 1000-10^7 <= k <= 10^7
560.2 题解
方法一:枚举法(暴力解法)
思路
双层循环,枚举所有可能的子数组 [start, end]。对固定的 start,从 end = start 开始累加,动态维护子数组和。每次更新和之后,如果等于 k,答案加一。
核心思想:枚举所有子数组,计算和是否等于 k。
具体步骤:
- 外层循环枚举起始位置
start。 - 内层循环从
start开始累加元素。 - 每次累加后检查和是否等于
k。
举例:对于 nums = [1,1,1], k = 2:
start=0:sum=1,sum=2✓,sum=3start=1:sum=1,sum=2✓start=2:sum=1- 结果:
2
复杂度分析
- 时间复杂度:
O(n^2)。 - 空间复杂度:
O(1)。
代码
// 方法一:枚举法(暴力解法)
// 核心思路:枚举所有可能的子数组起始位置,计算每个子数组的和,统计和为k的个数
// 时间复杂度:O(n²),其中n是数组长度。外层循环枚举起始位置,内层循环计算子数组和
// 空间复杂度:O(1),只需要常数空间存放变量
// 优点:思路简单直接,容易理解和实现
// 缺点:时间复杂度高,不适合大数据量
static int SubarraySum1(int[] nums, int k)
{
int count = 0; // 统计和为k的子数组个数
int numsLength = nums.Length; // 数组长度
// 外层循环:枚举所有可能的子数组起始位置(start从0到n-1)
for (int start = 0; start < numsLength; start++)
{
int sum = 0; // 记录从start到当前end位置的子数组和
// 内层循环:枚举从start开始到数组末尾的所有子数组
// 注意:这里是从start开始向后累加,而不是从末尾向前
for (int end = start; end < numsLength; end++)
{
// 累加当前元素到子数组和中
sum += nums[end];
// 如果当前子数组和等于k,则计数加1
if (sum == k)
{
count++;
}
}
}
return count; // 返回所有和为k的子数组个数
}
方法二:前缀和 + 哈希表优化
思路
维护前缀和 prefixSum[i] = nums[0] + ... + nums[i]。
子数组 [j+1, i] 的和为:prefixSum[i] - prefixSum[j]。
想要这段子数组和为 k,要满足:prefixSum[j] = prefixSum[i] - k。
遍历数组时,一边维护当前前缀和,一边用哈希表统计所有出现过的前缀和次数:
- 每到一个新位置
i,先看之前有没有出现过prefixSum - k,有的话说明存在若干个j使得[j+1, i]的和为k - 再把当前的
prefixSum记录进哈希表
关键细节:初始化时让前缀和 0 出现过一次,处理从下标 0 开始的子数组。
举个例子:nums = [1,1,1], k = 2
- prefixSum=0,map={0:1}
- i=0:prefixSum=1,找1-2=-1没有,map={0:1, 1:1}
- i=1:prefixSum=2,找2-2=0有1个,count=1,map={0:1, 1:1, 2:1}
- i=2:prefixSum=3,找3-2=1有1个,count=2
- 结果:2
复杂度分析
- 时间复杂度:
O(n) - 空间复杂度:
O(n)
代码
// 方法二:前缀和+哈希表优化法
//
// 核心思路(通俗理解):
// 想象你在一条路上走,边走边记录"累计走了多远"(前缀和)
// 当你发现"当前累计距离 - 目标距离k"在记录本里出现过
// 说明从那个记录点到当前位置的距离正好是k!
//
// 数学原理(通俗比喻):
// 想象你在一条路上走,路上有里程碑:
// prefixSum[i] = 从起点走到第i个里程碑的总距离
// prefixSum[j] = 从起点走到第j个里程碑的总距离
//
// 那么:从第j+1个里程碑到第i个里程碑的距离 = prefixSum[i] - prefixSum[j]
//
// 如果这个距离正好等于k:
// prefixSum[i] - prefixSum[j] = k
// 移项得:prefixSum[j] = prefixSum[i] - k
//
// 所以:当我们走到第i个里程碑时,只需要检查之前是否有人走到过"prefixSum[i] - k"这个距离
// 如果有,说明从那个里程碑到当前位置的距离正好是k!
//
// 时间复杂度:O(n),只需要一次遍历数组
// 空间复杂度:O(n),需要哈希表存储前缀和出现次数
// 优点:时间复杂度最优,适合大数据量
// 缺点:需要理解前缀和概念,实现相对复杂
static int SubarraySum2(int[] nums, int k)
{
int count = 0; // 统计和为k的子数组个数
int prefixSum = 0; // 记录当前前缀和(从0到当前位置的元素和)
// 哈希表:Key=前缀和的值,Value=该前缀和出现的次数
// 这个哈希表就像是我们的"累计和记录本"
Dictionary<int, int> prefixSumCountDict = new Dictionary<int, int>();
// 关键初始化:前缀和为0出现1次(表示空数组的情况)
// 为什么需要这个初始化?
// 当prefixSum == k时,说明从开头到当前位置的和就是k
// 此时我们需要找到prefixSum[j] = 0(即空数组的情况)
// 举例:nums = [1,1,1], k = 2,当走到第二个元素时:
// prefixSum = 2,prefixSum - k = 0,正好找到初始化时记录的0
prefixSumCountDict.Add(0, 1);
// 开始遍历数组,一步一步走
for (int i = 0; i < nums.Length; i++)
{
// 计算当前前缀和:prefixSum[i] = prefixSum[i-1] + nums[i]
// 相当于在当前位置的"累计距离"
prefixSum += nums[i];
// 核心逻辑:查找是否存在前缀和为 (prefixSum - k) 的情况
// 如果存在,说明存在子数组 [j+1, i] 的和为k
// 因为:prefixSum[i] - prefixSum[j] = k => prefixSum[j] = prefixSum[i] - k
//
// 举例说明(nums = [1,1,1], k = 2):
// 当i=2时,prefixSum=3,我们需要找prefixSum-k=3-2=1
// 哈希表中prefixSum=1出现过1次(在i=0时),说明存在子数组[1,2]的和为2
if (prefixSumCountDict.ContainsKey(prefixSum - k))
{
// 累加所有满足条件的前缀和出现次数
// 注意:这里可能不止出现一次,所以是累加而不是简单的+1
count += prefixSumCountDict[prefixSum - k];
// 调试信息:可以取消注释查看具体找到的子数组
// Console.WriteLine($"找到子数组:从某个位置到{i},和为{k}");
}
// 更新哈希表:将当前前缀和加入哈希表
// 这一步很重要!我们要记录当前累计和,供后面的位置使用
if (prefixSumCountDict.ContainsKey(prefixSum))
{
prefixSumCountDict[prefixSum]++; // 如果已存在,计数加1
}
else
{
prefixSumCountDict.Add(prefixSum, 1); // 如果不存在,添加新记录
}
// 调试信息:可以取消注释查看每一步的哈希表状态
// Console.WriteLine($"位置{i}:prefixSum={prefixSum}, 哈希表={string.Join(",", prefixSumCountDict)}");
}
return count; // 返回所有和为k的子数组个数
}
560.3 代码
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
#region 题目
// 给你一个整数数组 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
#endregion
#region 测试
// 示例 1
int[] nums1 = { 1, 1, 1 };
int k1 = 2;
int result11 = SubarraySum1(nums1, k1);
int result12 = SubarraySum2(nums1, k1);
Console.WriteLine($"示例1 方法1(枚举法)输出:{result11}");
Console.WriteLine($"示例1 方法2(前缀和法)输出:{result12}");
// 示例 2
int[] nums2 = { 1, 2, 3 };
int k2 = 3;
int result21 = SubarraySum1(nums2, k2);
int result22 = SubarraySum2(nums2, k2);
Console.WriteLine($"示例2 方法1(枚举法)输出:{result21}");
Console.WriteLine($"示例2 方法2(前缀和法)输出:{result22}");
#endregion
}
#region 答案
// 方法一:枚举法(暴力解法)
// 核心思路:枚举所有可能的子数组起始位置,计算每个子数组的和,统计和为k的个数
// 时间复杂度:O(n²),其中n是数组长度。外层循环枚举起始位置,内层循环计算子数组和
// 空间复杂度:O(1),只需要常数空间存放变量
// 优点:思路简单直接,容易理解和实现
// 缺点:时间复杂度高,不适合大数据量
static int SubarraySum1(int[] nums, int k)
{
int count = 0; // 统计和为k的子数组个数
int numsLength = nums.Length; // 数组长度
// 外层循环:枚举所有可能的子数组起始位置(start从0到n-1)
for (int start = 0; start < numsLength; start++)
{
int sum = 0; // 记录从start到当前end位置的子数组和
// 内层循环:枚举从start开始到数组末尾的所有子数组
// 注意:这里是从start开始向后累加,而不是从末尾向前
for (int end = start; end < numsLength; end++)
{
// 累加当前元素到子数组和中
sum += nums[end];
// 如果当前子数组和等于k,则计数加1
if (sum == k)
{
count++;
}
}
}
return count; // 返回所有和为k的子数组个数
}
// 方法二:前缀和+哈希表优化法
//
// 核心思路(通俗理解):
// 想象你在一条路上走,边走边记录"累计走了多远"(前缀和)
// 当你发现"当前累计距离 - 目标距离k"在记录本里出现过
// 说明从那个记录点到当前位置的距离正好是k!
//
// 数学原理(通俗比喻):
// 想象你在一条路上走,路上有里程碑:
// prefixSum[i] = 从起点走到第i个里程碑的总距离
// prefixSum[j] = 从起点走到第j个里程碑的总距离
//
// 那么:从第j+1个里程碑到第i个里程碑的距离 = prefixSum[i] - prefixSum[j]
//
// 如果这个距离正好等于k:
// prefixSum[i] - prefixSum[j] = k
// 移项得:prefixSum[j] = prefixSum[i] - k
//
// 所以:当我们走到第i个里程碑时,只需要检查之前是否有人走到过"prefixSum[i] - k"这个距离
// 如果有,说明从那个里程碑到当前位置的距离正好是k!
//
// 时间复杂度:O(n),只需要一次遍历数组
// 空间复杂度:O(n),需要哈希表存储前缀和出现次数
// 优点:时间复杂度最优,适合大数据量
// 缺点:需要理解前缀和概念,实现相对复杂
static int SubarraySum2(int[] nums, int k)
{
int count = 0; // 统计和为k的子数组个数
int prefixSum = 0; // 记录当前前缀和(从0到当前位置的元素和)
// 哈希表:Key=前缀和的值,Value=该前缀和出现的次数
// 这个哈希表就像是我们的"累计和记录本"
Dictionary<int, int> prefixSumCountDict = new Dictionary<int, int>();
// 关键初始化:前缀和为0出现1次(表示空数组的情况)
// 为什么需要这个初始化?
// 当prefixSum == k时,说明从开头到当前位置的和就是k
// 此时我们需要找到prefixSum[j] = 0(即空数组的情况)
// 举例:nums = [1,1,1], k = 2,当走到第二个元素时:
// prefixSum = 2,prefixSum - k = 0,正好找到初始化时记录的0
prefixSumCountDict.Add(0, 1);
// 开始遍历数组,一步一步走
for (int i = 0; i < nums.Length; i++)
{
// 计算当前前缀和:prefixSum[i] = prefixSum[i-1] + nums[i]
// 相当于在当前位置的"累计距离"
prefixSum += nums[i];
// 核心逻辑:查找是否存在前缀和为 (prefixSum - k) 的情况
// 如果存在,说明存在子数组 [j+1, i] 的和为k
// 因为:prefixSum[i] - prefixSum[j] = k => prefixSum[j] = prefixSum[i] - k
//
// 举例说明(nums = [1,1,1], k = 2):
// 当i=2时,prefixSum=3,我们需要找prefixSum-k=3-2=1
// 哈希表中prefixSum=1出现过1次(在i=0时),说明存在子数组[1,2]的和为2
if (prefixSumCountDict.ContainsKey(prefixSum - k))
{
// 累加所有满足条件的前缀和出现次数
// 注意:这里可能不止出现一次,所以是累加而不是简单的+1
count += prefixSumCountDict[prefixSum - k];
// 调试信息:可以取消注释查看具体找到的子数组
// Console.WriteLine($"找到子数组:从某个位置到{i},和为{k}");
}
// 更新哈希表:将当前前缀和加入哈希表
// 这一步很重要!我们要记录当前累计和,供后面的位置使用
if (prefixSumCountDict.ContainsKey(prefixSum))
{
prefixSumCountDict[prefixSum]++; // 如果已存在,计数加1
}
else
{
prefixSumCountDict.Add(prefixSum, 1); // 如果不存在,添加新记录
}
// 调试信息:可以取消注释查看每一步的哈希表状态
// Console.WriteLine($"位置{i}:prefixSum={prefixSum}, 哈希表={string.Join(",", prefixSumCountDict)}");
}
return count; // 返回所有和为k的子数组个数
}
#endregion
}
560.4 运行结果
示例1 方法1(枚举法)输出:2
示例1 方法2(前缀和法)输出:2
示例2 方法1(枚举法)输出:2
示例2 方法2(前缀和法)输出:2
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com