第 24 项四种写法都给 46368,但耗时是 12.278 对 0.004 毫秒。 递归在 n=32 时要调用 7049155 次。第 90 项用浮点公式算,比精确值少 8584。
上一节排数字,这一节算数列。 斐波那契是「同一个答案、几百倍代价」最干净的例子,也是理解递归代价的入口。
一、四种写法,一个答案
Demo 写了四种:递归、迭代、记忆化、矩阵快速幂。n=24 时全部给出 46368。 耗时差得很远:递归 12.278 毫秒,迭代 0.004,记忆化 0.015,矩阵快速幂 0.013。 递归慢在哪?它把同一个子问题反复算。 算 F(24) 要算 F(23) 和 F(22),而 F(23) 里面又要再算一遍 F(22)——重复 exponentially 增长。 记忆化就是把算过的存起来,下次直接取。 这一个改动,把 12.278 毫秒压到 0.015 毫秒,快了约八百倍——而且代码只多了两行。
二、递归到底调了多少次
Demo 直接算了调用次数:n=20 要 21891 次,n=24 要 150049 次,n=28 要 1028457 次,
n=32 要 7049155 次。
规律是 2×F(n+1)-1。也就是说,调用次数本身就是一个斐波那契数——每加 1,工作量涨约 1.6 倍。
这就是为什么递归版本看起来优雅、实际不能用:n=40 就是三亿次调用,等半天。
能算出「调用多少次」比「感觉有点慢」有用得多。估算耗时、判断会不会超时,靠的都是这个数。
三、矩阵快速幂:把递推变成乘方
斐波那契可以写成矩阵形式:把向量 [F(n), F(n-1)] 乘一个固定矩阵,就得到 [F(n+1), F(n)]。
于是求第 n 项变成求那个矩阵的 n 次方。
而 n 次方可以用快速幂:每次把指数减半、底数自乘,复杂度从 O(n) 降到 O(log n)。
Demo 的实现里没有幂运算符,是手写的 2×2 矩阵乘法加二进制拆解。
n=24 时它跑 0.013 毫秒,跟迭代差不多——小 n 时优势看不出来,n 到百万级才显出来。
这套「矩阵表示 + 快速幂」不只用于斐波那契,线性递推都能这么加速。
它是「把递推问题变成矩阵乘法」这个套路的最小样例。
四、精度:第 90 项差了 8584
Demo 最后对比了精确值与浮点通项公式:
- 整数迭代:F(90) = 2880067194370816120
- 浮点公式:2880067194370824704
- 差 8584
通项公式 F(n) = (φⁿ - ψⁿ) / √5 在数学上是精确的,但 φ 是无理数,用浮点存必然截断,
n 一大误差就放大。
结论:算整数数列不要碰浮点。
真要算大数,用 Python 的 int(它没有上限),或者上 decimal 提高精度。
五、动手 Demo:四种算法与一次精度崩塌
打出前 20 项;对 n=24 用递归、迭代、记忆化、矩阵快速幂各算一次并计时; 再算 n=20/24/28/32 时递归的调用次数;最后对比 F(90) 的精确值与浮点通项公式的结果。
def fib_iter(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
def fib_memo(n):
if n not in memo:
memo[n] = fib_memo(n - 1) + fib_memo(n - 2)
return memo[n]
def fib_mat(n): # 矩阵快速幂
r = [list((1, 0)), list((0, 1))]
m = [list((1, 1)), list((1, 0))]
e = n
while e:
if e & 1:
r = mul(r, m)
m = mul(m, m)
e >>= 1
return r[0][1]
实跑输出:
--------------------------------------------------------------------
实例 3:斐波那契数列,四种算法与一次精度崩塌
--------------------------------------------------------------------
前 20 项 [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181]
算法 F(24) 耗时(ms)
----------------------------
递归 46368 10.410
迭代 46368 0.003
记忆化 46368 0.012
矩阵快速幂 46368 0.011
n=24 时递归还没慢到离谱;每加 1,它就要多算一倍。
n 递归要调用的次数
--------------------
20 21891
24 150049
28 1028457
32 7049155
次数是 2*F(n+1)-1,随 n 指数长。n=32 已经七百万次了。
F(50) 12586269025
F(90) 整数算 2880067194370816120
F(90) 浮点公式 2880067194370824704
整数减浮点 -8584
n 越大,浮点通项公式的误差越大——第 90 项差了 8584。
算整数数列就别碰浮点,除非你能接受「差不多」。
前 20 项是 0, 1, 1, 2, 3, 5, 8, …,到第 19 项是 4181。
注意第一项从 0 开始,不同教材从 1 开始,写法要跟题目对齐。
四种写法的耗时:12.278、0.004、0.015、0.013 毫秒。
递归比迭代慢三千倍,而结果完全一样。
调用次数那四行最能说明递归的代价:21891 → 150049 → 1028457 → 7049155。
n 从 20 涨到 32,工作量涨了 322 倍。
F(50) 是 12586269025,F(90) 是 2880067194370816120——Python 的整数没有上限,
这么长照样精确。
浮点公式给出 2880067194370824704,比真值大 8584。
小数点后十几位的精度,在这个量级上已经撑不住了。
30 秒实验
- 把递归版本加一行
print(n),跑 n=6,数一数它到底打了几行。 - 用
memo字典做缓存,把递归版改造成记忆化版,量一下提速倍数。 - 想一想:为什么矩阵快速幂在 n=24 时看不出优势,n 很大时才显出来?
常见误区
- 「递归写法优雅,就用递归」——n=40 要三亿次调用。
- 「记忆化是高级技巧」——加两行缓存,快八百倍。
- 「数学公式总是精确的」——浮点一存就截断,第 90 项差 8584。
自测三问(点开对答案)
1. 递归求斐波那契为什么慢? —— 同一个子问题被反复计算,调用次数本身按斐波那契增长。 2. 记忆化只改了什么,就快了约八百倍? —— 把算过的结果存进字典,下次直接取,不再重复递归。 3. 为什么 F(90) 的浮点公式会算错? —— φ 是无理数,浮点存储必然截断,n 越大误差越大。数列算完,下一个实例练排版——九九乘法表看着简单,四种版式差别不小。
- ☐ (本节完,下一节 3.3.4 实例4:使用Python输出九九乘法口诀表)