引言
和为 K 的子数组
- 🎈 题目链接:https://leetcode.cn/problems/subarray-sum-equals-k/description/?envType=study-plan-v2&envId=top-100-liked
- 🎈 做题状态:
我的解题
首先想到的是暴力解法,也就是循环两次,然后每次循环都查询满足条件的结果。这种方法超时。
- 算法时间复杂度: O(n^2)
- 空间复杂度:O(1)
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int count = 0;
for (int i = 0; i < nums.size(); ++i)
{
int sum = 0;
for (int j = i; j < nums.size(); ++j)
{
sum += nums[j];
cout << "i = " << i << "; j = " << j << endl;
if (sum == k)
{
++count;
}
}
}
return count;
}
};
前缀和+哈希表 优化
思考:为什么可以使用前缀和优化?
这个题目是求子数组的和等于目标值的情况。然后前缀和相减可以计算任意子数组的和。例如已知前缀和 sum_i 和 sum_j 那么就可以通过 sum_j - sum_i 来计算 [i, j] 这个子数组的和。然后我们可以通过遍历一次计算出所有的前缀和。
思考:为什么使用哈希表?
计算完前缀和之后,我们要快速的查找 sum_j - k 的前缀和是否存在,此时就可以通过哈希表来优化查找效率。
核心问题
我们要找数组中所有 连续子数组的和等于k 的情况。例如,数组 [1,2,3]
,k=3,符合条件的子数组是 [1,2]
和 [3]
。
关键思路:前缀和
前缀和(Prefix Sum)是指从数组起始位置到当前位置所有元素的和。例如,数组 [1,2,3]
的前缀和依次是:
- 到第0位:
1
- 到第1位:
1+2=3
- 到第2位:
1+2+3=6
核心观察:如果前缀和到位置i的和是 sum_i
,到位置j的和是 sum_j
,那么子数组 [i+1, j]
的和就是 sum_j - sum_i
。
我们的目标是找到所有 sum_j - sum_i = k
的情况,即 sum_i = sum_j - k
。如果能快速知道有多少个 sum_i
等于 sum_j - k
,就能立刻知道有多少个子数组以j结尾且和为k。
哈希表的作用
为了快速查找 sum_i = sum_j - k
出现的次数,我们用哈希表存储所有出现过的前缀和及其出现的次数。
- 键(Key):前缀和的值。
- 值(Value):该前缀和出现的次数。
每次计算到当前前缀和 sum_j
时,我们检查哈希表中是否存在 sum_j - k
:
- 如果存在,说明之前有某些位置i的前缀和是
sum_j - k
,这些位置到当前位置j的子数组和就是k。 - 将这些次数累加到结果中。
初始条件:prefixSumCount[0] = 1
这是为了解决从数组开头到当前位置的和正好是k的情况。例如,如果前缀和到j是k,那么 sum_j - k = 0
,此时哈希表中必须已经存在一个前缀和0(即空数组的情况),才能正确计数。
举例说明
以 nums = [1,1,1], k = 2
为例:
- 初始化:
prefixSumCount = {0:1}
,当前和currentSum = 0
,结果count = 0
。 - 遍历第一个元素1:
currentSum = 0 + 1 = 1
- 检查
1 - 2 = -1
,哈希表中没有-1,结果不变。 - 更新哈希表:
{0:1, 1:1}
。
- 遍历第二个元素1:
currentSum = 1 + 1 = 2
- 检查
2 - 2 = 0
,哈希表中有0,出现1次,所以count += 1
。 - 更新哈希表:
{0:1, 1:1, 2:1}
。
- 遍历第三个元素1:
currentSum = 2 + 1 = 3
- 检查
3 - 2 = 1
,哈希表中有1,出现1次,所以count += 1
。 - 更新哈希表:
{0:1, 1:1, 2:1, 3:1}
。
最终结果 count = 2
,对应子数组 [1,1]
和 [1,1]
(注意:实际正确答案应该是 [1,1]
和 [1,1]
,但根据输入数组为 [1,1,1]
,实际子数组是索引0-1和1-2)。
为什么能处理负数?
例如 nums = [3,4,7,2,-3,1,4,2], k = 7
,前缀和可能出现重复或中间波动。但哈希表记录所有历史前缀和,无论正负,都能正确统计所有可能的情况。
总结
- 前缀和:计算到当前位置的总和。
- 哈希表:记录每个前缀和出现的次数。
- 查找目标:对于当前前缀和
sum_j
,查找sum_j - k
是否在哈希表中,如果在,则累加次数。 - 初始化:处理从数组开头开始的子数组。
这个算法将时间复杂度从 O(n²) 优化到 O(n),空间复杂度为 O(n),是一种典型的“空间换时间”策略。
代码
#include <vector>
#include <unordered_map>
class Solution {
public:
int subarraySum(std::vector<int>& nums, int k) {
// 哈希表:记录前缀和及其出现的次数
// key = 前缀和, value = 该前缀和出现的次数
std::unordered_map<int, int> prefixSumCount;
// 初始化:前缀和为 0 出现 1 次(用于处理从数组开头开始的子数组和为k的情况)
prefixSumCount[0] = 1;
int count = 0; // 记录满足条件的子数组数量
int currentSum = 0; // 当前前缀和(从第0个元素到当前元素的和)
// 遍历数组中的每个元素
for (int num : nums) {
// 更新当前前缀和(累加当前元素)
currentSum += num;
// 核心逻辑:如果存在前缀和等于 (currentSum - k),说明存在子数组和为k
// 例如:假设 currentSum = 10,k = 3,则我们需要找的前缀和是 10-3=7
// 如果哈希表中记录前缀和7出现过n次,说明有n个子数组的和为k(即从那些前缀和的位置到当前位置)
if (prefixSumCount.find(currentSum - k) != prefixSumCount.end()) {
count += prefixSumCount[currentSum - k];
}
// 将当前前缀和加入哈希表(如果已存在则次数+1,否则插入1)
prefixSumCount[currentSum]++;
}
return count;
}
};