Fibonacci数
斐波那契数列(Fibonacci Sequence)
又称黄金分割数列,或兔子数列(因为是斐波那契观察兔子生殖而总结得到)。
在数学上,被定义为递推式:F(1)=1, F(2)=1, F(3)=2, F(4)=3, F(5)=5, ..., F(n)=F(n-1)+F(n-2)(n≥3)。
单元测试
#include <stdio.h> #include <time.h> int R[1000]={0}; int main(void){ int N = 50; // int N = 1e6; double start, finish; start = clock(); printf("%lld\n", fib(N)); finish = clock(); printf("%f s", (finish-start)/CLOCKS_PER_SEC); return 0; }
递归实现
根据它的递推表达式,很容易想到使用递归实现。
版本A:
1 int fib_A(int N){ 2 if( N <= 2 ) 3 return 1; 4 int tmp = fib_A(N-1)+fib_A(N-2); 5 printf("%d: %d\n", N, tmp); 6 return tmp; 7 }
代码很简单,但是计算很慢!算前50个Fibonacci数需要超过60秒。分析算法,发现很多实例在进行重复计算!
如此,根据这一点很容易想到使用查表的形式进行改进。
版本B:
1 long long fib_B(int N, long long* A){ 2 if( A[N] != 0 )//A已初始化为0 3 return A[N]; 4 5 A[N] = fib_B(N-1, A)+fib_B(N-2, A); 6 if( R[N] == 0 ){ 7 printf("%d: %lld\n", N, A[N]); 8 R[N] = 1; 9 } 10 return A[N]; 11 }
从结果看,仅算前50个改进明显,事实测试中算前10000个数压力都不大(不超过1s)。可以证明得到比较好的改进。
迭代
递归是一种自上而下的算法思路,因此为了计算n,会保留n(压栈)去结算n-1,以此类推,直到碰到递归基,占内存较大(O(n))。而迭代是自下而上,如果能得到迭代表达式,计算应该是非常快且不占空间的(O(1))。
Fibonacci递推式:F(n)=F(n-1)+F(n-2)。可知当前F(n)的结果需要根据前两项得到,因此我们可以使用两个变量,一直保存前两项的值,每次迭代都更新这两项,一直跌到到需要计算的n。
1 long long fib_itera(int N){ 2 int i; 3 long long f, g; 4 5 f = 1; g = 1;//第1项、第2项的值 6 for( i=3; i<=N; i++){ 7 f = f + g; 8 g = f - g; 9 printf("%d: %lld\n", i, f); 10 } 11 return f; 12 }
运行速度是跟之前的递归B版本差不多。
Fibonacci数应用
Fibonacci数不仅仅只是在学习递归的时候可以用到,实际中应该是很广泛的。这里举两个例子。
1. Fibonacci查找
是对二分查找的longN的常系数进行改进,可以通过严格的证明,使用Fibonacci黄金分割在查找时对数列进行分割会得到最优的常系数。具体实现与证明与在查找篇给出,这里暂不提。
2. 爬楼梯
也是比较常见的一种面试题:如果每次爬楼梯只能一次跨一个或两个台阶,那么到第n个台阶有多少中走法?语义规定地面算第0个台阶。
第一层台阶:F(1) = 1; 第二层台阶:可以一步一步,也可以一次两步,F(2) = 2;第三层台阶:可以1、1、1,或1、2,或2、1,F(3) = 3;第四层:1、1、1、1,或1、1、2,或1、2、1,或2、1、1,或2、2,F(4) = 5。
得到递推式,F(1) = 1,F(2) = 2,F(n) = F(n-1) + F(n-2)(n>2)。
同理,如果一次可以跨1、2、3个台阶,也一样可以先推理出前几项的值,然后得到递推表达式(变形的Fibonacci数列)。