【问题标题】:Performance of Fibonacci斐波那契的表现
【发布时间】:2011-05-07 00:30:29
【问题描述】:
f[0] = 0;
f[1] = 1;
f[x_] := f[x-1] + f[x-2]

这个函数在 Mathematica 中运行缓慢,我需要提高速度。我必须使用函数式编程和递归。我不知道为什么它运行得这么慢,即使是最轻微的想法如何改进它也会有所帮助。

【问题讨论】:

标签: performance wolfram-mathematica fibonacci


【解决方案1】:

编写更快的递归函数的一个好方法是让它记住以前的值。当然,这确实是以内存为代价的,但在这种情况下它会有所帮助。为了计算f[x],你计算f[x-1]f[x-2] - 然后计算f[x-1],你再计算f[x-2];您最终会多次重新计算很多值。 (原谅我的不精确!)

要随时随地存储东西,您可以使用以下成语:

f[x_] := ( f[x] = (* calculation of f[x] goes here *)  )

编辑:我在这台机器上没有mathematica,但我很确定不可能这会计算出错误的值。

f[0] = 0;
f[1] = 1;
f[x_] := ( f[x] = f[x-1] + f[x-2] );

f[256]

就像我在下面的评论中所说的那样,如果您对 f 有其他定义,您可能想先用 Clear[f] 清除它们。

感谢 rcollyer:小心$RecursionLimit!它默认为 256。(当然,这是有充分理由的。真正的深度递归通常是个坏主意。)

【讨论】:

  • f[x_] := f[x] = f[x-1] + f[x-2] 但这不起作用,你是这个意思吗?
  • @Jon:我忘了你是否需要任何括号,但是是的。确保不要忘记您的基本案例!如果你把它搞砸了并尝试使用它,你最终可能会存储不好的结果——你可能需要Clear[f] 来修复它,然后再试一次。
  • @rcollyer:谢谢。我很确定我不需要它们,但是在“它计算出错误的值”之后我变得偏执。
  • @Jon,刚想到这个,你用的 x 初始值是多少?而且,如果有的话,你得到了什么错误?从x = 1000 开始,我收到了Recursion depth of 256 exceeded 错误。
  • @rcollyer:哦,对了,我早该想到的。伙计,我生锈了。您可以设置变量 $RecursionLimit,或者(感谢 memoization)使用 Do[f[x],{x,0,1024,256}] 之类的方法来设置变量。
【解决方案2】:

杰弗罗米是对的。查看维基百科上的Memoization。他们使用阶乘的例子以及如何通过记忆来加速它。

【讨论】:

    【解决方案3】:

    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

    【讨论】:

      猜你喜欢
      • 2012-12-29
      • 1970-01-01
      • 2013-04-15
      • 2017-07-08
      • 2018-07-03
      • 2014-02-19
      • 2015-06-05
      • 2014-05-23
      • 2013-08-09
      相关资源
      最近更新 更多