我一直在玩wolfram语言并注意到一些事情:以不同方式编写的相同功能在时间上的作用非常不同。
考虑这两个功能:
NthFibonacci[num_] :=
If [num == 0 || num == 1, Return[ 1],
Return[NthFibonacci[num - 1] + NthFibonacci[num - 2]]
]
Fibn[num_] := {
a = 1;
b = 1;
For[i = 0, i < num - 1, i++,
c = a + b;
a = b;
b = c;
];
Return [b];
}
NthFibonacci[30]
需要大约5秒的时间来评估
Fibn[900 000]
也需要大约5秒的时间来评估
内置的Fibonacci[50 000 000]
我根本无法理解为什么三者之间的速度存在差异。从理论上讲,递归应该或多或少等同于for循环。造成这种情况的原因是什么?
答案 0 :(得分:3)
这是因为你提出的递归版本会进行大量的重复计算。构建一个函数调用树,看看我的意思。即使对于小到4的参数,也要看一下生成了多少函数调用来获取每个逻辑链的基本情况。
f(1)
/
f(2)
/ \
f(3) f(0)
/ \
/ f(1)
/
f(4)
\
\ f(1)
\ /
f(2)
\
f(0)
通过递归,函数调用的数量会以参数num
呈指数级增长。
相比之下,您的循环版本在num
中呈线性增长。在n
批次工作量少于2 n 之前,它不会占用n
的非常大的值。
答案 1 :(得分:1)
实现递归的方法有很多种; Fibonacci函数是一个可爱的例子。正如 pjs 已经指出的那样,经典的双递归定义呈指数级增长。基地是
φ=(sqrt(5)+1)/ 2 = 1.618 +
您的 NthFibonacci 实施方式就是这样做的。它的顺序为φ^ n,意味着对于大 n ,调用f(n + 1)需要与f(n)一样长的φ倍。
更温和的方法在执行流中仅计算一次功能值。而不是指数时间,它需要线性时间,这意味着调用f(2n)需要f(n)的2倍。
还有其他方法。例如,动态编程(DP)保留先前结果的缓存。在 pjs f(4)的情况下,DP实现只计算f(2)一次;第二个调用将看到第一个调用的结果是在缓存中,并返回结果而不是进一步调用f(0)和f(1)。这倾向于线性时间。
还有一些实现可以创建检查点,例如缓存f(k)和f(k)+1,k可被1000整除。这些节省时间的起点不会低于所需的值,给它们一个998次迭代的上限,以找到所需的值。
最终,最快的实现使用直接计算(至少对于更大的数字)并且在恒定时间内工作。
φ = (1+sqrt(5)) / 2 = 1.618...
ψ = (1-sqrt(5)) / 2 = -.618...
f(n) = (φ^n - ψ^n) / sqrt(5)
答案 2 :(得分:0)
@pjs指出的问题可以通过让递归函数记住先前的值来解决。 (消除If
也有帮助)
Clear[NthFibonacci]
NthFibonacci[0] = 1
NthFibonacci[1] = 1
NthFibonacci[num_] :=
NthFibonacci[num] = NthFibonacci[num - 1] + NthFibonacci[num - 2]
NthFibonacci[300] // AbsoluteTiming
{0.00201479,3.59 10 ^ 62}
清理你的循环版本(你几乎不应该在mathematica中使用Return
):
Fibn[num_] := Module[{a = 1, b = 1,c},
Do[c = a + b; a = b; b = c, {num - 1}]; b]
Fibn[300] // AbsoluteTiming
{0.000522175,3.59 10 ^ 62}
你看到递归形式较慢,但并非如此可怕。 (注意递归形式也会达到1000左右的深度限制)