📖 第4课:和为K的子数组

3 阅读19分钟

想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。

📖 第4课:和为K的子数组

模块:哈希表 | 难度:Medium ⭐⭐⭐ LeetCode 链接:leetcode.cn/problems/su… 前置知识:第1课(两数之和)、第3课(最长连续序列) 预计学习时间:30分钟


🎯 题目描述

给你一个整数数组和一个整数 k,请你统计并返回该数组中和为 k 的连续子数组的个数。

注意:子数组是数组中连续的非空序列

示例:

输入:nums = [1, 1, 1], k = 2
输出:2
解释:有2个和为2的子数组:[1,1][1,1](下标不同)
输入:nums = [1, 2, 3], k = 3
输出:2
解释:有2个和为3的子数组:[1,2][3]

约束条件:

  • 1 <= nums.length <= 2 * 10^4(数组最多2万个元素)
  • -1000 <= nums[i] <= 1000(元素可以为负数、零或正数)
  • -10^7 <= k <= 10^7

🧪 边界用例(面试必考)

用例类型输入期望输出考察点
单元素匹配nums=[3], k=31单个元素本身就是答案
单元素不匹配nums=[1], k=20无符合条件的子数组
含负数nums=[1,-1,0], k=03负数使得多种组合可能:[-1,1]、[0]、[1,-1,0]
全为零nums=[0,0,0], k=06n个0有 n*(n+1)/2 个子数组,全为0
多个解nums=[1,1,1], k=22[1,1]有两个(下标不同)
无解nums=[1,2,3], k=100所有子数组和都小于k
大规模n=2*10^4暴力法 O(n²) 约4亿次操作,会超时

💡 思路引导

生活化比喻

想象你在记录每天的收支流水账,现在要找"哪些时间段的总收支正好是 1000 元"。

🐌 笨办法:枚举所有可能的时间段(从第 i 天到第 j 天),每段都从头到尾加一遍算总和,看是否等于 1000。这样做的话,有多少个时间段?大约 n²/2 个!每段还要加 O(n) 次,总共 O(n³) ——太慢了!

🤔 稍聪明一点:用"前缀和"优化计算。先算出"从第 1 天到第 i 天的累计总和",这样任意时间段 [i, j] 的和就是 前缀和[j] - 前缀和[i-1],查询变成 O(1)。但还是要枚举 n²/2 个时间段,总体 O(n²)。

🚀 最聪明办法:一边算前缀和,一边用"哈希表记住之前见过的前缀和"。当你算到第 j 天的前缀和时,只需要在哈希表里查:"之前有没有出现过 当前前缀和 - 1000?"如果有,说明存在某个时间段 [i+1, j] 的和正好是 1000!而且,如果这个前缀和出现过 3 次,说明有 3 个不同的起点都能到达当前位置且和为 k。这样只需要遍历一次,O(n) 搞定!

关键魔法:"我要找和为 k 的区间"转化为"我要找之前出现过的某个特定前缀和"——这就是哈希表的用武之地!

关键洞察

子数组和问题的本质:区间和 = 两个前缀和的差。用哈希表记录"每个前缀和出现的次数",遍历时查找 当前前缀和 - k 是否出现过,出现几次就有几个符合条件的子数组。


🧠 解题思维链

这一节模拟你在面试中"从零开始思考"的过程。

Step 1:理解题目 → 锁定输入输出

  • 输入:一个整数数组 nums(长度 1~2万),一个整数 k
  • 输出:一个整数,表示和为 k 的连续子数组的个数(不是子数组本身!)
  • 限制:元素可以为负数/零/正数,k 也可以是任意整数

Step 2:先想笨办法(暴力枚举)

最直接的想法——枚举所有连续子数组,计算每个的和

  • 外层循环:起点 i
  • 内层循环:终点 j (j >= i)
  • 每次计算 sum(nums[i:j+1]) 是否等于 k
  • 时间复杂度:O(n²) ~ O(n³)(取决于求和方式)
  • 瓶颈在哪:重复计算。比如 [1,2,3],算 [1,2] 的和是 3,算 [1,2,3] 时又从头加一遍,浪费!

Step 3:瓶颈分析 → 优化方向

暴力法的问题:每次求子数组和都要重新累加。能不能 O(1) 求出任意区间和?

  • 核心问题:"区间 [i, j] 的和"需要重复计算
  • 优化思路:用前缀和(prefix sum)预处理,区间和 = preSum[j] - preSum[i-1],查询变 O(1)
  • 但还是要枚举 n²/2 个区间,能不能更快?

Step 4:数学转换 + 哈希表

关键变换:如果 preSum[j] - preSum[i] = k,那么 preSum[i] = preSum[j] - k

这意味着:当我们遍历到位置 j,计算出前缀和 preSum[j] 时,只需要查找之前是否出现过前缀和 preSum[j] - k——如果出现过,就说明存在某个区间 [i+1, j] 的和为 k!

而且,如果 preSum[j] - k 出现过 count 次,说明有 count 个不同的起点都能满足条件!

  • 选用:哈希表 {前缀和: 出现次数}
  • 理由:O(1) 查找某个前缀和出现过几次,一次遍历就能统计所有符合条件的子数组

🔑 模式识别提示:当题目出现"子数组和等于某个值"、"区间和查询",优先考虑"前缀和 + 哈希表"模式


🔑 解法一:暴力枚举(超时但帮助理解)

思路

枚举所有可能的子数组 [i, j],对每个子数组计算和,检查是否等于 k。为了避免 O(n³),在内层循环累加求和,边扫边加,时间降到 O(n²)。

图解过程

示例:nums = [1, 2, 3], k = 3

枚举所有子数组:
  i=0: [1]→sum=1 ❌  [1,2]→sum=3 ✅  [1,2,3]→sum=6i=1: [2]→sum=2 ❌  [2,3]→sum=5i=2: [3]→sum=3 ✅

  找到2个:count = 2

Step by Step:
  i=0, j=0: sum=1, 13
  i=0, j=1: sum=1+2=3, 3==3 ✅ count=1
  i=0, j=2: sum=3+3=6, 63
  i=1, j=1: sum=2, 23
  i=1, j=2: sum=2+3=5, 53
  i=2, j=2: sum=3, 3==3 ✅ count=2

  结果:2 ✅
示例:nums = [1, 1, 1], k = 2

  i=0: [1]→1 ❌  [1,1]→2 ✅  [1,1,1]→3i=1: [1]→1 ❌  [1,1]→2i=2: [1]→1 ❌

  找到2个:count = 2

Python代码

from typing import List


def subarray_sum_brute(nums: List[int], k: int) -> int:
    """
    解法一:暴力枚举
    思路:双层循环枚举所有子数组,边扫边加计算和
    """
    n = len(nums)
    count = 0

    for i in range(n):                    # 枚举起点
        current_sum = 0                   # 当前子数组和(从 i 开始)
        for j in range(i, n):             # 枚举终点
            current_sum += nums[j]        # 边扫边加,避免重复计算
            if current_sum == k:          # 检查是否等于 k
                count += 1

    return count


# ✅ 测试
print(subarray_sum_brute([1, 1, 1], 2))       # 期望输出:2
print(subarray_sum_brute([1, 2, 3], 3))       # 期望输出:2
print(subarray_sum_brute([1], 0))             # 期望输出:0
print(subarray_sum_brute([1, -1, 0], 0))      # 期望输出:3

复杂度分析

  • 时间复杂度:O(n²) — 两层循环,每个子数组访问一次
    • 具体地说:如果 n=2×10^4,需要约 (2×10^4)² / 2 ≈ 2×10^8 次操作,会超时!
  • 空间复杂度:O(1) — 只用了几个变量

优缺点

  • ✅ 思路直观,容易理解和实现
  • ✅ 不需要额外空间
  • ❌ 时间复杂度 O(n²),大数据会超时——能不能优化到 O(n)?

⚡ 解法二:前缀和 + 哈希表(最优解 O(n))

优化思路

暴力法的痛点是枚举所有 O(n²) 个子数组。我们用数学变换:如果 preSum[j] - preSum[i] = k,那么 preSum[i] = preSum[j] - k

这样,遍历到位置 j 时,只需要在哈希表里查"之前是否出现过 preSum[j] - k",如果出现过 count 次,就有 count 个子数组以 j 结尾且和为 k。

💡 关键想法:一边计算前缀和,一边记录每个前缀和出现的次数。遇到新的前缀和时,查找 当前前缀和 - k 是否出现过,出现几次就贡献几个子数组。

图解过程

示例:nums = [1, 2, 3], k = 3

初始化:prefix_count = {0: 1}  (关键!前缀和为0出现过1次,对应空子数组)
        preSum = 0, count = 0

  ╔════════════════════════════════════════════════════════════╗
  ║ i=0, num=1                                                ║
  ║   preSum = 0 + 1 = 1                                      ║
  ║   查找: preSum - k = 1 - 3 = -2 在 prefix_count 里吗?     ║
  ║         prefix_count = {0:1} → ❌ 不在                     ║
  ║   count = 0 (未增加)                                      ║
  ║   记录: prefix_count = {0:1, 1:1}                         ║
  ╠════════════════════════════════════════════════════════════╣
  ║ i=1, num=2                                                ║
  ║   preSum = 1 + 2 = 3                                      ║
  ║   查找: preSum - k = 3 - 3 = 0 在 prefix_count 里吗?      ║
  ║         prefix_count = {0:1, 1:1} → ✅ 在!出现过1次        ║
  ║   count = 0 + 1 = 1  (找到子数组 [1,2])                  ║
  ║   记录: prefix_count = {0:1, 1:1, 3:1}                    ║
  ╠════════════════════════════════════════════════════════════╣
  ║ i=2, num=3                                                ║
  ║   preSum = 3 + 3 = 6                                      ║
  ║   查找: preSum - k = 6 - 3 = 3 在 prefix_count 里吗?      ║
  ║         prefix_count = {0:1, 1:1, 3:1} → ✅ 在!出现过1次   ║
  ║   count = 1 + 1 = 2  (找到子数组 [3])                    ║
  ║   记录: prefix_count = {0:1, 1:1, 3:1, 6:1}               ║
  ╚════════════════════════════════════════════════════════════╝

  最终答案:count = 2 ✅

  解释:
  - i=1时找到的子数组 [1,2]:前缀和3 - 前缀和0(空) = 3 ✅
  - i=2时找到的子数组 [3]:前缀和6 - 前缀和3([1,2]) = 3
示例:nums = [1, 1, 1], k = 2

初始化:prefix_count = {0: 1}, preSum = 0, count = 0

  ╔════════════════════════════════════════════════════════════╗
  ║ i=0, num=1                                                ║
  ║   preSum = 0 + 1 = 1                                      ║
  ║   查找: 1 - 2 = -1 → ❌ 不在                              ║
  ║   count = 0                                               ║
  ║   prefix_count = {0:1, 1:1}                               ║
  ╠════════════════════════════════════════════════════════════╣
  ║ i=1, num=1                                                ║
  ║   preSum = 1 + 1 = 2                                      ║
  ║   查找: 2 - 2 = 0 → ✅ 在!出现过1次                        ║
  ║   count = 0 + 1 = 1  (找到 [1,1])                        ║
  ║   prefix_count = {0:1, 1:1, 2:1}                          ║
  ╠════════════════════════════════════════════════════════════╣
  ║ i=2, num=1                                                ║
  ║   preSum = 2 + 1 = 3                                      ║
  ║   查找: 3 - 2 = 1 → ✅ 在!出现过1次                        ║
  ║   count = 1 + 1 = 2  (找到 [1,1],下标1-2)                ║
  ║   prefix_count = {0:1, 1:1, 2:1, 3:1}                     ║
  ╚════════════════════════════════════════════════════════════╝

  最终答案:count = 2
边界用例:nums = [1, -1, 0], k = 0

初始化:prefix_count = {0: 1}, preSum = 0, count = 0

  ╔════════════════════════════════════════════════════════════╗
  ║ i=0, num=1                                                ║
  ║   preSum = 0 + 1 = 1                                      ║
  ║   查找: 1 - 0 = 1 → ❌ 不在                               ║
  ║   count = 0                                               ║
  ║   prefix_count = {0:1, 1:1}                               ║
  ╠════════════════════════════════════════════════════════════╣
  ║ i=1, num=-1                                               ║
  ║   preSum = 1 + (-1) = 0                                   ║
  ║   查找: 0 - 0 = 0 → ✅ 在!出现过1次                        ║
  ║   count = 0 + 1 = 1  (找到 [1,-1])                       ║
  ║   prefix_count = {0:2, 1:1}  (注意0出现了2次!)            ║
  ╠════════════════════════════════════════════════════════════╣
  ║ i=2, num=0                                                ║
  ║   preSum = 0 + 0 = 0                                      ║
  ║   查找: 0 - 0 = 0 → ✅ 在!出现过2次                        ║
  ║   count = 1 + 2 = 3  (找到 [0] 和 [1,-1,0] 两个!)        ║
  ║   prefix_count = {0:3, 1:1}                               ║
  ╚════════════════════════════════════════════════════════════╝

  最终答案:count = 3 ✅
  三个子数组:[1,-1][0][1,-1,0]

Python代码

from typing import List


def subarray_sum_hash(nums: List[int], k: int) -> int:
    """
    解法二:前缀和 + 哈希表(最优解)
    思路:记录每个前缀和出现的次数,查找 preSum - k 是否出现过
    """
    # 哈希表:{前缀和: 出现次数}
    prefix_count = {0: 1}              # ⚠️ 初始化:前缀和0出现过1次(对应空子数组)
    presum = 0                         # 当前前缀和
    count = 0                          # 符合条件的子数组个数

    for num in nums:
        presum += num                  # 更新前缀和

        # 关键步骤:查找 preSum - k 是否出现过
        # 如果出现过,说明存在子数组 [i+1, j] 的和为 k
        if presum - k in prefix_count:
            count += prefix_count[presum - k]  # 出现几次就贡献几个子数组

        # 记录当前前缀和出现的次数
        prefix_count[presum] = prefix_count.get(presum, 0) + 1

    return count


# ✅ 测试
print(subarray_sum_hash([1, 1, 1], 2))       # 期望输出:2
print(subarray_sum_hash([1, 2, 3], 3))       # 期望输出:2
print(subarray_sum_hash([1], 0))             # 期望输出:0
print(subarray_sum_hash([1, -1, 0], 0))      # 期望输出:3  (含负数)
print(subarray_sum_hash([0, 0, 0], 0))       # 期望输出:6  (全为0)
print(subarray_sum_hash([3], 3))             # 期望输出:1  (单元素)

复杂度分析

  • 时间复杂度:O(n) — 只遍历一次数组,每次哈希表查找和插入都是 O(1)
    • 具体地说:如果 n=2×10^4,只需要约 2×10^4 次操作,比暴力法快 1万倍!
  • 空间复杂度:O(n) — 哈希表最多存储 n+1 个不同的前缀和
    • 典型的"空间换时间"策略

🐍 Pythonic 写法

利用 defaultdict 简化代码:

from collections import defaultdict
from typing import List


def subarray_sum_pythonic(nums: List[int], k: int) -> int:
    """
    Pythonic 简洁版:用 defaultdict 自动处理默认值
    """
    prefix_count = defaultdict(int)
    prefix_count[0] = 1               # 初始化
    presum = count = 0

    for num in nums:
        presum += num
        count += prefix_count[presum - k]  # 查找并累加
        prefix_count[presum] += 1          # 记录

    return count

这个写法用 defaultdict(int) 自动处理不存在的键(默认返回 0),代码更简洁。

⚠️ 面试建议:先写清晰版本(解法二)展示思路,如果面试官问"能不能更简洁",再提 Pythonic 写法。面试官更看重你的数学推导和边界处理,而非代码行数。


📊 解法对比

维度解法一:暴力枚举解法二:前缀和+哈希
时间复杂度O(n²)O(n) ⭐
空间复杂度O(1)O(n)
代码难度简单中等(需要理解前缀和变换)
面试推荐⭐⭐⭐
适用场景小数据集(n<1000)大数据集,题目要求O(n)(首选!)

面试建议:先讲暴力法展示基本思路,然后指出"O(n²)会超时",再引出"前缀和+哈希表"优化。关键是要清晰推导:preSum[j] - preSum[i] = kpreSum[i] = preSum[j] - k,这样展示了数学建模能力!


🎤 面试现场

模拟面试中的完整对话流程,帮你练习"边想边说"。

面试官:请你解决一下这道题,要求尽可能优化。

:(审题30秒)好的,这道题要求统计和为 k 的连续子数组个数。我的第一个想法是枚举所有子数组,用双层循环,时间复杂度 O(n²)。但数据范围是 2万,O(n²) 约 4 亿次操作,可能会超时。

让我想想能不能优化...子数组和问题我想到了前缀和。如果记 preSum[i] 为前 i 个元素的和,那么区间 [i, j] 的和就是 preSum[j] - preSum[i-1]。我要找 preSum[j] - preSum[i-1] = k,可以变换为 preSum[i-1] = preSum[j] - k

这样,我遍历到位置 j 时,只需要查找"之前是否出现过前缀和 preSum[j] - k",如果出现过,就说明存在符合条件的子数组!用哈希表记录每个前缀和出现的次数,这样查找是 O(1),总体 O(n)。

面试官:很好,为什么要记录"出现次数"而不只是记录"是否出现"?

:因为同一个前缀和可能出现多次!比如 [1, -1, 1],前缀和分别是 [1, 0, 1],1 出现了 2 次。如果 k=0,当我到达位置 2(前缀和=1)时,查找 1-0=1 发现出现过 2 次(位置 0 和位置 2 本身之前就有 1 次),但实际上只有 1 个子数组 [1, -1] 满足...等等,我说错了。让我重新理清楚...(修正)应该是:当遍历到位置 i,前缀和是 preSum[i] 时,我查找之前是否出现过 preSum[i] - k。如果出现过 count 次,说明有 count 个不同的位置 j,使得 preSum[i] - preSum[j] = k,也就是有 count 个子数组 [j+1, i] 的和为 k。

面试官:对,思路清晰了。还有一个细节:prefix_count 为什么要初始化为 {0: 1}?

:这是一个关键的边界处理!{0: 1} 表示"前缀和为 0 出现过 1 次",对应空子数组(前 0 个元素的和是 0)。这样,如果某个位置 i 的前缀和正好等于 k,那么 preSum[i] - k = 0 能在哈希表里找到,表示子数组 [0, i] 的和为 k。如果不初始化,这种情况会漏掉。

面试官:完美!请写一下代码并测试。

:(写代码)...(测试)用 [1, 2, 3], k=3 测试...结果是 2,正确。再测试 [1, -1, 0], k=0(含负数)...结果是 3,也对。

高频追问

追问应答策略
"能不能不用哈希表,空间O(1)?"不行。如果不记录前缀和,每次查找 preSum - k 需要遍历之前的元素,退化为 O(n²)。哈希表是用 O(n) 空间换 O(n) 时间的关键
"如果要返回所有符合条件的子数组本身?"需要在哈希表中存储 {前缀和: [下标列表]},找到匹配时记录区间 [j+1, i]。空间复杂度仍是 O(n),但常数更大
"如果数组很大,内存放不下?"可以分块处理,但这道题必须记录所有可能的前缀和,很难优化空间。或者用双指针,但仅适用于全正数的情况(本题有负数不适用)
"为什么不用前缀和数组,而是用一个变量?"因为我们只需要"当前前缀和"和"之前出现过的前缀和",不需要存储完整的前缀和数组。用一个变量 presum 边遍历边更新,节省空间

🎓 知识点总结

Python技巧卡片 🐍

# 技巧1:字典的 get 方法设置默认值
count = prefix_count.get(presum, 0) + 1  # 如果键不存在,返回 0

# 技巧2:defaultdict 自动处理默认值
from collections import defaultdict
prefix_count = defaultdict(int)  # 访问不存在的键自动返回 0
prefix_count[10] += 1            # 无需判断键是否存在

# 技巧3:一行完成查找和累加
count += prefix_count.get(presum - k, 0)  # 不存在返回0,存在返回次数

💡 底层原理(选读)

什么是前缀和?为什么它这么有用?

前缀和(Prefix Sum)是一种预处理技巧,通过预先计算"前 i 个元素的累积和",使得任意区间和查询变成 O(1):

  • preSum[i] = nums[0] + nums[1] + ... + nums[i-1]
  • 区间 [i, j] 的和 = preSum[j+1] - preSum[i]

为什么区间和 = 两个前缀和的差?

nums = [1, 2, 3, 4]
preSum = [0, 1, 3, 6, 10]  (preSum[0]=0 表示前0个元素和)

区间 [1, 2] 的和 = nums[1] + nums[2] = 2 + 3 = 5
             = preSum[3] - preSum[1] = 6 - 1 = 5 ✅

本质:preSum[3] 包含了 [0,1,2],preSum[1] 包含了 [0]
     两者相减正好剩下 [1,2]

前缀和 + 哈希表的威力:

  • 前缀和:把"区间和"问题转化为"两个点的差"
  • 哈希表:把"枚举所有点对"的 O(n²) 优化为"查找配对"的 O(n)
  • 结合起来:O(1) 查询区间和 + O(1) 查找配对 = O(n) 总体!

这个组合在很多题目中都有用:子数组和等于 k和为 k 的最长子数组连续子数组和为 k 的倍数等。

算法模式卡片 📐

  • 模式名称:前缀和 + 哈希表
  • 适用条件:子数组和问题,需要快速查询"某个区间和是否等于某值"
  • 识别关键词:"子数组和等于 k"、"连续子数组"、"区间和"、"累积和"
  • 核心变换:区间和 = preSum[j] - preSum[i] → 转化为"查找之前是否出现过某个前缀和"
  • 模板代码:
def subarray_sum_pattern(nums, target):
    prefix_count = {0: 1}  # 初始化:前缀和0出现过1次
    presum = count = 0
    for num in nums:
        presum += num
        # 查找 preSum - target 是否出现过
        if presum - target in prefix_count:
            count += prefix_count[presum - target]
        # 记录当前前缀和
        prefix_count[presum] = prefix_count.get(presum, 0) + 1
    return count

易错点 ⚠️

  1. 忘记初始化 prefix_count[0] = 1: 这会导致"前缀和正好等于 k"的子数组被漏掉。必须初始化!
  2. 先记录再查找: 如果写成 prefix_count[presum] += 1 在前,count += ... 在后,会把自己和自己配对(子数组长度为 0),错误!必须先查找再记录
  3. 混淆"个数"和"是否存在": 哈希表存的是出现次数,不是布尔值。因为同一个前缀和可能出现多次,每次都贡献一个子数组
  4. 边界用例: 记得测试负数、全为零、单元素等特殊情况
  5. 前缀和可能很大: 由于元素范围 [-1000, 1000],前缀和范围约 [-2×10^7, 2×10^7],Python 的 dict 可以处理任意大小的键,不用担心溢出

🏗️ 工程实战(选读)

这个算法思想在真实项目中的应用,让你知道"学了有什么用"。

  • 场景1:金融数据分析 - 股票交易中,找"累计收益率等于某个目标的时间段"。把每日收益率作为数组元素,用前缀和+哈希表快速定位符合条件的交易周期。
  • 场景2:用户行为分析 - 在用户活跃度数据中,找"连续 N 天活跃度总和等于某个阈值的时间窗口",用于精准营销或异常检测。
  • 场景3:服务器负载均衡 - 分析请求日志,找"请求数总和等于某个容量"的时间段,优化服务器资源调度。

🏋️ 举一反三

完成本课后,试试这些同类题目来巩固知识:

题目难度相关知识点提示
LeetCode 325. 和等于 k 的最长子数组长度Medium前缀和+哈希表本题变体:不是统计个数,而是找最长的子数组。哈希表存 {前缀和: 第一次出现的下标},更新最大长度
LeetCode 974. 和可被 K 整除的子数组Medium前缀和+哈希表+同余变换:(preSum[j] - preSum[i]) % k == 0preSum[j] % k == preSum[i] % k,记录余数出现次数
LeetCode 523. 连续的子数组和Medium前缀和+哈希表+同余类似 974,但要求子数组长度 >= 2,需要额外判断
LeetCode 1248. 统计优美子数组Medium前缀和+哈希表把"奇数个数等于 k"转化为前缀和问题:奇数个数的前缀和

📝 课后小测

试试这道变体题,不要看答案,自己先想5分钟!

题目:给定一个整数数组和一个整数 k,找出和等于 k 的最长连续子数组的长度。如果不存在返回 0。

例如:nums = [1, -1, 5, -2, 3], k = 3,最长子数组是 [1, -1, 5, -2, 3][-2, 3, ...],长度是...等等,让你自己算!

💡 提示(实在想不出来再点开)

仍然用前缀和+哈希表,但这次哈希表存 {前缀和: 第一次出现的下标}。当找到 preSum - k 时,计算当前下标与第一次出现下标的距离,更新最大长度。

关键:要存"第一次出现的下标",因为我们要最长的子数组,所以起点越早越好!

✅ 参考答案
def max_subarray_length(nums: List[int], k: int) -> int:
    """和等于 k 的最长子数组长度"""
    prefix_index = {0: -1}  # {前缀和: 第一次出现的下标},-1表示空子数组
    presum = 0
    max_len = 0

    for i, num in enumerate(nums):
        presum += num

        # 查找 preSum - k 是否出现过
        if presum - k in prefix_index:
            # 计算长度:当前下标 - 第一次出现的下标
            max_len = max(max_len, i - prefix_index[presum - k])

        # 只记录第一次出现的下标(保证最长)
        if presum not in prefix_index:
            prefix_index[presum] = i

    return max_len

# 测试
print(max_subarray_length([1, -1, 5, -2, 3], 3))  # → 5  ([1,-1,5,-2,3])
print(max_subarray_length([1, 2, 3], 3))          # → 2  ([1,2] 或 [3])

核心思路:哈希表存第一次出现的下标,因为要最长子数组,起点越早越好。找到匹配时,计算当前位置与第一次出现位置的距离,更新最大值。O(n) 时间,O(n) 空间!


如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。