前缀和
把一段区间,变成两个边界。
用 3 道题建立一条完整思考路径:从前缀定义出发,理解单调性、历史状态计数,再把思想迁移到前后缀乘积。每种解法都附有可独立提交的代码。
单文件 · 无外部依赖 · 可反复复习建议每题先读题意并尝试 10 分钟,再看推导;理解后关闭答案默写,最后用边界用例自测。题目掌握进度与文末能力自测分别保存。
当前浏览器不允许保存本地数据,本次操作仍有效;关闭页面后进度可能丢失。
先看学习路线
| 阶段 | 学习内容 | 要达到的目标 |
|---|---|---|
| 01 · 基础与模板 | 前缀和定义、左闭右开、静态区间查询 | 看到区间 [l,r) 能写出 P[r] − P[l] |
| 02 · 单调区间应用 | 209 · 长度最小的子数组 | 先用前缀和 + 二分,再理解线性滑动窗口 |
| 03 · 历史状态计数 | 560 · 和为 K 的子数组 | 从双重枚举推导出前缀和 + 频次哈希表 |
| 04 · 前后缀分解 | 238 · 除自身以外数组的乘积 | 从两个数组优化为复用输出数组的常数额外空间 |
选题边界:手册严格包含这 3 道题。238 研究的是前缀乘积与后缀乘积,归入前缀思想的扩展。209 的线性窗口解法依赖正整数;560 允许负数,不能照搬。
第一阶段 · 基础知识与核心模板
01 / FOUNDATION
1. 前缀和到底存什么?
设数组长度为 n。统一使用长度为 n + 1 的前缀数组 P:P[i] 表示前 i 个元素的和,它包含下标 0 到 i − 1,不包含 nums[i]。P[0] = 0 表示尚未取任何元素。
对于左闭右开区间 [l, r),P[r] 包含「左侧部分 + 目标区间」,P[l] 恰好是「左侧部分」。相减便留下目标区间。区间长度也是 r − l。
| 表达方式 | 区间元素 | 求和公式 | 长度 |
|---|---|---|---|
| 左闭右开 [l, r) | nums[l] … nums[r − 1] | P[r] − P[l] | r − l |
| 左右都闭 [l, r] | nums[l] … nums[r] | P[r + 1] − P[l] | r − l + 1 |
| 空区间 [l, l) | 没有元素 | P[l] − P[l] = 0 | 0 |
动手验证 · 两个边界决定一段和
固定数组 [2, 3, 1, 2, 4, 3];切换左闭右开边界。若 l > r,会将 r 自动对齐到 l。
2. 把模板理解成状态约定
| 模式 | 状态含义 | 更新顺序 / 核心式 | 对应场景 |
|---|---|---|---|
| 静态区间和 | P[i] = 前 i 个数的和 | 初始化 P[0]=0 → 逐个累加 → P[r]−P[l] | 大量区间求和;209 的二分版本 |
| 前缀和 + 频次表 | freq[s] = 已扫描的前缀中和值 s 的数量 | freq={0:1} → 更新 running → 查询 running−k → 记录 running | 560:连续子数组的计数 |
| 正数窗口 | total = nums[left:right+1] 的和 | 右端加入 → while 达标时记录 → 左端移出 | 209:最短达标区间 |
| 前后缀分解 | left[i] 不含 i;right[i] 不含 i | answer[i] = left[i] × right[i] | 238:排除当前位置的聚合 |
一次构建前缀数组花费 O(n) 时间和 O(n) 空间,之后每次区间查询只需 O(1)。若 q 次查询都直接对切片求和,最坏需要 O(nq);前缀和将其降到 O(n + q)。数组发生修改后,原前缀数组不再有效,需要相应重建或改用支持更新的数据结构。
三个易混点:前缀数组有 n + 1 项;Python 的 nums[l:r] 与 [l,r) 一致,但切片会分配新列表;在每次查询里执行 sum(nums[l:r]) 不会得到 O(1) 时间。
自测:为什么 P[0] 必须存在?
它统一了从下标 0 开始的区间。例如 [0,3) 的和为 P[3]−P[0]。在 560 中,空前缀还能代表「子数组从数组开头开始」;它是合法左边界,但答案中的子数组仍必须非空。
第二阶段 · 正数区间与单调性
02 / MONOTONICITY
先做 209,把「区间和」与「单调性」联系起来:正数使前缀和严格递增,因此可以二分,也可以让左右边界只向前移动。
01LEETCODE 209 / MEDIUM长度最小的子数组
题意与约束
给定一个正整数 target 和正整数数组 nums,返回连续子数组中总和至少为 target 的最短长度。如果所有区间都无法达标,返回 0。注意是「大于等于」而非恰好等于。
原题约束:1 ≤ n ≤ 10⁵;1 ≤ nums[i] ≤ 10⁴;1 ≤ target ≤ 10⁹。正数条件是下面两种高效解法的前提。题意及约束见卡片顶部原题链接。
| 输入 | 输出 | 解释 |
|---|---|---|
| target=7,nums=[2,3,1,2,4,3] | 2 | [4,3] 达标,没有单个元素达到 7 |
| target=4,nums=[1,4,4] | 1 | 任选一个 [4] |
| target=11,nums=[1,1,1,1,1,1,1,1] | 0 | 总和 8,无法达标 |
解法一 · 前缀和 + 二分查找
暴力做法枚举左右边界,维护区间累加和,需要 O(n²)。先用前缀和把每个区间的计算变为相减,再利用正数形成的有序性,便能避免枚举全部右边界。
固定左边界 left,寻找最小的右边界 right,使 P[right] − P[left] ≥ target,等价于在 P 中查找第一个不小于 P[left] + target 的位置。因为 nums 中每一项为正,P 严格递增,bisect_left 可以完成这个 lower_bound 查询。
from bisect import bisect_left
from typing import List
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
n = len(nums)
prefix = [0] * (n + 1)
for i, x in enumerate(nums):
prefix[i + 1] = prefix[i] + x
best = n + 1
for left in range(n):
need = prefix[left] + target
right = bisect_left(prefix, need, left + 1)
if right <= n:
best = min(best, right - left)
return 0 if best == n + 1 else best
关键代码解释
prefix[i + 1]把 nums[i] 纳入前缀,避免遗漏第一个元素。bisect_left(prefix, need, left + 1)从 left 后的边界开始查找,保证子数组非空。它返回的是前缀数组下标。- prefix 长度为 n + 1:查找失败会返回 n + 1;right = n 仍是合法边界,代表区间延伸至末尾。
best = n + 1表示尚无解,最后统一转成 0。
示例推演 · 前缀数组 [0,2,5,6,8,12,15]
| left | P[left] + 7 | 首个合格 right | 区间 [left,right) | 长度 |
|---|---|---|---|---|
| 0 | 7 | 4(P[4]=8) | [2,3,1,2] | 4 |
| 1 | 9 | 5(P[5]=12) | [3,1,2,4] | 4 |
| 2 | 12 | 5(P[5]=12) | [1,2,4] | 3 |
| 3 | 13 | 6(P[6]=15) | [2,4,3] | 3 |
| 4 | 15 | 6(P[6]=15) | [4,3] | 2 |
| 5 | 19 | 7(查找失败) | 不存在 | 不更新 |
正确性:对于固定左端点,二分找到的首个合格右边界给出该左端点下的最短区间。对所有左端点取最小值,就是全局最短长度。
解法二 · 滑动窗口(面试首选)
正数还保证了窗口的单调性:右端加入元素,总和变大;左端移出元素,总和变小。右端每走一步,只要当前窗口已达标,就先记录答案,再持续缩短左端,直到再次不达标。
循环使用闭区间 [left,right]。虽然与前缀数组的 [l,r) 写法不同,但含义是明确的:窗口长度为 right − left + 1,移出元素前先更新答案。
from typing import List
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
n = len(nums)
left = 0
total = 0
best = n + 1
for right, x in enumerate(nums):
total += x
while total >= target:
best = min(best, right - left + 1)
total -= nums[left]
left += 1
return 0 if best == n + 1 else best
关键代码解释
while total >= target保证一次右移后能尝试所有仍达标的更短窗口,不能换成单次 if。total -= nums[left]必须在 left 自增之前,移走的才是窗口旧左端点。- total 始终维护当前窗口的和,无需重复求和或创建切片。
示例推演 · target = 7
| right / 加入 | 加入后的 total | while 内记录与收缩 | 结束时 left / total / best |
|---|---|---|---|
| 0 / 2 | 2 | 未达标 | 0 / 2 / 未找到 |
| 1 / 3 | 5 | 未达标 | 0 / 5 / 未找到 |
| 2 / 1 | 6 | 未达标 | 0 / 6 / 未找到 |
| 3 / 2 | 8 | [0,3] 长 4 → 移出 2 | 1 / 6 / 4 |
| 4 / 4 | 10 | [1,4] 长 4 → 7;[2,4] 长 3 → 6 | 3 / 6 / 3 |
| 5 / 3 | 9 | [3,5] 长 3 → 7;[4,5] 长 2 → 3 | 5 / 3 / 2 |
正确性与循环不变量:每次记录时 total 就是 [left,right] 的和。每次 while 结束,窗口已不达标。被移出的旧左端点在当前 right 下已经考察过;将来 right 更大时,沿用它只会得到更长的窗口,所以可以永久丢弃。所有可能改善答案的窗口都会被覆盖。
解法比较
| 方法 | 时间 / 辅助空间 | 优点 | 限制 / 使用建议 |
|---|---|---|---|
| 枚举左右端点 + 累加 | O(n²) / O(1) | 适合小数据核对答案 | n=10⁵ 时不适合提交 |
| 前缀和 + 二分 | O(n log n) / O(n) | 直接练习区间变换和 lower_bound | 要求前缀有序;掌握题目进阶要求 |
| 滑动窗口 | O(n) / O(1) | 最优,代码简洁 | 优先提交;必须解释正数保证的单调性 |
易错点与边界检查
- 判定条件是 ≥,示例 [4,3] 的和恰好为 7 也必须接受。
- 无解时返回 0,不能返回 n + 1 或负数。
- 单元素刚好达标:target=4,nums=[4] → 1;单元素不足:target=4,nums=[3] → 0。
- 整个数组才达标:target=6,nums=[1,2,3] → 3。
- 负数会破坏窗口推理。例如 target=3,nums=[1,-1,3]:上述窗口会停留在长度 3,漏掉 [3]。该输入不属于 209 的约束,不能把算法无条件推广。
- 二分查找右边界时,比较的是 P[left] + target,而非 target 本身。
面试追问:代码有两层循环,为什么仍然是 O(n)?
内层循环不在每次外层时重置 left。right 最多移动 n 次,left 也最多移动 n 次,窗口元素总共进入和离开各一次,因此总操作数为 O(n)。
第三阶段 · 前缀和与哈希计数
03 / PREFIX FREQUENCY
560 允许负数和零。区间和不再单调,于是将「寻找边界」转化为「查询历史状态出现了多少次」。
02LEETCODE 560 / MEDIUM和为 K 的子数组
题意与约束
给定整数数组 nums 和整数 k,统计有多少个连续、非空子数组的元素和等于 k。不同下标区间分别计数,即使它们的元素值相同。
原题约束:1 ≤ n ≤ 2 × 10⁴;−1000 ≤ nums[i] ≤ 1000;−10⁷ ≤ k ≤ 10⁷。允许负数、零和负目标值。题意及约束见卡片顶部原题链接。
| 输入 | 输出 | 满足条件的下标区间 |
|---|---|---|
| nums=[1,1,1],k=2 | 2 | [0,2) 与 [1,3) |
| nums=[1,2,3],k=3 | 2 | [0,2) 与 [2,3) |
解法一 · 枚举左端点,向右累加(建立基线)
固定 left 后,把右端点逐步向右移动,每次只添加一个新元素。当累加和恰好为 k 时计数。不要因为当前和超过 k 就 break,后面可能有负数把总和拉回来。
from typing import List
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
count = 0
n = len(nums)
for left in range(n):
total = 0
for right in range(left, n):
total += nums[right]
if total == k:
count += 1
return count
关键代码:total 在每次开始新的 left 时清零,内层循环增量维护 [left,right] 的和。对 [1,1,1]、k=2,left=0 时在 right=1 命中一次,left=1 时在 right=2 命中一次,left=2 无命中,最终为 2。
正确性:每个非空区间都由唯一的一对 left、right 决定,两层循环恰好枚举一次,没有遗漏或重复。
这份代码适合验证思路和小规模对拍;原题上限下可能超时。若每次改成 sum(nums[left:right+1]),最坏会退化到 O(n³)。
解法二 · 前缀数组 + 枚举边界(公式桥梁)
先构建 P,再枚举所有满足 0 ≤ left < right ≤ n 的前缀边界,检查 P[right] − P[left] 是否为 k。这个版本没有减少区间数量,但清楚展示了从「子数组」到「前缀对」的映射,为下一步优化做准备。
from typing import List
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
n = len(nums)
prefix = [0] * (n + 1)
for i, x in enumerate(nums):
prefix[i + 1] = prefix[i] + x
count = 0
for right in range(1, n + 1):
for left in range(right):
if prefix[right] - prefix[left] == k:
count += 1
return count
关键代码:range(right) 只枚举严格位于当前右边界之前的 left,排除空区间。[1,1,1] 的 P=[0,1,2,3],满足差为 2 的边界对为 (0,2)、(1,3),输出 2。
正确性:每个 [left,right) 与一对前缀边界一一对应,区间和公式直接给出判断条件。
解法三 · 前缀和 + 频次哈希表(推荐)
固定当前右边界 right,把等式移项:
现在只需知道:在当前边界之前,值为 P[right]−k 的前缀出现了多少次。每出现一次,就对应一个不同的合法左端点。用字典 freq 记录频次,让查询从扫描历史前缀变成平均 O(1)。
- 初始
freq = {0: 1},登记一次空前缀。 - 读入 x,更新当前前缀 running。
- 将历史
running − k的出现次数加入答案。 - 最后登记当前 running,供后面的右边界使用。
from typing import List
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
freq = {0: 1} # 空前缀,代表从数组开头开始的区间
running = 0
count = 0
for x in nums:
running += x
count += freq.get(running - k, 0)
freq[running] = freq.get(running, 0) + 1
return count
关键代码解释
freq.get(running - k, 0)查的是所需历史前缀值,不是查询 k,也不是 running 本身。- 查到的是频次,所以直接累加这个数量;不能只加 1,也不能使用只表示「存在」的集合。
- 必须先查历史再登记当前。否则 k=0 时会将当前前缀与自身配对,错误地计入空区间。
- 只维护 running,无需保存整个 P 数组;字典仍可能有 O(n) 个不同的前缀值。
示例推演 A · nums=[1,1,1],k=2
| 读入下标 / 值 | running | 查 running−k | 本次增加 | 登记后的 freq | 累计答案 |
|---|---|---|---|---|---|
| 开始 | 0 | — | — | {0:1} | 0 |
| 0 / 1 | 1 | −1 | 0 | {0:1, 1:1} | 0 |
| 1 / 1 | 2 | 0 | 1 | {0:1, 1:1, 2:1} | 1 |
| 2 / 1 | 3 | 1 | 1 | {0:1, 1:1, 2:1, 3:1} | 2 |
示例推演 B · nums=[1,-1,0],k=0
前缀边界的和值依次为 [0,1,0,0]。最后一个 0 到来之前,历史已经有两个和值为 0 的前缀,当前一步就应增加 2 个答案。
| 读入 | running | 查询时 freq[0] | 本次增加 | 登记后 freq | 累计答案 |
|---|---|---|---|---|---|
| 1 | 1 | 1(本次查的是 1) | 0 | {0:1, 1:1} | 0 |
| −1 | 0 | 1 | 1 → [1,−1] | {0:2, 1:1} | 1 |
| 0 | 0 | 2 | 2 → [1,−1,0] 与 [0] | {0:3, 1:1} | 3 |
逐步演示 · 先查询,再登记
固定 nums=[1,-1,0],k=0。每步展示查询前的字典与登记后的字典。
为什么一定正确?
处理 P[right] 时,字典恰好记录 P[0] 到 P[right−1] 的频次,这是循环不变量。每个被查到的 P[left] 都满足 left < right 和差为 k,因此对应一个合法非空子数组;反过来,每个合法子数组都会在其右边界被查询到。按右边界分组,区间既不重复也不遗漏。登记当前前缀后,不变量对下一轮继续成立。
上述时间分析使用常规哈希表平均复杂度模型;不是哈希冲突极端情况下的最坏界。
解法比较
| 解法 | 时间 | 辅助空间 | 教学价值 / 提交建议 |
|---|---|---|---|
| 枚举 + 增量求和 | O(n²) | O(1) | 建立正确性基线;大数据可能超时 |
| 前缀数组 + 边界枚举 | O(n²) | O(n) | 推导前缀对;还没有消除二重枚举 |
| 前缀和 + 频次哈希表 | 平均 O(n) | O(n) | 推荐提交;用计数替代逐个匹配历史边界 |
易错点与边界检查
- 遗漏空前缀:nums=[3]、k=3 应输出 1。没有 {0:1} 就会漏掉它。
- 先插再查:nums=[0]、k=0 应输出 1。错误顺序会得到 2。
- 把频次当布尔值:nums=[0,0,0]、k=0 应输出 6,即 3×4/2;三个右边界各贡献 1、2、3。
- 遇到超出 k 就停止:nums=[3,-2]、k=1 应输出 1,扫描到 3 时不能提前结束。
- 把子数组当子序列:nums=[1,2,1]、k=2 只有 [2] 合法,不能跳过中间的 2 拼出 [1,1]。
- 负目标:nums=[-1,-1,1]、k=-1 → 3。Python int 可以容纳答案;换到固定宽度语言时应关注 n(n+1)/2 的上界。
面试追问:为什么不能直接用 209 的窗口?
因为负数使「右移增大、左移减小」不再成立,零还可能让多个不同左端点同时合法。560 统计全部区间,不是只找最短或最长。频次表保留了所有历史左边界的数量,不依赖数组符号。
第四阶段 · 从前缀和到前后缀乘积
04 / PREFIX × SUFFIX
前缀思想不只用于加法。238 将「排除当前位置」拆成互不相交的左右两部分,进一步练习状态含义与空间优化。
03LEETCODE 238 / MEDIUM除自身以外数组的乘积
题意与约束
返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 以外所有元素的乘积。要求不使用除法,并在 O(n) 时间内完成;进阶要求 O(1) 额外空间,输出数组不计入额外空间。
原题约束:2 ≤ n ≤ 10⁵;−30 ≤ nums[i] ≤ 30,允许零与负数。原题保证相关前缀、后缀乘积及答案在 32 位整数范围内。力扣当前标题为「除了自身以外数组的乘积」,与题单中的写法对应同一题。
| 输入 | 输出 | 解释 |
|---|---|---|
| nums=[1,2,3,4] | [24,12,8,6] | 例如 i=2:1×2×4=8 |
| nums=[-1,1,0,-3,3] | [0,0,9,0,0] | 只有排除唯一零时,剩余乘积非零 |
为什么不能套「总积 ÷ 当前元素」?题目禁止除法;nums[i]=0 时也无法这样计算。前后缀分解自然处理零和负数,不需要特判零的数量。
解法一 · 显式保存左右乘积
定义 left[i] 为 i 左侧所有元素的乘积,right[i] 为 i 右侧所有元素的乘积,二者都不包含 nums[i]。空集合的乘积为 1,所以 left[0]=1、right[n−1]=1。
from typing import List
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
left = [1] * n
right = [1] * n
for i in range(1, n):
left[i] = left[i - 1] * nums[i - 1]
for i in range(n - 2, -1, -1):
right[i] = right[i + 1] * nums[i + 1]
return [left[i] * right[i] for i in range(n)]
关键代码与示例推演
nums[i - 1] 和 nums[i + 1] 是「排除自己」的关键。计算 left 从左向右,计算 right 从右向左,保证依赖的状态已经就绪。对 nums=[1,2,3,4]:
| i | nums[i] | left[i]:不含 i | right[i]:不含 i | answer[i] |
|---|---|---|---|---|
| 0 | 1 | 1 | 2×3×4 = 24 | 24 |
| 1 | 2 | 1 | 3×4 = 12 | 12 |
| 2 | 3 | 1×2 = 2 | 4 | 8 |
| 3 | 4 | 1×2×3 = 6 | 1 | 6 |
正确性:递推式分别构造左右两侧的乘积;两段不相交且并集恰好是除 i 以外的所有位置,将它们相乘就是定义要求的答案。
解法二 · 输出数组存左积,单变量维护右积(推荐)
最终答案还未生成时,answer[i] 可以先存 i 左侧乘积。再从右向左扫描,用 suffix 保存 i 右侧乘积,直接乘进 answer[i],就不需要 right 数组。
正向循环开始处理 i 时,prefix 恰好是不含 i 的左积;反向循环开始处理 i 时,suffix 恰好是不含 i 的右积。两轮都必须先使用旧状态,再把 nums[i] 纳入累计。
from typing import List
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
answer = [1] * n
prefix = 1
for i in range(n):
answer[i] = prefix
prefix *= nums[i]
suffix = 1
for i in range(n - 1, -1, -1):
answer[i] *= suffix
suffix *= nums[i]
return answer
关键代码解释
answer[i] = prefix放在prefix *= nums[i]之前,确保左积不包含当前元素。range(n - 1, -1, -1)的结束边界 −1 不包含在遍历中,所以最后会处理 i=0。answer[i] *= suffix使用当前右积后,再纳入 nums[i];顺序反过来会把自身错误乘入。- 代码没有修改输入数组,答案是一个新列表。
示例推演 · 两次扫描
正向扫描时,prefix 依次以 1、1、2、6 写入 answer,得到 [1,1,2,6],然后反向合并:
| i | 使用前 suffix | answer[i] × suffix | 更新 suffix 后 | 此时 answer |
|---|---|---|---|---|
| 3 | 1 | 6×1 = 6 | 4 | [1,1,2,6] |
| 2 | 4 | 2×4 = 8 | 12 | [1,1,8,6] |
| 1 | 12 | 1×12 = 12 | 24 | [1,12,8,6] |
| 0 | 24 | 1×24 = 24 | 24 | [24,12,8,6] |
零元素推演:nums=[-1,1,0,-3,3],正向得到 [1,-1,-1,0,0]。反向访问 i=4、3、2、1、0 时,使用前的 suffix 分别为 1、3、−9、0、0;相乘后得到 [0,0,9,0,0]。零在扫描中自然传播。
正确性:第一轮归纳保证 answer[i] 为左侧乘积;第二轮归纳保证使用前 suffix 为右侧乘积。因此更新后的每项与解法一完全相同,压缩的是存储方式,状态定义未变。
复杂度采用题目规定范围下的整数运算模型。输出数组有 n 项,不能把「O(1) 额外空间」说成总空间 O(1)。
解法比较
| 方法 | 时间 / 额外空间 | 优点 | 使用建议 |
|---|---|---|---|
| 显式 left + right | O(n) / O(n) | 状态直观,方便画表推演 | 先理解排除当前元素的定义 |
| 复用 answer + suffix | O(n) / O(1) | 达到原题进阶要求,零无需分支 | 推荐提交,说明输出不计额外空间 |
易错点与边界检查
- 累计初值是 1,不是 0;乘法的单位元与加法不同。
- 不要用前缀总积相除来求区间乘积,零会导致信息丢失,且本题禁止除法。
- 没有零:[2,3] → [3,2],同时检查首尾的空积。
- 只有一个零:[0,2,3] → [6,0,0];两个零:[0,0,3] → [0,0,0]。
- 负数:[−1,2,−3] → [−6,3,−2],按正常乘法处理符号。
- 切片和逐项重算乘积会造成 O(n²),不满足线性要求。
面试追问:前缀乘积与前缀和有什么不同?
二者都复用已算过的聚合结果,但加法可以通过减法消去左段;乘法遇到零时不能可靠地通过除法恢复区间。本题改为组合左右两段,依赖的是乘法结合律与空积为 1。
复盘 · 形成选型能力
看到题目,先问这三个问题
| 判断问题 | 这组题中的结论 | 选择 |
|---|---|---|
| 需要查询某段和,还是统计全部区间? | 区间和 → 两个前缀相减;计数 → 查询历史频次 | P[r]−P[l] / freq[running−k] |
| 元素符号是否保证单调? | 209 全为正数;560 允许负数、零 | 209 二分或窗口;560 频次表 |
| 当前位置要排除,左右是否可以独立聚合? | 238 左边与右边的乘积独立 | 前缀乘积 × 后缀乘积 |
核心模板速查
| 记忆锚点 | 定义或顺序 | 检查点 |
|---|---|---|
| n + 1 个边界 | P[0]=0;P[i+1]=P[i]+nums[i] | P[i] 不含 nums[i] |
| 移项后查历史 | P[left] = P[right] − k | 查频次,不是只查存在 |
| 空前缀登记一次 | freq={0:1} | 允许从下标 0 起步 |
| 560:先查再登记 | running += x → count += freq[running−k] → freq[running] += 1 | 避免空子数组 |
| 209:先记再收缩 | while total ≥ target:更新长度 → 移出 nums[left] → left += 1 | ≥;while;长度 +1 |
| 238:先用再累计 | answer[i]=prefix → prefix*=nums[i];反向同理 | 空积 1;输出与辅助空间分开 |
面试前的最小回归用例集
| 题号 | 输入 | 期望 | 检测的错误 |
|---|---|---|---|
| 209 | target=4,nums=[4] | 1 | 边界相等与长度 1 |
| 209 | target=7,nums=[2,3,1,2,4,3] | 2 | 多次收缩 |
| 209 | target=100,nums=[1,2,3] | 0 | 无解哨兵 |
| 560 | nums=[3],k=3 | 1 | 空前缀 |
| 560 | nums=[0,0,0],k=0 | 6 | 先查后插、重复前缀 |
| 560 | nums=[1,-1,0],k=0 | 3 | 负数、频次累加 |
| 238 | nums=[2,3] | [3,2] | 首尾空积 |
| 238 | nums=[0,2,3] | [6,0,0] | 唯一零 |
| 238 | nums=[0,0,3] | [0,0,0] | 两个零 |
| 238 | nums=[-1,2,-3] | [-6,3,-2] | 符号与排除自身 |
掌握程度自测
题目是否掌握由题卡按钮标记;下面的能力清单独立保存,不影响 3 题总进度。
阶段性学习计划 · 4 天 + 间隔复习
| 安排 | 任务 | 验收标准 |
|---|---|---|
| Day 1 · 30–45 分钟 | 阶段 1;在纸上写出 P 并操作区间演示 | 任给合法 l、r,30 秒内算对区间和 |
| Day 2 · 45–60 分钟 | 阶段 2;分别默写 209 的二分与窗口 | 说清 while 的必要性和总时间 O(n) |
| Day 3 · 45–60 分钟 | 阶段 3;从 O(n²) 推到 560 哈希解 | 手推 [0,0,0] 得 6;解释空前缀与顺序 |
| Day 4 · 40–60 分钟 | 阶段 4;写出 238 两种方法,再闭卷复做三题 | 正确处理零和负数;分清总空间与额外空间 |
| 完成后第 3 / 7 / 14 天 | 每次随机选一题限时 15 分钟,不先看代码 | 答出状态定义、正确性、复杂度和至少 3 个边界用例 |
闭卷练习:把三题各压缩成一句话
209:正数保证单调,窗口达标就记长度并持续收缩。
560:当前前缀减 k 得到所需历史前缀值,查频次后再登记自己。
238:每个位置的答案等于不含自己的左积乘右积,输出先存左积再反向合并。
说明:原题链接需要联网;手册正文、语法高亮、图解、复制、主题、学习进度和演示均在本文件内。进度属于当前浏览器与文件位置;更换浏览器、移动文件或清理浏览器数据后,可能需要重新标记。打印时会展开自测答案。