为什么以不同方式编写的相同函数具有不同的结果时间?

时间:2016-06-28 10:00:01

标签: performance recursion wolfram-mathematica wolframalpha wolfram-language

我一直在玩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循环。造成这种情况的原因是什么?

3 个答案:

答案 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左右的深度限制)