(前置技能:高中数学知识,了解C/C++的函数)
求斐波那契数列的原理是十分十分之简单的…
这里认为“求斐波那契数列”这个编程题可以获得一些较为有用的算法思想,在这里分享一下自己学习后的理解.(语言为C++)
在这里插入代码片(新人发帖记录学习,只为更好地理解知识点.如有不对之处非常愿意受教ouo)
定义:对于数列F(n),
F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*),
则称这个数列是一个斐波那契数列.
换言之,某一项的值应该等于前两项的和.特别地,F(1) = F(2) = 1.
除此之外.我们也可以规定F(0) = 0.
问题:输入一个n,输出斐波那契数列的F(n).
直接实现
对于这个具有递推公式的简单数列,可以直接写循环结构实现:
#include <iostream>
using namespace std;
int main()
{
int n = 0;
cin >> n;
long long *F = new int[n];//新建一个长度为n的数组F[n].
F[0] = 0 , F[1] = 1;
for(int i = 2; i < n; ++i)
{
F[i] = F[i - 1] + F[i - 2];
}
cout << F[n] << endl;
return 0;
}
可以清楚地看到该程序的复杂度为O(n).
递归实现
如果用递归方式来实现,会怎么样呢?
#include <iostream>
using namespace std;
long long Fib(int n)
{
if(n == 1) return 1;
if(n == 0) return 0;
return Fib(n - 1) + Fib(n - 2);
}
int main()
{
int n = 0;
cin >> n;
cout << Fib(n) << endl;
return 0;
}
递归的问题
当输入为0~33的数时,递归方法和直接计算方法的速度差别很难发现. 但数字增加到34及以上时,很容易发现递归法存在卡顿,甚至在40以上已经远大于1秒.
为什么会出现这样的问题呢?
我们分析递归做法的二叉树结构. (通俗点讲,也就是树状图啦)
……
可以看出,调用F(n-1)的时候,函数也调用了F(n-2),而调用F(n-2)和F(n-1)的时候都需要调用F(n-3)… …继续下去,导致了数量巨大的函数重复调用.我们不难发现:
Fn会被调用1次、Fn-1会被调用1次、Fn-2会被调用2次、Fn-3会被调用3次、Fn-4会被调用5次… …
F(k) 会被调用F(n - k + 1)次!
因为函数调用次数过多,导致整个程序运算步骤过多.因此递归实现在数据量稍大时运算时间会成指数型增长.
这是一个非多项式复杂度的程序.
递归的优化
那么,有没有办法避免重复计算已经计算的F(k)呢?
我们先在程序中将所有F(n)初始化为-1,表示未计算.
再次遇到调用F(k)时,如果其值不是-1, 表示已经计算,不再向下调用,直接返回F(k).
这样就有效避免了重复调用的问题.
代码如下:
#include <iostream>
using namespace std;
long long f[100001] = {0};
long long fib(int n)
{
if(n == 1) f[n] == 1;
else if(n == 0) f[n] == 0;
else
{
if(f[n] != -1) return f[n];
else{
f[n] = fib(n - 1) + fib(n - 2);
return f[n];
}
}
}
int main()
{
for(int i = 0; i < 100001; ++i)
f[i] = -1;
int n = 0;
cin >> n;
cout << fib(n) << endl;
return 0;
}
这样就有效避免了重复调用, 效率提升极大.
在优化的工作中,我们学会了运用标记数组的方式做判断.同时也发现,很多时候递归其实不是优秀的算法. 除了容易重复计算以外,还有另一个原因:递归至终点需要回溯. 这又增加了运算的步骤. 另外,对简单的斐波那契数列使用函数结构本身就是一种对时间、空间的浪费.
总结:ACM的日子,得精打细算啊~OUO