Memoization 是写一个更快的递归函数的好方法。但是,在这种情况下,有一个递归替代方案,它的运行速度比原始函数快得多,而且不需要记忆。
关键观察是看到原始定义执行了很多冗余计算。考虑一下如果我们计算fib[4]会发生什么:
fib[4] = fib[3] + fib[2]
fib[3] = fib[2] + fib[1]
fib[2] = fib[1] + fib[0]
fib[1] = 1
fib[0] = 1
∴ fib[2] = 1 + 1 = 2
fib[1] = 1
∴ fib[3] = 2 + 1 = 3
fib[2] = fib[1] + fib[0]
fib[1] = 1
fib[0] = 1
∴ fib[2] = 1 + 1 = 2
∴ fib[4] = 2 + 1 = 3
在这个过程中,fib[2] 和 fib[0] 被计算了两次,fib[1] 被计算了三次。对于更大的计算,浪费会急剧增加——事实上是指数级的。
如果要手动计算相同的斐波那契数,可能会进行如下操作:
0: given 0
1: given 1
2: 0 + 1 = 1
3: 1 + 1 = 2
4: 1 + 2 = 3
没有多余的计算。在任何给定点,只需要考虑前两个结果。后一种方法可以递归表示为:
fib2[0] = 0;
fib2[n_] :=
Module[{f},
f[n, p1_, _] := p1;
f[x_, p1_, p2_] := f[x + 1, p1 + p2, p1];
f[1, 1, 0]
]
Block[{$IterationLimit = Infinity}, fib2[100000]]
毫无疑问,这个表格不像原来的那样容易阅读。另一方面,原始函数在我的机器上计算 fib[35] 需要 35 秒,而修改后的函数的运行时间报告为零。此外,修改后的函数在 0.281 秒内计算 fib2[100000],不需要任何额外的记忆存储。 fib[100000] 完全超出了原始函数的范围,memoized version 使我的 Mathematica 7.01 内核崩溃——可能记忆的规则太多?
请注意,默认情况下,Mathematica 将迭代函数不超过 4096 次。要提高该限制,您必须为 $IterationLimit 分配更高的值,如上例所示。
当然,在 Mathematica 中,有很多非递归方法可以计算斐波那契数,包括内置的 Fibonacci 函数。但这不是本练习的重点。
尾调用优化?
总是希望使用tail calls 来表达递归函数。这允许通过简单的迭代来执行递归,而无需在堆栈上保留中间结果的开销。 fib2 是尾递归的。一些语言,如 Scheme,要求尾调用优化。其他语言,如Java,可以支持但不支持(或不支持,如Python)。
对于 Mathematica,尾调用优化执行到何种程度尚不清楚。有关这一点的进一步讨论,请参阅another SO question。