在大屏或电脑上体验更佳——动画和操作区域更宽敞。
输入

斐波那契与记忆化

记忆化把函数结果缓存,重复参数可直接查询。斐波那契递推为 fib(n) = fib(n-1) + fib(n-2),边界是 fib(0) = 0、fib(1) = 1。朴素递归重复求解同一子问题,调用量指数增长;加入缓存后,只需求解 O(n) 个状态。

像计算员反复重算同一小计,记忆化给他一本结果簿:算一次,后续查表。本课比较朴素递归、自顶向下记忆化和自底向上填表,看三种方法如何获得相同结果。

记忆化如何工作

朴素递归是默认模式。fib(6) 调用 fib(5) 与 fib(4),各自继续分解到 fib(1)、fib(0)。没有记录,同一个 fib(4) 子树会重建多次。

自顶向下记忆化保留递归结构,增加 memo 字典。第一次算完 fib(4) 就存储,之后用期望 O(1) 查询命中,不再展开其子树。

自底向上填表不用递归。先设 dp[0] = 0、dp[1] = 1,再依次写 dp[i] = dp[i-1] + dp[i-2]。每格只写一次,依赖的前两格已经完成。

三者结果一致:朴素法不利用重复状态,记忆化按需要计算,填表按依赖顺序主动计算。

逐步演示

默认朴素递归,n = 6,目标 fib(6) = 8。

  1. 调用 fib(6):需要 fib(5) + fib(4),先进入 fib(5)。
  2. 沿左侧深入:fib(5) → fib(4) → fib(3) → fib(2),到 fib(1) = 1、fib(0) = 0。
  3. 向上合并:combine 计算 fib(2) = 1 + 0 = 1,再由独立的 return 步骤交回结果。随后得到 fib(3) = 1 + 1 = 2、fib(4) = 2 + 1 = 3、fib(5) = 3 + 2 = 5。
  4. 重算 fib(4):根仍需第二个孩子。没有缓存,整个九次调用的子树重新展开,状态提示指出同参数已出现过。
  5. 完成:fib(6) = 5 + 3 = 8,总计 25 次调用。

切到 Memoization,同样输入只用 11 次调用。再次请求 fib(2)、fib(3)、fib(4) 时直接查表,不再展开。切到 Bottom-up,树改为数组,填出 0, 1, 1, 2, 3, 5, 8;递推格依次聚焦、读取前两格、写入和。

树每次调用才出现一个节点;合并时两个孩子作为加数高亮,返回时结果标签使节点变绿。记忆化面板在 store 时新增行,命中时闪亮。调用计数 25 与 11 展示被省去的重复工作。

时间与空间复杂度

方法时间,以固定成本算术计空间对照
朴素递归O(φⁿ),也可用较松 O(2ⁿ) 上界O(n) 调用栈fib(6) 25 次,fib(7) 41 次
记忆化O(n)O(n) 缓存与 O(n) 栈每个非基础状态计算一次
自底向上O(n)O(n) 表只保留前两值可降为 O(1) 辅助存储

φ 约为 1.618。朴素调用数与斐波那契数同样指数增长;到 n = 50 时,重复调用已很多,DP 仍只处理线性数量状态。若使用任意精度整数,还需计入大整数加法和每个数的位数,表中的单位成本假设不能直接覆盖无限增长的数值。

边界情况

  • n = 0、1:立即返回,不再递归。滑块从 2 开始,到 7 截止,因为朴素树已达到 41 节点。
  • 基础状态不缓存:展示代码先检查 n < 2,再查 memo,因此 0、1 不作为缓存命中。
  • Python memo={} 默认参数:会跨调用保留同一个字典。这里可复用,但需要独立缓存时应另行创建。
  • 大 n:固定宽度整数可能先溢出,不能只看循环次数。

常见错误

  • 用小输入判断朴素法可扩展。 结果正确,但指数成本在小 n 时不明显。
  • 更改缓存与基础判断顺序却期待同一轨迹。 本课先处理基础状态,避免缓存它们。其他一致的实现也可正确,但会改变命中、存储和退出路径。
  • 表长与边界差一位。 一般递推需要 n + 1 格,循环前初始化 dp[0]、dp[1];n = 0 应单独处理,不能写越界的第二格。
  • 认为记忆化改变答案。 它只减少计算量,不改变递推结果。