斐波拉契问题的求解
最近复习算法和数据结构,看到斐波拉契数列的求解。
突然间想到一年多前参加华为面试时,面试官提出的,上楼梯问题。
说是上楼梯时有两个选择,一次上一阶,一次上两阶,请问对于给定的正整数 N,有多少种上楼梯的方法。
说来惭愧,当时本人其实只是个小菜鸟,想用排列组合的方式解决它,花了挺久的时间,后来经过提醒,才发现想求得上N层阶梯的方法数量必须得到(N-1)层和(N-2)层的数量,然后二者相加,这是一个斐波拉契数列。
即 fib(N) = fib(N-1) + fib(N-2);/* N>=2, fib(0) = 0; fib(1) = 1; */
斐波拉契数的数学表示法:
fib(N)={N(N<=1)fib(N−1)+fib(N−2)(N>=2) fib(N)=\begin{cases} N & (N <= 1)\\ fib(N-1) + fib(N-2) &(N>=2)\ \end{cases}fib(N)={Nfib(N−1)+fib(N−2)(N<=1)(N>=2)
斐波拉契数的求法1:
根据斐波拉契数的定义和数学公式,可以轻松写出斐波拉契输的二分递归求解函数:
unsigned int fib_base(unsigned int n)
{
return (2 > n) ? n : fib_base(n - 1) + fib_base(n - 2);
}
实际上这种算法的效率及其低下,空间复杂度高,时间复杂度更是高达O(2N)O(2^N)O(2N)(其时间复杂度诸位可自行查资料计算),当我传入的参数为 1000 时,运行了接近 10 分钟依然没有答案,传递参数为 5000 时,直接栈溢出了,显然这种递归的效率是极其低下的,这样的算法甚至根本不能称之为算法。
斐波拉契数的求法2:
算法1 的好处在于直观,正确性一目了然,并且简洁自然。然而其效率实在太过低下,我们可以考虑改进一下,经过分析。我们可以得知这样的现象,求法1的效率低下的原因是大量的重复计算,例如 fib(4)=fib(3)+fib(2),fib(4) = fib(3) + fib(2),fib(4)=fib(3)+fib(2),于是根据方法1的递归,fib(3)=fib(2)+fib(1);fib(3) = fib(2) + fib(1);fib(3)=fib(2)+fib(1); fib(2)=fib(1)+fib(0);fib(2) = fib(1) + fib(0);fib(2)=fib(1)+fib(0); fib(1)=1;fib(1) = 1;fib(1)=1; fib(0)=0;fib(0) = 0;fib(0)=0; 此处,fib(2)fib(2)fib(2) 被重复求解了 111 次,fib(1)fib(1)fib(1) 被重复求解 222 次,随着 NNN 的增大,这种被重复求解的次数还要增长,这种增长也是指数级O(2n)O(2^n)O(2n)的。
我们可以考虑将已经求得的 fib(n)fib(n)fib(n) 用变量保存起来,等到下一次直接拿来用。
//由于得到 fib_opt(N - 1) 时需要得到 fib_opt(N - 2),因此设置变量将 fib_opt(N - 2)保存起来
unsigned int fib_opt(unsigned int n, unsigned int& preFib)
{
if (n == 0)
{
/************************************************************************************
* 为了解决 数列前两个数的问题,我们将 fib_opt(-1) 的值设为 1,然后直接返回 0,
* 这样就可以满足 fib_opt(0) = 0; fib_opt(1) = fib_opt(1-1) + fib_opt(1 - 2) = 1 了。
************************************************************************************/
preFib = 1;
return 0;
}
else
{
unsigned int prePreFib;
preFib = fib_opt(n - 1, prePreFib);
return preFib + prePreFib;
}//用辅助变量记录前一项,然后返回当前项
}
由于方法1中的 fib(N−2)fib(N - 2)fib(N−2) 的另一次递归在这里被省掉了,此函数的递归为线性递归模式,可以直观得到其时间复杂度与传入的参数 NNN 成线性相关,因此时间复杂度为 O(N)O(N)O(N)。遗憾的是,该函数的空间复杂度为 O(N)O(N)O(N),并且全部占用的是栈空间,查阅资料得知,Windows 32 位机的栈空间大小为 1M,Linux 为8M,可自定义大小。但是无论如何,方法2的算法中,数据上升到一定规模(这个规模并不会很大)后,栈空间依旧无法满足算法所需。事实上,当我传入的数据为 5000 时,直接栈溢出了。
斐波拉契数的求法3:
通过上面的讨论,我门可以得知,递归虽然方便且直观,但是存在很大的限制。因此就我个人目前的习惯和结论,轻易不要使用递归。事实上,我们可以考虑将其改为非递归的形式,比如我们可以将其改为递推算法,即从 fib(0)开始,求解 fib(1), fib(2), … 直到 fib(N),代码如下:
unsigned int fib_rec(unsigned int n)
{
//初始化 fib_rec(0)=0; fib_rec(1)=1;
unsigned int f = 0;
unsigned int g = 1;
while (n-- > 0)
{
g += f;
f = g - f;//用 f 表示当前的 斐波拉契数
}
return f;
}
如果代码一时之间没看懂,可以自己先写成判断 N==0N == 0N==0 和 N==1N == 1N==1 以及 N>=2N >= 2N>=2 三种情况,然后逐步修改即可得到上述精简代码。虽说不敢肯定这是最优算法,但是比起前两种算法,这种算法的优点不言而喻,线性的时间复杂度,空间复杂度为 O(1)O(1)O(1),执行起来时间不长,而且也不会造成栈溢出。
总结
在设计算法时,我们应该尽量避免递归,但是可以先用递归得到最简洁明了的算法,让问题更清晰,然后尽量将其改成非递归的形式。
本文探讨了斐波拉契数列的递归求解方法,包括直接递归、存储已计算结果的递归以及递推算法。指出递归在效率上的不足,如空间复杂度高、时间复杂度大,可能导致栈溢出,并提倡将递归转换为非递归形式以提高效率。
309

被折叠的 条评论
为什么被折叠?



