1. 问题背景与核心挑战
今天想和大家聊聊LeetCode上那道经典的560题——和为K的子数组。这道题在各大公司的面试中出现频率极高,尤其是考察候选人对前缀和与哈希表结合运用的理解。我第一次遇到这个问题是在一次模拟面试中,当时只想到了暴力解法,结果时间复杂度直接爆炸。后来经过系统学习和反复练习,终于掌握了其中的精髓。
这道题的表面要求很简单:给定一个整数数组和一个整数k,我们需要找到该数组中和为k的连续子数组的个数。比如数组[1,1,1]和k=2,那么符合条件的子数组有[1,1]和[1,1](注意这两个子数组虽然元素相同,但位置不同),所以返回2。
关键点:子数组必须是连续的,且不同位置的相同元素组合算作不同的子数组。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 暴力解法:从直观思路开始
2.1 双重循环的实现
最直观的解法当然是暴力枚举所有可能的子数组,然后计算它们的和是否等于k。这种方法虽然效率不高,但对于理解问题本质很有帮助。
python复制def subarraySum(nums, k):
count = 0
n = len(nums)
for i in range(n):
current_sum = 0
for j in range(i, n):
current_sum += nums[j]
if current_sum == k:
count += 1
return count
这个解法的时间复杂度是O(n²),空间复杂度是O(1)。对于小规模数据尚可接受,但当数组长度达到10⁵量级时(这在LeetCode测试用例中很常见),这种解法就会超时。
2.2 暴力解法的局限性
在实际面试中,如果只给出这种解法,面试官很可能会追问优化方案。因为:
- 存在大量重复计算:每次计算子数组和时,都是从头开始累加,没有利用之前计算的结果
- 没有利用题目中的特殊性质——我们只需要知道和等于k的次数,而不需要知道具体是哪些子数组
3. 前缀和:优化思路的突破口
3.1 前缀和的基本概念
前缀和(Prefix Sum)是一种常见的数组处理技巧,它通过预处理数组,使得区间和的查询可以在O(1)时间内完成。具体来说,对于数组nums,我们定义前缀和数组prefix,其中prefix[i]表示nums[0]到nums[i-1]的和。
python复制prefix = [0] * (len(nums) + 1)
for i in range(len(nums)):
prefix[i+1] = prefix[i] + nums[i]
3.2 如何应用于本题
有了前缀和数组后,任意子数组nums[i..j]的和可以表示为prefix[j+1] - prefix[i]。因此,我们的问题转化为:找到所有满足prefix[j+1] - prefix[i] == k的(i,j)对的数量。
这看起来似乎没有减少时间复杂度(仍然是O(n²)),但为我们后续的优化奠定了基础。
4. 哈希表优化:从O(n²)到O(n)
4.1 关键思路转换
观察等式prefix[j+1] - prefix[i] == k,我们可以将其重写为prefix[i] == prefix[j+1] - k。这意味着,对于每个j,我们只需要统计前面有多少个i满足prefix[i] == prefix[j+1] - k。
这正是哈希表大显身手的地方!我们可以在遍历数组时,用哈希表记录各个前缀和出现的次数。
4.2 具体实现步骤
- 初始化一个哈希表prefix_map,记录前缀和及其出现次数。初始时prefix_map = {0:1},因为空数组的和为0
- 维护一个变量current_sum记录当前的前缀和
- 遍历数组,对于每个元素:
- 更新current_sum
- 检查current_sum - k是否在prefix_map中,如果在,则count增加对应的值
- 将current_sum加入prefix_map(或增加其计数)
python复制def subarraySum(nums, k):
count = 0
prefix_map = {0: 1}
current_sum = 0
for num in nums:
current_sum += num
if (current_sum - k) in prefix_map:
count += prefix_map[current_sum - k]
prefix_map[current_sum] = prefix_map.get(current_sum, 0) + 1
return count
4.3 复杂度分析
这种方法只需要一次遍历数组,时间复杂度降到了O(n)。空间复杂度也是O(n),因为最坏情况下需要存储n个不同的前缀和。
5. 边界条件与特殊案例
5.1 处理负数的情况
这道题的一个巧妙之处在于它允许数组包含负数。这使得问题更加复杂,因为:
- 滑动窗口方法不再适用(因为窗口和不再单调)
- 前缀和可能会出现重复值(因为有正有负,和可能先增后减)
这也是为什么哈希表方法如此适合本题的原因——它能高效处理这种情况。
5.2 空子数组的处理
根据题意,空子数组(长度为0)的和为0。如果k=0,我们需要特别注意是否要计入空子数组。在LeetCode的测试用例中,通常不考虑空子数组,所以我们的初始化prefix_map = {0:1}是正确的。
6. 实际面试中的变体问题
在面试中,面试官可能会基于这道题提出一些变体问题,例如:
- 不仅要求数量,还要求返回所有满足条件的子数组的起始和结束索引
- 限制子数组的长度在一定范围内
- 处理二维数组的情况(这时需要二维前缀和)
对于第一个变体,我们可以修改哈希表的值,使其不仅记录出现次数,还记录对应的索引列表:
python复制def subarraySumWithIndices(nums, k):
result = []
prefix_map = {0: [-1]}
current_sum = 0
for i, num in enumerate(nums):
current_sum += num
if (current_sum - k) in prefix_map:
for start in prefix_map[current_sum - k]:
result.append((start + 1, i))
if current_sum not in prefix_map:
prefix_map[current_sum] = []
prefix_map[current_sum].append(i)
return result
7. 同类问题延伸
前缀和+哈希表的组合在解决子数组和问题时非常强大。类似的问题还有:
-
- 连续数组(找到含有相同数量0和1的最长子数组)
-
- 和可被K整除的子数组
-
- 和等于k的最长子数组长度
以974题为例,它要求子数组和能被K整除。解法非常类似,只是将条件改为(prefix[j+1] - prefix[i]) % K == 0,可以转化为prefix[i] ≡ prefix[j+1] (mod K)。
8. 个人刷题心得
在刷这道题的过程中,我总结了几个关键点:
- 遇到子数组和的问题,前缀和是第一个应该想到的工具
- 当问题转化为寻找两个数的差时,哈希表通常是优化时间复杂度的关键
- 初始条件prefix_map = {0:1}很容易被忽略,但非常重要
- 对于包含负数的情况,传统的滑动窗口方法会失效
在实际编码时,还需要注意:
- Python中字典的get方法比直接访问更安全
- 更新计数的操作应该放在检查之后,避免将当前元素自身计入
- 对于大型测试用例,使用defaultdict(int)可能比普通字典更简洁
这道题的价值不仅在于它本身,更在于它教会我们如何将一个问题转化为数学表达式,然后利用合适的数据结构进行优化。这种思维方式在解决其他算法问题时也同样适用。
