【问题标题】:Recursive Finbonacci Optimization递归 Finbonacci 优化
【发布时间】:2016-07-28 15:07:57
【问题描述】:

以下代码用于计算前 70 个斐波那契数。我有两个问题:

1) 为什么对于i 的每个连续值,程序变得越来越慢? 是不是因为调用大数的函数会占用大量内存。

2) 我可以使用什么技术或编码方案来加速程序在运行时的计算?

#include <iostream>

int fib(int n) {
  if (n == 1 || n == 2) return 1;
  return fib(n - 1) + fib(n - 2);
}

void main() {
  for (int i = 1; i<70; i++)
    cout << " fib(" << i << ") = " << fib(i) << endl;
}

【问题讨论】:

  • 这主要是因为递归调用 fib() 函数的堆栈操作而变慢。
  • 存储上一次计算的结果,并在下一次计算中使用。每次循环时,您都在重新计算中间 fib' 系列,
  • 创建一个循环来计算数字并输出它。
  • Here 是您的代码的一个版本(由于时间原因有一个下限),它输出每次计算中的函数调用次数。 fib(30) 需要超过一百万次函数调用。
  • 实际上尝试在 32 位整数中计算第 70 个斐波那契并没有多大意义。大约 47 个之后会溢出。

标签: c++ recursion optimization runtime fibonacci


【解决方案1】:

这是一个简单的递归版本,它以线性时间 O(N) 运行。诀窍是返回两个值而不是一个。

void Fibo(int N, int& F0, int& F1)
{
  if (N == 1) {
    F1= F0= 1;
  }
  else {
    Fibo(N - 1, F0, F1);
    F1= F1 + F0;
    F0= F1 - F0;
  }
}

【讨论】:

    【解决方案2】:

    我想定量地强调递归算法的一个非常烦人的特性。

    当您调用fib(1)fib(2) 时,函数会立即返回,因此在这两种情况下执行的调用次数都是1。我们写成c(1) = c(2) = 1,其中c(n)是计算fib(n)的调用次数。

    当您使用n &gt; 2 调用fib(n) 时,您间接调用fib(n-1)fib(n-2),总调用次数为1 + c(n-1) + c(n-2)

    所以c(n)是由递归定义的

    c(n) = c(n-1) + c(n-2) + 1,
    c(1) = c(2) = 1
    

    解决方案称为莱昂纳多数 (https://oeis.org/A001595)。

    考虑到递归的形式,您很容易看到这些数字超过了斐波那契数,因此它需要更多的递归函数调用来计算第 N 个斐波那契数,而不是数字本身的值。随着斐波那契数呈指数级增长,调用次数也在增长。

    所以这不仅仅是“更多递归调用”,它是“大量更多递归调用”。这使得该方法对于大型 n 非常低效且不切实际。


    幸运的是,有一种简单的方法可以通过应用递归来迭代计算数字(在其他答案中给出)。

    直接公式也很有趣

    Fn = φ^n/√5
    

    四舍五入到最接近的整数,其中φ 是黄金分割率。

    【讨论】:

    • 我的猜测是直接公式会比我上面建议的整数和花费更多的时间来计算,因为它使用浮点数和昂贵的幂运算。
    • @UriRaz:谁知道?
    • 一个人总是可以写几个函数,看看哪个运行得更快。在我的家用电脑上,整数版本运行得更快。
    • @UriRaz:我不会做比较。
    【解决方案3】:

    要加速程序,您需要使用memoisation 的技术,这是一种非常奇特的说法“不要重新计算,只需存储答案并在需要时再次使用它”。

    您正在使用递归来计算答案,并且在每一步都调用您之前已经计算过的函数,从而增加了复杂性。上述程序的复杂性是指数级的,但是您可以将其减少到线性时间。

    您的代码稍作修改和memoisation

    #include<iostream>
    #define NOT_DEFINED -1
    
    using namespace std;
    
    long long memo[1000];
    
    long long fib(int n){
        if(memo[n] != NOT_DEFINED) return memo[n];
        if(n==1 || n==2) return 1;
    return memo[n] = fib(n-1)+fib(n-2);
    }
    
    int main(){
        for(int i = 0;i < 1000;i++) memo[i] = NOT_DEFINED;
        for(int i=1; i<70; i++)
        cout<<" fib("<<i<<") = "<<fib(i)<<endl;
    return 0;
    }
    

    ideone 上的解决方案链接:http://ideone.com/jW1VKD

    【讨论】:

      【解决方案4】:

      1) 为什么对于i 的每个连续值,程序会变得越来越慢?

      只是函数的递归调用越多,执行的时间就越多。

      是不是因为调用大数字的函数会占用大量内存。

      不,没有过多的内存占用(通过昂贵的动态内存分配操作)。所需的所有内存都保存在堆栈上,该堆栈已为进程预先分配。

      不过,对于稍大的数字,您可能很容易用完可用的堆栈内存。

      2)我可以使用什么技术或编码方案来加快程序运行时的计算速度?

      递归可能不是解决该问题的最佳方法。此处已提供更详细的答案:

      Is there a better way (performance) calculate fibonacci than this one?

      【讨论】:

      • 我不同意这个答案的最后一部分;您可以使用记忆或缓存递归计算 fib;只是 OP 的答案没有缓存之前的结果。
      【解决方案5】:

      递归解决方案的主要问题是它具有 O(2^N) 复杂度。例如。要计算 fib(10),它必须计算 fib(9)+fib(8)。要计算 fib(9),它必须计算 f(8) [第二次!] + f(7)。要计算 f(8) [对于第一个总和],它必须...

      最佳解决方案是使用一个简单的循环,其复杂度为 O(N)

      unsigned int f(unsigned int n)
      {
          unsigned int retVal = 1;
      
          if (n > 2)
          {
              unsigned int a, b = 1, c = 1;
      
              for (unsigned int index = 2; index < n; index++)
                  a = b, b = c, c = a + b;
      
              retVal = c;
          }
      
          return retVal;
      }
      

      [巨大的]差异是由于没有重新计算元素。

      您可以通过权衡内存来优化运行时 - 分配一个静态向量,每次调用该函数时要么存储以前未计算的值,要么使用那些已经存储的值。

      这将使程序运行期间使用的最大 N 的内存和运行时间计算 O(N)。

      【讨论】:

        【解决方案6】:

        除了其他答案。 它越来越慢,因为程序需要“记住”计算结果。

        如果你必须使用递归,我建议你研究一下尾递归。它重用了先前的堆栈帧。见Tail recursion in C++

        这是一个小例子:

        #include <iostream>
        
        int tail_fib(int n, int a, int b) {
          if (n == 0) return a;
          if (n == 1) return b;
          return tail_fib(n - 1, b, a + b);
        }
        
        void main() {
          for (int i = 1; i < 45; i++)
            cout << " fib(" << i << ") = " << tail_fib(i, 0, 1) << endl;        
        }
        

        【讨论】:

        • 这不是因为尾递归,而是将树递归转换为线性递归(将复杂性从O(fib(n)) 转换为O(n))。即使使用不优化尾调用的编译器也会更快。
        【解决方案7】:

        递归斐波那契计算具有指数复杂性,因此它变得非常缓慢。可以看分析here

        更快地计算斐波那契甚至不要考虑使用递归! 只使用普通循环,如下所示:

        #include <iostream>
        #include <cassert>
        #include <stdint.h>
        
        uint64_t fib(uint64_t n)
        {
            assert(n>0);
            uint64_t fprev = 1;
            uint64_t fprev2 = 0;
            while (--n>0)
            {
                uint64_t t = fprev2;
                fprev2 = fprev;
                fprev += t;
        
            }
            return fprev;
        };
        
        int main(int, char**)
        {
            for(uint64_t i=1; i<70; i++)
            std::cout<<" fib("<<i<<") = "<<fib(i)<<std::endl;
            return 0;
        }
        

        如果您需要更快地计算它,您可以在程序启动时预先计算它们并保存到表格中以供进一步使用。

        此外,斐波那契数列增长非常快(也像指数函数),所以要小心整数溢出。

        【讨论】:

          猜你喜欢
          • 2021-03-28
          • 1970-01-01
          • 2019-01-27
          • 2014-01-10
          • 2019-12-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多