面试官问你斐波那契数列的时候不要高兴得太早

前言

假如面试官让你编写求斐波那契数列的代码时,是不是心中暗喜?不就是递归么,早就会了。如果真这么想,那就危险了。

递归求斐波那契数列

递归,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。
斐波那契数列的计算表达式很简单:

F(n) = n; n = 0,1
F(n) = F(n-1) + F(n-2),n >= 2;

因此,我们能很快根据表达式写出递归版的代码:

/*fibo.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契数列递归版*/
unsigned long fibo(unsigned long int n)
{
    if(n <= 1)
        return n;
    else 
        return fibo(n-1) + fibo(n-2);
}
int main(int argc,char *argv[])
{
    if(1 >= argc)
    {
       printf("usage:./fibo num\n");
       return -1;
    }
    unsigned long  n = atoi(argv[1]);
    unsigned long  fiboNum = fibo(n);
    printf("the %lu result is %lu\n",n,fiboNum);
    return 0;
}

关键代码为3~9行。简洁明了,一气呵成。
编译:

gcc -o fibo fibo.c

运行计算第5个斐波那契数:

$ time ./fibo 5
the 5 result is 5

real    0m0.001s
user    0m0.001s
sys 0m0.000s

看起来并没有什么不妥,运行时间也很短。
继续计算第50个斐波那契数列:

$ time ./fibo 50
the 50 result is 12586269025

real    1m41.655s
user    1m41.524s
sys 0m0.076s

计算第50个斐波那契数的时候,竟然花了一分多钟!

递归分析

为什么计算第50个的时候竟然需要1分多钟。我们仔细分析我们的递归算法,就会发现问题,当我们计算fibo(5)的时候,是下面这样的:

                         |--F(1)
                  |--F(2)|
           |--F(3)|      |--F(0)
           |      |
    |--F(4)|      |--F(1)
    |      |      
    |      |      |--F(1)
    |      |--F(2)|
    |             |--F(0)
F(5)|             
    |             |--F(1)
    |      |--F(2)|
    |      |      |--F(0)
    |--F(3)|
           |
           |--F(1)

为了计算fibo(5),需要计算fibo(3),fibo(4);而为了计算fibo(4),需要计算fibo(2),fibo(3)......最终为了得到fibo(5)的结果,fibo(0)被计算了3次,fibo(1)被计算了5次,fibo(2)被计算了2次。可以看到,它的计算次数几乎是指数级的!

因此,虽然递归算法简洁,但是在这个问题中,它的时间复杂度却是难以接受的。除此之外,递归函数调用的越来越深,它们在不断入栈却迟迟不出栈,空间需求越来越大,虽然访问速度高,但大小是有限的,最终可能导致栈溢出
在linux中,我们可以通过下面的命令查看栈空间的软限制:

$ ulimit -s
8192

可以看到,默认栈空间大小只有8M。一般来说,8M的栈空间对于一般程序完全足够。如果8M的栈空间不够使用,那么就需要重新审视你的代码设计了。

迭代解法

既然递归法不够优雅,我们换一种方法。如果不用计算机计算,让你去算第n个斐波那契数,你会怎么做呢?我想最简单直接的方法应该是:知道第一个和第二个后,计算第三个;知道第二个和第三个后,计算第四个,以此类推。最终可以得到我们需要的结果。这种思路,没有冗余的计算。基于这个思路,我们的C语言实现如下:

/*fibo1.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契数列迭代版*/
unsigned long  fibo(unsigned long  n)
{
    unsigned long  preVal = 1;
    unsigned long  prePreVal = 0;
    if(n <= 2)
        return n;
    unsigned long  loop = 1;
    unsigned long  returnVal = 0;
    while(loop < n)
    {
        returnVal = preVal +prePreVal;
        /*更新记录结果*/
        prePreVal = preVal;
        preVal = returnVal;
        loop++;
    }
    return returnVal;
}
/**main函数部分与fibo.c相同,这里省略*/

编译并计算第50个斐波那契数:

$ gcc -o fibo1 fibo1.c
$ time ./fibo1 50
the 50 result is 12586269025

real    0m0.002s
user    0m0.001s
sys 0m0.002s

可以看到,计算第50个斐波那契数只需要0.002s!时间复杂度为O(n)。

尾递归解法

同样的思路,但是采用尾递归的方法来计算。要计算第n个斐波那契数,我们可以先计算第一个,第二个,如果未达到n,则继续递归计算,尾递归C语言实现如下:

/*fibo2.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契数列尾递归版*/
unsigned long fiboProcess(unsigned long n,unsigned long  prePreVal,unsigned long  preVal,unsigned long begin)
{
    /*如果已经计算到我们需要计算的,则返回*/
    if(n == begin)
        return preVal+prePreVal;
    else
    {
        begin++;
        return fiboProcess(n,preVal,prePreVal+preVal,begin);
    }
}

unsigned long  fibo(unsigned long  n)
{
    if(n <= 1)
        return n;
    else 
        return fiboProcess(n,0,1,2);
}

/**main函数部分与fibo.c相同,这里省略*/

效率如何呢?

$ gcc -o fibo2 fibo2.c
$ time ./fibo2 50
the 50 result is 12586269025

real    0m0.002s
user    0m0.001s
sys 0m0.002s

可见,其效率并不逊于迭代法。尾递归在函数返回之前的最后一个操作仍然是递归调用。尾递归的好处是,进入下一个函数之前,已经获得了当前函数的结果,因此不需要保留当前函数的环境,内存占用自然也是比最开始提到的递归要小。时间复杂度为O(n)。

递归改进版

既然我们知道最初版本的递归存在大量的重复计算,那么我们完全可以考虑将已经计算的值保存起来,从而避免重复计算,该版本代码实现如下:

/*fibo3.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契数列,避免重复计算版本*/
unsigned long fiboProcess(unsigned long *array,unsigned long n)
{
    if(n < 2)
        return n;
    else
    {
        /*递归保存值*/
        array[n] = fiboProcess(array,n-1) + array[n-2];
        return array[n];
    }
}

unsigned long  fibo(unsigned long  n)
{
    if(n <= 1)
        return n;
    unsigned long ret = 0;
    /*申请数组用于保存已经计算过的内容*/
    unsigned long *array = (unsigned long*)calloc(n+1,sizeof(unsigned long));
    if(NULL == array)
    {
        return -1;
    }
    array[1] = 1;
    ret = fiboProcess(array,n);
    free(array);
    array = NULL;
    return ret;
}
/**main函数部分与fibo.c相同,这里省略*/

效率如何呢?

$ gcc -o fibo3 fibo3.c
$ time ./fibo3 50
the 50 result is 12586269025

real    0m0.002s
user    0m0.002s
sys 0m0.001s

可见效率是不逊于其他两种优化算法的。但是特别注意的是,这种改进版的递归,虽然避免了重复计算,但是调用链仍然比较长。

其他解法

其他两种时间复杂度为O(logn)的矩阵解法以及O(1)的通项表达式解法本文不介绍。欢迎留言补充。

总结

总结一下递归的优缺点:
优点:

  • 实现简单
  • 可读性好

缺点:

  • 递归调用,占用空间大
  • 递归太深,易发生栈溢出
  • 可能存在重复计算

可以看到,对于求斐波那契数列的问题,使用一般的递归并不是一种很好的解法。
所以,当你使用递归方式实现一个功能之前,考虑一下使用递归带来的好处是否抵得上它的代价。

更多解法请参考 https://www.yanbinghu.com/2019/01/07/16863.html

微信公众号【编程珠玑】:专注但不限于分享计算机编程基础,Linux,C语言,C++,算法,数据库等编程相关[原创]技术文章,号内包含大量经典电子书和视频学习资源。欢迎一起交流学习,一起修炼计算机“内功”,知其然,更知其所以然。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容