递归Finbonacci优化

Recursive Finbonacci Optimization

本文关键字:优化 Finbonacci 递归      更新时间:2023-10-16

下面的代码是用来计算前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;
}

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

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

是因为用高数字调用函数会导致占用大量内存吗。

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

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

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

递归可能不是解决这个问题的最佳方法。这里已经有了更详细的答案:

有没有比这更好的方法(性能)来计算斐波那契?

除其他答案外。它变得越来越慢,因为程序需要"记住"计算结果。

如果必须使用递归,我建议您研究尾部递归。它重复使用上一个堆栈帧。参见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(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)。

为了加快程序的速度,你需要使用记忆技术,这是一种非常奇特的说法,"不要重新计算,只需存储答案,并在需要时再次使用"。

您使用递归来计算答案,在每一步中,您都会调用之前已经计算过的函数,从而增加复杂性。上述程序的复杂度是指数级的,但您可以将其简化为线性时间。

您的代码经过一些小的编辑和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

递归斐波那契计算具有指数复杂性,因此速度非常慢。你可以在这里阅读分析

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

#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;
}

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

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

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

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

当您用n > 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)。

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

因此,这不仅仅是"更多的递归调用",它是"大量的更递归的调用"。这使得该方法对于大型CCD_ 16来说效率极低且不切实际。


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

同样令人感兴趣的是直接公式

Fn = φ^n/√5

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

这里有一个简单的递归版本,它在线性时间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;
  }
}