RECURSION · 从定义到优化
递归:让小问题回答大问题。 用斐波那契数与爬楼梯,完整走过“写出递推 → 理解调用栈 → 消除重复计算 → 改写迭代 → 递归倍增”的学习路径。
Python 3 2 道指定题 · 无额外刷题 本地双击 · 离线阅读 建议用时 3–4 小时
02 完整题目教程
04 递进学习阶段
11 完整 Python 解法
01 递归执行实验室
学习顺序:先掌握递归,再理解为什么以及如何优化 阶段 学习内容 完成标准 01 基础知识 函数契约、终止条件、递归树、调用栈 能手推 F(4),区分调用次数与同时在栈的层数 02 核心模板 返回型递归、字典记忆化、复杂度分析 能解释缓存键和先查缓存的原因 03 典型应用 先 509 斐波那契数,再 70 爬楼梯 从题意推导状态与边界,再完成递归到滚动变量的优化 04 进阶算法 复用两题学习快速倍增 理解返回相邻两项与每层只递归一次
这本手册怎么用 先读思路并手推例子,再复制一份完整 Python 代码到力扣的 Python3 编辑器。每个 Python 代码块都可独立提交;两题卡片中的“标记已掌握”按题统计,进阶拓展不重复计数。搜索只筛选目录,正文始终完整,便于打印和离线查阅。
当前浏览器禁用了 JavaScript。教程、图示和 Python 代码仍可阅读;目录搜索、复制、进度和主题切换需要启用 JavaScript。 STAGE 01 / FOUNDATION 第一阶段:理解递归的契约递归是一种实现方式。先把函数含义说清楚,代码才有可靠的边界与返回值。
1. 递归函数到底在承诺什么? 递归函数会调用自身来求解更小的同类问题。以 fib(k) 为例,它的契约是“返回第 k 个斐波那契数”,并不要求调用者知道它内部怎样计算。写 fib(k - 1) + fib(k - 2) 时,我们使用的是子问题的答案。
01 定义状态与返回值k 表示第几项;fib(k) 返回一个整数,含义在全部递归层保持一致。
02 列出终止条件fib(0)=0,fib(1)=1;先处理边界,再访问子问题。
03 缩小问题规模k≥2 时,k−1 与 k−2 都比 k 小,最终到达 0 或 1。
04 组合并返回答案将两个子问题的返回值相加,用 return 交还上一层。
状态含义 → 终止条件 → 规模缩小 → 结果合并
2. 为什么这样写是正确的? 可以用数学归纳法理解:边界 0、1 的结果直接正确;假设所有小于 k 的状态都能正确返回,那么把第 k−1 项与第 k−2 项相加,就得到了按定义应有的第 k 项。正确的边界、正确的递推和一定会缩小的规模,三者缺一不可。
3. 递归树画“工作量”,调用栈看“同时存活” 下面是求 F(4) 的完整调用树。相同参数的节点仍是不同的调用,例如 F(2) 会被计算两遍。
F(4) 的完整递归调用树 根 F4 分为 F3 与 F2;F3 分为 F2 与 F1;两个 F2 各自分为 F1 与 F0。共九次调用,最长路径有四个栈帧,重复的 F2 以金色显示。 F(4) F(3) F(2) F(2) F(1)=1 F(1)=1 F(0)=0 F(1)=1 F(0)=0 蓝:递归状态 金:重复的子问题 绿:终止状态 调用总数 9;最大同时存活的 fib 栈帧数 4。整棵树不会同时驻留在调用栈中。 Python 先求加号左边,再求右边。父调用等待子调用返回,随后从原来的位置继续执行。观察第一个 F(2) 的返回过程:
进入 F(4) → 进入 F(3) → 进入 F(2) → 进入 F(1)
F(1) 返回 1:F(2) 记住左结果 1
进入 F(0),返回 0:F(2) 返回 1 + 0 = 1
F(3) 再调用 F(1),得到 1:F(3) 返回 1 + 1 = 2
F(4) 再调用 F(2),重复求得 1:F(4) 返回 2 + 1 = 3
4. 逐步执行实验室 选择输入,对比普通递归与“已预存 0、1”的字典记忆化。此处用内联 JavaScript 模拟与 Python 相同的调用顺序,不执行 Python,也不访问网络。
输入 n 0 1 2 3 4 5 6 7 8 方式 普通递归 记忆化递归 下一步 显示完整过程 重新开始
统计包含命中缓存的函数调用,不含外层 Solution 方法。返回事件不会额外计入调用次数;n 最多为 8,以便专注观察。
STAGE 02 / PATTERNS 第二阶段:从核心模板到复杂度这两题都属于“分解问题、合并返回值”的递归;状态依赖有重叠,适合进一步记忆化。
模板 A:返回型递归 用一条清晰的函数契约贯穿整个过程。以下是语言无关的伪代码,便于迁移;可提交的 Python 完整实现分别放在两题中。
函数 solve(状态):
如果状态是边界:
返回边界答案
左答案 ← solve(更小的状态一)
右答案 ← solve(更小的状态二)
返回合并(左答案, 右答案)
模板 B:带记忆化的递归 相同状态若总返回相同答案,就可以只计算一次。键必须包含所有会影响答案的参数。本专题只依赖剩余规模 k,因此用一个整数作键即可;如果问题还依赖位置、背包容量等,就要把这些量一起放入键。
在本次求解开始时建立缓存,并存入边界答案
函数 solve(状态):
如果状态已在缓存中:
返回缓存[状态]
答案 ← 合并(solve(子状态一), solve(子状态二))
缓存[状态] ← 答案
返回答案
返回 solve(初始状态)
查缓存必须使用“是否存在” if k in memo 能正确识别答案为 0 的状态;if memo.get(k) 会把 0 当作假值。将字典或装饰器定义在解题方法内部,可让每次求解使用自己的缓存,也避免可变默认参数带来的跨调用残留。
复杂度:数状态、数转移、数栈深度 φ = (1 + √5) / 2 ≈ 1.618。O(2ⁿ) 是正确上界,但并非最紧界。 形式 如何计算时间 时间复杂度 辅助空间 普通二分支递归 T(n)=T(n−1)+T(n−2)+O(1);大量状态被重复求值 O(φⁿ),常用较松上界 O(2ⁿ) O(n) 调用栈 记忆化递归 O(n) 个不同状态 × 每个状态 O(1) 次转移 O(n) O(n) 缓存 + O(n) 栈 = O(n) 数组动态规划 按状态顺序把每项计算一次 O(n) O(n) 数组,无递归栈 滚动变量迭代 只保留下一步所需的相邻两项 O(n) O(1) 辅助变量 快速倍增递归 T(n)=T(⌊n/2⌋)+O(1) O(log n) O(log n) 调用栈
对于斐波那契普通递归,函数调用总数满足 C(0)=C(1)=1 和 C(n)=1+C(n−1)+C(n−2),可得 C(n)=2F(n+1)−1。例如 n=30 时需要 2,692,537 次调用,而记忆化只需要线性次调用。
统一复杂度口径 全文使用面试常见的单位代价模型:一次整数加法、乘法和哈希表查找按 O(1) 计算,空间按变量/状态数计。Python 整数会随数值增长;若扩展到远超题目范围的大 n,则 F(n) 的位数为 Θ(n),大整数运算耗时及每个整数占用的位空间必须另行计入。
STAGE 03 / PRACTICE 第三阶段:两道典型应用先从数学定义直接写递归,再从真实计数问题推导同一递推关系。每种解法都给出完整提交代码。
01 LeetCode 509 斐波那契数简单 | 核心知识点:返回型递归、递推关系、记忆化、动态规划、滚动变量 接口:Solution.fib(self, n: int) -> int | 输入范围:0 ≤ n ≤ 30
题意与状态定义 输入非负整数 n,返回斐波那契序列的第 n 项。序列从 0, 1 开始,每个后续数等于前两个数之和。这里“第 n 项”按 0 起始编号,不能把第 0 项跳过。
F(0) = 0,F(1) = 1;F(n) = F(n−1) + F(n−2),n ≥ 2
输入 输出 核对 n=2 1 F(1)+F(0)=1+0 n=3 2 F(2)+F(1)=1+1 n=4 3 F(3)+F(2)=2+1 n=0(边界) 0 直接返回定义值
解题思路:从定义走向高效实现 先把递推式翻译为函数调用,得到最直观的递归。随后观察到相同 k 会反复出现,用字典或缓存装饰器把答案保留下来;再按 k 从小到大填写数组,最后利用“只依赖前两项”的特点压缩为两个变量。它们计算同一状态,只是求值顺序和保存结果的方式不同。
解法一 · 朴素递归:先写对递归关系 dfs(k) 承诺返回 F(k)。k 为 0 或 1 时直接返回 k;否则先求前一项,再求前两项,并把它们相加。
class Solution :
def fib (self , n: int ) -> int :
def dfs (k: int ) -> int :
if k <= 1 :
return k
return dfs(k - 1 ) + dfs(k - 2 )
return dfs(n)
关键代码解释: k <= 1 同时覆盖 0、1;两个递归分支都严格缩小规模;外层必须 return dfs(n),否则调用结果会是 None。
复杂度: 时间 O(φⁿ),也可写 O(2ⁿ) 上界;空间 O(n),来自最长递归链,不能按整棵调用树的节点数算栈空间。
解法二 · 字典记忆化:每个状态只展开一次 在每次 fib 调用内部建立 memo,预存两个边界。先查缓存,未命中才向下递归;计算结束后将结果写回缓存。所有内层调用共享这一个字典。
class Solution :
def fib (self , n: int ) -> int :
memo = {0 : 0 , 1 : 1 }
def dfs (k: int ) -> int :
if k in memo:
return memo[k]
memo[k] = dfs(k - 1 ) + dfs(k - 2 )
return memo[k]
return dfs(n)
关键代码解释: k in memo 检查键而非答案真假;右边的递归求值完成后,才执行写入 memo[k];字典不在 dfs 内反复创建,否则无法复用子问题答案。
复杂度: 时间 O(n);空间 O(n),其中字典 O(n),递归栈 O(n)。
解法三 · lru_cache:用标准库表达记忆化 lru_cache(maxsize=None) 按参数缓存返回值,不限制缓存条目数。这里的参数 k 是整数,可哈希;每次外层调用都会新建被装饰的内部函数。该写法与手动字典的核心思路一致。
from functools import lru_cache
class Solution :
def fib (self , n: int ) -> int :
@lru_cache(maxsize=None )
def dfs (k: int ) -> int :
if k <= 1 :
return k
return dfs(k - 1 ) + dfs(k - 2 )
return dfs(n)
关键代码解释: 装饰器必须放在递归实际调用的 dfs 上,且递归分支也调用这个函数;仅在外层缓存一次入口不能消除内部重复调用。无须自行维护缓存字典。
复杂度: 时间 O(n);空间 O(n) 缓存与 O(n) 栈合计 O(n)。
解法四 · 数组动态规划:从小到大填写状态 令 dp[i]=F(i),按照 i=2 到 n 的顺序计算,依赖的前两项已经准备好。先处理 n=0、1,再初始化数组,避免长度不足时访问 dp[1]。
class Solution :
def fib (self , n: int ) -> int :
if n <= 1 :
return n
dp = [0 ] * (n + 1 )
dp[1 ] = 1
for i in range (2 , n + 1 ):
dp[i] = dp[i - 1 ] + dp[i - 2 ]
return dp[n]
关键代码解释: range(2, n + 1) 包含 n;dp[0] 使用数组初始值 0;dp[i] 对应第 i 项,不是前 i 项的总和。
复杂度: 时间 O(n);空间 O(n) 数组,无递归调用栈。
解法五 · 滚动变量:面试推荐提交版 数组里更早的项在后续不再使用。初始化 a=F(0), b=F(1)。每轮把窗口向右移动一项;循环 t 轮后始终有 a=F(t), b=F(t+1),因此循环 n 轮后返回 a。
class Solution :
def fib (self , n: int ) -> int :
a, b = 0 , 1
for _ in range (n):
a, b = b, a + b
return a
关键代码解释: a, b = b, a + b 右侧先按旧值一起求出,再赋给左侧。不能拆成先 a=b 再 b=a+b,否则第二行用到的是更新后的 a。n=0 时循环不执行,返回 0。
复杂度: 时间 O(n);空间 O(1) 辅助变量。
示例推演:n=4 的三种视角 递归合并: F(4)=F(3)+F(2)=(F(2)+F(1))+(F(1)+F(0))=2+1=3。完整调用顺序见第一阶段的递归树。
字典记忆化: 初始 {0:0,1:1} → 求得 F(2)=1 后写入 → 求得 F(3)=2 后写入 → F(4) 的右分支 F(2) 直接命中 → 写入 F(4)=3。每个非边界状态只展开一次。
滚动变量推演:n=4 已完成轮数 t a=F(t) b=F(t+1) 说明 0 0 1 初始化 1 1 1 旧 b=1;旧 a+b=1 2 1 2 继续右移 3 2 3 继续右移 4 3 5 返回 a=3
509 解法对比(单位代价模型) 解法 时间 空间 优点与取舍 朴素递归 O(φⁿ) O(n) 最直观;重复计算严重,适合教学 字典记忆化 O(n) O(n) 缓存过程清晰,便于调试 lru_cache O(n) O(n) 简洁;需理解装饰器与缓存键 数组 DP O(n) O(n) 中间项全部保留,容易画表 滚动变量 O(n) O(1) 简洁高效;需准确维护变量含义 快速倍增(第四阶段) O(log n) O(log n) 递归规模减半;推导与实现更复杂
边界检查与易错点 漏掉 n=0: F(0) 必须返回 0;用“n≤2 返回 1”会把这个边界写错。把递归返回值当输出: LeetCode 检查方法的返回值,不检查 print 的文本。缓存写在递归函数内部初始化: 每层都新建字典会失去记忆化效果;当前写法在外层创建一次。循环次数与返回变量不匹配: 上面滚动代码执行 n 轮、返回 a。若改为返回 b,就会错位一项。仅凭“两个分支”就断言 O(n²): 时间取决于整棵调用树,未缓存时是指数增长;栈空间看最长活动链。n 正确返回 检查目的 0 0 空循环与第 0 项 1 1 第二个基本状态 2 1 第一次递推 30 832040 题目范围上界
题号、范围、难度与示例依据 力扣 509 原题 ;讲解、推导与代码为本手册编写。
02 LeetCode 70 爬楼梯简单 | 核心知识点:分类计数、递归建模、记忆化、动态规划、滚动变量 接口:Solution.climbStairs(self, n: int) -> int | 输入范围:1 ≤ n ≤ 45
题意:不同的行走顺序分别计数 有 n 级台阶,每次可以向上走 1 级或 2 级,返回恰好到达楼顶的走法数。1→2 与 2→1 的顺序不同,因此是两种方案;越过楼顶不算合法完成。
输入 输出 所有方案 n=2 2 1+1;2 n=3 3 1+1+1;1+2;2+1 n=1(边界) 1 1
解题思路:按最后一步分类,不重不漏 定义 W(k):恰好爬到第 k 级的不同走法数。 若最后一步走 1 级,之前必须在 k−1 级;在这些走法后追加 1,得到 W(k−1) 种。 若最后一步走 2 级,之前必须在 k−2 级;在这些走法后追加 2,得到 W(k−2) 种。 每种合法走法的最后一步只能属于其中一类,两类互不重叠,且包含所有可能,因此相加。 W(0) = 1,W(1) = 1;W(k) = W(k−1) + W(k−2),k ≥ 2
为什么 W(0)=1? 这是为递推引入的内部状态:还剩 0 级时,“不再迈步”就是一种完整方案,称为空方案。它让 W(2)=W(1)+W(0)=2 自然成立。题目实际输入从 1 开始;也可以使用 W(1)=1、W(2)=2 并从 k=3 递推,但两套边界不能混用。
爬到第四级的两种最后一步分类 从第三级的三种方案各追加一步,得到三种;从第二级的两种方案各追加两步,得到两种;合计五种。 W(3) = 3 W(2) = 2 W(4) = 3 + 2 = 5 最后一步前在第 3 级 最后一步前在第 2 级 追加 1 级 追加 2 级 按最后一步分类求和 计数用加法;两类的“最后一步”不同,所以不会重复统计。 解法一 · 朴素递归:直接实现分类计数 dfs(k) 表示还剩 k 级时的走法数,递推关系与“爬到第 k 级”的 W(k) 相同。k=0 或 1 时返回 1;只在 k≥2 时展开两个分支,因此不会访问负数状态。
class Solution :
def climbStairs (self , n: int ) -> int :
def dfs (k: int ) -> int :
if k <= 1 :
return 1
return dfs(k - 1 ) + dfs(k - 2 )
return dfs(n)
关键代码解释: k <= 1 这里返回 1,因为内部状态 0 的含义是“恰好完成”;与斐波那契返回 k 不同。本实现只覆盖题目输入和递推产生的非负状态,不能把它当作负数状态也返回 1 的通用模板。
复杂度: 时间 O(φⁿ),或用 O(2ⁿ) 上界;空间 O(n) 调用栈。
解法二 · 记忆化递归:推荐先掌握 相同剩余级数的答案相同,将 dfs 用标准库缓存装饰器包装。递归计算仍是自顶向下,只是每个 k 的答案在首次计算后直接复用。
from functools import lru_cache
class Solution :
def climbStairs (self , n: int ) -> int :
@lru_cache(maxsize=None )
def dfs (k: int ) -> int :
if k <= 1 :
return 1
return dfs(k - 1 ) + dfs(k - 2 )
return dfs(n)
关键代码解释: maxsize=None 保留本次求解的所有已算状态;只缓存整数走法数,不保存每条路径。若实际枚举所有路径,输出规模会很大,不能沿用当前线性时间结论。
复杂度: 时间 O(n);空间 O(n),含缓存和递归栈。
解法三 · 数组动态规划:把递归依赖排成顺序 令 dp[i]=W(i)。初始化内部状态 dp[0]=1 与第一阶 dp[1]=1,然后从第 2 阶算到第 n 阶。
class Solution :
def climbStairs (self , n: int ) -> int :
if n <= 1 :
return 1
dp = [0 ] * (n + 1 )
dp[0 ], dp[1 ] = 1 , 1
for i in range (2 , n + 1 ):
dp[i] = dp[i - 1 ] + dp[i - 2 ]
return dp[n]
关键代码解释: 数组大小为 n+1,确保下标 n 存在;提前处理 n=1,也使内部扩展 n=0 时不越界;循环从 2 开始,以 1、1 两个边界为依赖。
复杂度: 时间 O(n);空间 O(n) 数组。
解法四 · 滚动变量:面试推荐提交版 保留相邻两项 a=W(0)=1、b=W(1)=1。每次把窗口向右移动一格。循环 t 轮后 a=W(t),所以 n 轮后返回 a。
class Solution :
def climbStairs (self , n: int ) -> int :
a, b = 1 , 1
for _ in range (n):
a, b = b, a + b
return a
关键代码解释: 这里循环范围仍是 range(n),但初始值改为 1、1。n=1 时进行一轮,a=1;n=2 时进行两轮,a=2。最终 b 是 W(n+1),不要误返回 b。
复杂度: 时间 O(n);空间 O(1) 辅助变量。
示例推演:n=4,答案为 5 分类推演: W(2)=1+1=2;W(3)=2+1=3;W(4)=3+2=5。按最后一步分类,前三种以 1 结束,后两种以 2 结束:
1+1+1+1最后走 1
1+2+1最后走 1
2+1+1最后走 1
1+1+2最后走 2
2+2最后走 2
记忆化推演: 首次展开 dfs(4)→dfs(3)→dfs(2),计算边界后缓存 W(2)=2;回到 dfs(3) 得到并缓存 3;回到 dfs(4) 时右分支 dfs(2) 直接使用缓存 2,合并得到 5。
滚动变量推演:n=4 完成轮数 t a=W(t) b=W(t+1) 动作 0 1 1 初始化 1 1 2 同一轮先算右侧,再一起更新 2 2 3 保留当前与下一项 3 3 5 继续右移 4 5 8 返回 a=5
70 解法对比(单位代价模型) 解法 时间 空间 优点与取舍 朴素递归 O(φⁿ) O(n) 建模直接;上限下不可实用 记忆化递归 O(n) O(n) 保持递归语义,消除重叠状态 数组 DP O(n) O(n) 状态清楚,可随时查看每阶答案 滚动变量 O(n) O(1) 本题通常首选;不保存所有中间答案 快速倍增(第四阶段) O(log n) O(log n) 利用 W(n)=F(n+1),适合进阶理解
边界检查与易错点 把 W(0) 写为 0: 会导致 W(2)=1,漏掉直接跨两阶这一种方案。混用两套边界: 若使用 W(1)=1、W(2)=2,循环就应从 3 开始;本页代码统一使用 W(0)=W(1)=1。把顺序不同的方案合并: 1+2 与 2+1 是两种;本题不是忽略顺序的组合计数。用乘法合并最后一步的类别: 两类互斥,应相加;乘法用于连续独立选择的组合。无边界地向负数递归: 若另写“从当前位置向上走”的版本,越界应返回 0、恰好到顶返回 1。本页缩小剩余级数的写法在 k≤1 提前返回,避免负数。直接枚举全部走法: 题目只求数量,无须保存每个路径列表;缓存走法数才能达到 O(n)。认为加缓存就能处理任意大 n: 缓存仍有线性调用栈;扩展规模时可改迭代或快速倍增。n 正确返回 检查目的 1 1 原题下界 2 2 一步两级方案 3 3 区分行走顺序 4 5 按最后一步分类 45 1836311903 原题上界
题号、范围、难度与示例依据 力扣 70 原题 ;讲解、推导与代码为本手册编写。
STAGE 04 / ADVANCED 第四阶段:用快速倍增把规模减半继续深化原有两题,不增加题目。掌握线性解法后,再学习“递归一次,同时返回两个相关答案”的设计。
1. 为什么返回相邻两项? 如果递归只返回 F(k),很难直接构造 F(2k+1)。让 pair(k) 同时返回 (F(k), F(k+1)),就能在一层内用恒等式完成倍增。设 a=F(k),b=F(k+1):
F(2k) = a × (2b − a);F(2k+1) = a² + b²
对 k≥1,斐波那契加法恒等式给出 F(2k)=F(k)·[F(k−1)+F(k+1)];再用 F(k−1)=b−a,就得到第一式。另一加法恒等式给出 F(2k+1)=F(k)²+F(k+1)²。k=0 时直接代入 a=0、b=1,两式也都成立。
先递归一次求 pair(m // 2),得到 a、b。 算出 c=F(2k)、d=F(2k+1)。若 m 为偶数,返回 (c,d)。 若 m 为奇数 m=2k+1,返回 (d,c+d),即 (F(m),F(m+1))。 递归边界是 pair(0)=(0,1);每次参数整除 2,深度为 O(log n)。 pair(5) 的减半与回溯过程 pair(5) pair(2) pair(1) pair(0) ÷2 向下 ÷2 ÷2 向上返回:(0,1) → (1,1) → (1,2) → (5,8) 每层一次递归;每个返回值是一对相邻斐波那契数 倍增的关键:缩小参数与扩大返回信息同时发生。 2. 509 · 快速倍增完整代码 入口取相邻两项中的第一项,得到 F(n)。代码不使用浮点数,所有中间运算均为精确整数。
class Solution :
def fib (self , n: int ) -> int :
def pair (m: int ) -> tuple :
if m == 0 :
return 0 , 1
a, b = pair(m // 2 )
c = a * (2 * b - a)
d = a * a + b * b
if m % 2 == 0 :
return c, d
return d, c + d
return pair(n)[0 ]
关键代码解释: a,b 来自同一个递归调用,避免把两项分别递归求值。奇数分支返回 (d,c+d),第二项必须是下一项。n=0 直接返回 (0,1) 的第一项 0。
复杂度: 单位代价模型下,时间 O(log n),空间 O(log n) 调用栈;n=0 时均为 O(1)。
3. 为什么爬楼梯 W(n)=F(n+1)? 比较边界:W(0)=1=F(1),W(1)=1=F(2)。两者都满足“前两项相加”的递推关系,所以由归纳法可得任意 n≥0 都有 W(n)=F(n+1)。这是一项索引偏移,而不是改变了递推式。
4. 70 · 快速倍增迁移完整代码 仍返回相邻斐波那契数,但入口求 pair(n + 1) 并取第一项。为便于独立复制提交,这里保留完整辅助函数。
class Solution :
def climbStairs (self , n: int ) -> int :
def pair (m: int ) -> tuple :
if m == 0 :
return 0 , 1
a, b = pair(m // 2 )
c = a * (2 * b - a)
d = a * a + b * b
if m % 2 == 0 :
return c, d
return d, c + d
return pair(n + 1 )[0 ]
关键代码解释: n+1 来自已证明的状态映射。n=4 时求 F(5)=5;n=1 时求 F(2)=1。不要直接返回 F(n),那会让答案少偏移一项。
复杂度: 单位代价模型下,时间 O(log n),空间 O(log n) 调用栈(原题 n≥1)。
示例:509 输入 5 得 5;70 输入 4 也用 pair(5),得到 5 回溯状态 已知的半规模结果 c=F(2k) d=F(2k+1) 返回 pair(0) 直接命中边界 — — (0,1) pair(1),奇数 a=0,b=1 0 1 (1,1) pair(2),偶数 a=1,b=1 1 2 (1,2) pair(5),奇数 a=1,b=2 3 5 (5,8)
进阶易错点与选择建议 不要在同一层分别调用两次 pair(m//2);不要把幂运算写成异或 ^;奇数返回值不能写成 (c+d,d)。在本题的小范围内,滚动变量实现通常已足够清楚高效;倍增用于学习分治和回答优化追问。浮点闭式公式有舍入风险,不作为这里的精确整数提交方案。
矩阵快速幂也可以在 O(log n) 次矩阵运算内求解同一递推,但需要维护 2×2 矩阵乘法。快速倍增把该递推的代数结构直接浓缩为两项恒等式;本专题优先完整掌握这条进阶路径。
两题对照与核心模板速查 比较项 509 斐波那契数 70 爬楼梯 函数契约 F(n):第 n 项的值 W(n):到第 n 阶的方案数 边界 F(0)=0,F(1)=1 W(0)=1,W(1)=1(0 是内部状态) 递推依据 数学定义 按最后走 1 阶或 2 阶分类求和 基本递推 F(n)=F(n−1)+F(n−2) W(n)=W(n−1)+W(n−2) 滚动初始化 a,b=0,1 a,b=1,1 滚动不变量 t 轮后 a=F(t),b=F(t+1) t 轮后 a=W(t),b=W(t+1) n 轮后返回 a a 两题联系 F(n) W(n)=F(n+1)
面试时如何解释你的优化? 先建模: 说明参数和返回值,列出边界、递推与规模缩小的依据。找重复: 在 F(4) 的递归树标出重复 F(2),解释为何普通递归是指数时间。加缓存: 相同参数重复出现,记忆化使每个状态只展开一次,时间 O(n)、空间 O(n)。排顺序: 依赖只指向更小状态,改成从小到大的 DP,消除递归栈。压空间: 只读取前两项,用两个变量维护不变量,空间降为 O(1)。回应追问: 若要求更少算术步骤,解释快速倍增;同时说明大整数代价的适用口径。面试追问与参考回答 为什么记忆化后是 O(n),而不是 O(2ⁿ)? 不同 k 只有 O(n) 个,每个状态只首次展开两个子调用。再次访问仅做一次缓存查询,因此总转移数也是 O(n),不再随完整递归树指数增长。
递归有两个分支,为什么栈空间只有 O(n)? 分支依次执行。求左分支时,右分支还没有入栈;返回后栈帧释放,再执行右分支。同时存活的帧沿一条根到叶路径,最多线性层数。
爬楼梯如何证明两类方案不会重复? 最后一步要么走 1 级,要么走 2 级,不可能同时属于两类。删除最后一步后分别对应到第 n−1 级与第 n−2 级的唯一前缀路径,因此两类既互斥又穷尽。
为什么不能把两个滚动更新拆成普通顺序赋值? 新 b 需要旧 a 与旧 b。先更新 a 会覆盖旧值,接着算 b 得到错误结果。可使用 Python 并行赋值,或额外临时变量保存旧 a。
若要输出每一条爬楼梯路径,复杂度还是线性吗? 不是。计数只返回一个整数;枚举必须至少花时间写出所有方案以及各方案的路径长度,总成本至少受输出规模约束,不能用“每个剩余级数只算一次”直接推导 O(n)。