斐波那契的问题应该是每个人都听说过的,它在中学数学课本以及某些C语言教材中曾以兔子问题的形式出现过,它的发明者是意大利数学家列昂纳多·斐波那契(Leonardo Fibonacci,1170—1250)。1202 年,他撰写了《Liber Abaci》一书,该书是一部较全面的初等数学著作。兔子问题的具体描述如下:
假设第 1 个月有1 对刚诞生的兔子,第2 个月进入成熟期,第3 个月开始生育兔子,而1 对成熟的兔子每月会生1 对兔子,兔子永不死去……那么,由1 对初生兔子开始,12 个月后会有多少对兔子呢?
从兔子数列抽象出来的斐波那契数列如下:
1,1,2,3,5,8,13,21,34,…
对上述的斐波拉契数列进行分析,可以得到这样的一个递推式:
下面介绍几种代码解决思路:
使用通项
直接使用通项公式求解每一项的值是常规的数组元素求解办法。斐波拉契数列的通项公式可以通过列解数列的特征方程来进行求解,当n>2的时候可以得到这样的一个特征方程:
x2−x−1=0
x
2
−
x
−
1
=
0
,对这个特征方程进行求解,可以得到:
x1=1−5√2
x
1
=
1
−
5
2
,
x2=1+5√2
x
2
=
1
+
5
2
。代入原递推式,有
f(n)=Axn1+Bxn2
f
(
n
)
=
A
x
1
n
+
B
x
2
n
,又由于f(1)=f(2)=1,可以列方程组
求解出 A=15√ A = 1 5 , B=−15√ B = − 1 5 即斐波拉契的通项公式是: f(n)=−15√xn1+15√xn2 f ( n ) = − 1 5 x 1 n + 1 5 x 2 n ,当n趋向于无穷的时候,通项公式可以近似等于为: f(n)=15√xn2 f ( n ) = 1 5 x 2 n 。
#include<stdio.h>
#include<math.h>
//直接使用通项公式
int Fib1(int n){
double sum = 0.0;
sum = (1 / sqrt(5)) * (pow(((1 + sqrt(5)) / 2),n) - pow(((1 - sqrt(5)) / 2),n));
return (int)sum;
}
//使用近似的通项公式,只测试了前面几个数据就是咯
int Fib2(int n){
double dsum = 0.0;
int isum = 0;
dsum = (1 / sqrt(5)) * pow(((1 + sqrt(5)) / 2),n);
//+0.5强制类型转换实现一个简单的四舍五入
isum = (int)(dsum + 0.5);
return isum;
}
int main()
{
int n = 0;
int sum1 = 0;
int sum2 = 0;
scanf("%d",&n);
sum1 = Fib1(n);
sum2 = Fib2(n);
printf("%d,%d\n",sum1,sum2);
return 0;
}
使用递归
针对上面的递推式,我们也可以很容易就会想到使用递归的方法来编写程序代码来解决兔子问题,记忆里面当初接触的C语言教材上面也是拿它来讲解递归这个知识点的。递归的代码也很好写,基本如下:
int Fib3(int n){
if(n < 1)
return -1;
else if(n == 2 || n == 1)
return 1;
else
return Fib1(n - 1) + Fib1(n - 2);
}
嗯,分析一下上面代码的时间和空间复杂度。当然,使用通项公式的求解方法的时间复杂度和空间复杂度都是1,这当然是效率最高的代码,但是其通项公式的求解需要较高的数学计算能力。
递归法的时间复杂度的话,当n=1和n=2的时候,都是直接返回1,故时间复杂度T(1)=T(2)=1;当n>2的时候,时间复杂度是T(n)=T(n-1)+T(n-2)+1;故总的来说时间复杂度就是:
比较公式(1)和公式(2)可以发现在n>2的时候T(n)总是大于或者等于f(n)的,而 f(n)=15√xn2 f ( n ) = 1 5 x 2 n ,故递归法为一个指数阶的算法,即为O(n^2)级别。
递归法的空间复杂度的话,由于递归栈的使用,空间复杂度可以简单的看出来是O(n)级别。
使用数组
斐波那契数列中的每一项是前两项之和,如果记录前两项的值,只需要一次加法运算就可以得到当前项的值而不必每次都重新计算要求的当前项的前两项的值,从而减少了大量的计算。可以考虑使用数组记录前两项的值,具体实现代码如下:
int Fib4(int n){
if(n < 1)
return -1;
int a[n]; //c99只后支持使用变长数组 c99之前可以使用int *a = (int *)malloc(sizeof(int));
int i;
a[1] = 1;
a[2] = 1;
for(i = 3; i <= n; i++)
a[i] = a[i-1] + a[i-2];
return a[n];
}
算法的时间复杂度和空间复杂度都是O(n)。
使用迭代
上面的使用数组辅助计算斐波那契数列实现的算法其实相当于是存储了从第一项到第n项的斐波那契数列,其实我们除了第n项并不需要其他的数据,作为中间使用而最终不需要的数据,一直到函数执行完依旧存储在数组里面的行为明显是不智的,它造成了不必要的空间浪费。鉴于斐波那契数列中的每一项只是前两项之和,所以我们每次计算新的项时只需要它前面两个项的值即可,因此,我们可以使用对变量的迭代相加来实现整个计算步骤,实现代码如下:
int Fib5(int n){
if(n < 1)
return -1;
int s1,s2,i;
s1 = s2 = 1;
for(i = 3; i <= n; i++){
s2 = s1 + s2; //s2存储当前项的值
s1 = s2 - s1; //s1存储前一项的值
}
return s2;
}
如此,我们可以在保持时间复杂度为O(n)的基础上将空间复杂度降低到O(1)级别。
总结
综上四种解决办法,无疑直接使用通项公式进行计算是最快的,其次就是使用迭代或者数组的办法,使用递归则是最耗时的。但相对的,通项公式的方法要求相对较高的数学计算,这使得这种方式局限性很大,因为不是所有人都能根据一个递推关系计算出通项。总的来说,迭代的方法是编程解决该问题最优的解决方法,如果想不到,使用辅助数组进行计算也是相对较为优的办法,一般能够使用循环解决的问题不推荐采用递归的方式进行解决。