由斐波拉契问题看递归算法

本文探讨了斐波拉契数列的递归求解方法,包括直接递归、存储已计算结果的递归以及递推算法。指出递归在效率上的不足,如空间复杂度高、时间复杂度大,可能导致栈溢出,并提倡将递归转换为非递归形式以提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

斐波拉契问题的求解

       最近复习算法和数据结构,看到斐波拉契数列的求解。
       突然间想到一年多前参加华为面试时,面试官提出的,上楼梯问题。
       说是上楼梯时有两个选择,一次上一阶,一次上两阶,请问对于给定的正整数 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(N1)+fib(N2)(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(N2) 的另一次递归在这里被省掉了,此函数的递归为线性递归模式,可以直观得到其时间复杂度与传入的参数 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==0N==1N == 1N==1 以及 N>=2N >= 2N>=2 三种情况,然后逐步修改即可得到上述精简代码。虽说不敢肯定这是最优算法,但是比起前两种算法,这种算法的优点不言而喻,线性的时间复杂度,空间复杂度为 O(1)O(1)O(1),执行起来时间不长,而且也不会造成栈溢出。

总结

       在设计算法时,我们应该尽量避免递归,但是可以先用递归得到最简洁明了的算法,让问题更清晰,然后尽量将其改成非递归的形式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值