面試官問你斐波那契數(shù)列的時候不要高興得太早

前言

假如面試官讓你編寫求斐波那契數(shù)列的代碼時毅往,是不是心中暗喜?不就是遞歸么攀唯,早就會了。如果真這么想侯嘀,那就危險了戒幔。

遞歸求斐波那契數(shù)列

遞歸,在數(shù)學與計算機科學中工坊,是指在函數(shù)的定義中使用函數(shù)自身的方法。
斐波那契數(shù)列的計算表達式很簡單:

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

因此罢吃,我們能很快根據(jù)表達式寫出遞歸版的代碼:

/*fibo.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契數(shù)列遞歸版*/
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個斐波那契數(shù):

$ time ./fibo 5
the 5 result is 5

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

看起來并沒有什么不妥吁伺,運行時間也很短租谈。
繼續(xù)計算第50個斐波那契數(shù)列:

$ time ./fibo 50
the 50 result is 12586269025

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

計算第50個斐波那契數(shù)的時候割去,竟然花了一分多鐘!

遞歸分析

為什么計算第50個的時候竟然需要1分多鐘夸赫。我們仔細分析我們的遞歸算法咖城,就會發(fā)現(xiàn)問題,當我們計算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次≈椋可以看到恬惯,它的計算次數(shù)幾乎是指數(shù)級的!

因此浓恳,雖然遞歸算法簡潔颈将,但是在這個問題中言疗,它的時間復雜度卻是難以接受的。除此之外疑务,遞歸函數(shù)調(diào)用的越來越深梗醇,它們在不斷入棧卻遲遲不出棧叙谨,空間需求越來越大,雖然訪問速度高涤垫,但大小是有限的竟终,最終可能導致棧溢出
在linux中榆芦,我們可以通過下面的命令查看棧空間的軟限制:

$ ulimit -s
8192

可以看到驻右,默認椘榇荆空間大小只有8M拣凹。一般來說,8M的椶置兀空間對于一般程序完全足夠祈惶。如果8M的椗跚耄空間不夠使用,那么就需要重新審視你的代碼設計了疹蛉。

迭代解法

既然遞歸法不夠優(yōu)雅可款,我們換一種方法。如果不用計算機計算筋讨,讓你去算第n個斐波那契數(shù)摸恍,你會怎么做呢立镶?我想最簡單直接的方法應該是:知道第一個和第二個后,計算第三個嗜逻;知道第二個和第三個后缭召,計算第四個,以此類推妨蛹。最終可以得到我們需要的結果蛙卤。這種思路噩死,沒有冗余的計算已维。基于這個思路栅屏,我們的C語言實現(xiàn)如下:

/*fibo1.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契數(shù)列迭代版*/
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函數(shù)部分與fibo.c相同堂鲜,這里省略*/

編譯并計算第50個斐波那契數(shù):

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

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

可以看到缔莲,計算第50個斐波那契數(shù)只需要0.002s!時間復雜度為O(n)蛀骇。

尾遞歸解法

同樣的思路读拆,但是采用尾遞歸的方法來計算建椰。要計算第n個斐波那契數(shù),我們可以先計算第一個屠列,第二個伞矩,如果未達到n乃坤,則繼續(xù)遞歸計算沟蔑,尾遞歸C語言實現(xiàn)如下:

/*fibo2.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契數(shù)列尾遞歸版*/
unsigned long fiboProcess(unsigned long n,unsigned long  prePreVal,unsigned long  preVal,unsigned long begin)
{
    /*如果已經(jīng)計算到我們需要計算的狱杰,則返回*/
    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函數(shù)部分與fibo.c相同瘦材,這里省略*/

效率如何呢?

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

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

可見仿畸,其效率并不遜于迭代法食棕。尾遞歸在函數(shù)返回之前的最后一個操作仍然是遞歸調(diào)用。尾遞歸的好處是错沽,進入下一個函數(shù)之前簿晓,已經(jīng)獲得了當前函數(shù)的結果,因此不需要保留當前函數(shù)的環(huán)境千埃,內(nèi)存占用自然也是比最開始提到的遞歸要小憔儿。時間復雜度為O(n)。

遞歸改進版

既然我們知道最初版本的遞歸存在大量的重復計算放可,那么我們完全可以考慮將已經(jīng)計算的值保存起來,從而避免重復計算吴侦,該版本代碼實現(xiàn)如下:

/*fibo3.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契數(shù)列屋休,避免重復計算版本*/
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;
    /*申請數(shù)組用于保存已經(jīng)計算過的內(nèi)容*/
    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函數(shù)部分與fibo.c相同,這里省略*/

效率如何呢备韧?

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

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

可見效率是不遜于其他兩種優(yōu)化算法的劫樟。但是特別注意的是,這種改進版的遞歸织堂,雖然避免了重復計算叠艳,但是調(diào)用鏈仍然比較長。

其他解法

其他兩種時間復雜度為O(logn)的矩陣解法以及O(1)的通項表達式解法本文不介紹易阳。歡迎留言補充附较。

總結

總結一下遞歸的優(yōu)缺點:
優(yōu)點:

  • 實現(xiàn)簡單
  • 可讀性好

缺點:

  • 遞歸調(diào)用,占用空間大
  • 遞歸太深潦俺,易發(fā)生棧溢出
  • 可能存在重復計算

可以看到拒课,對于求斐波那契數(shù)列的問題,使用一般的遞歸并不是一種很好的解法事示。
所以早像,當你使用遞歸方式實現(xiàn)一個功能之前,考慮一下使用遞歸帶來的好處是否抵得上它的代價肖爵。

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

微信公眾號【編程珠璣】:專注但不限于分享計算機編程基礎卢鹦,Linux,C語言劝堪,C++冀自,算法揉稚,數(shù)據(jù)庫等編程相關[原創(chuàng)]技術文章,號內(nèi)包含大量經(jīng)典電子書和視頻學習資源熬粗。歡迎一起交流學習搀玖,一起修煉計算機“內(nèi)功”,知其然荐糜,更知其所以然巷怜。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市暴氏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绣张,老刑警劉巖答渔,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異侥涵,居然都是意外死亡沼撕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門芜飘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來务豺,“玉大人,你說我怎么就攤上這事嗦明×ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵娶牌,是天一觀的道長奔浅。 經(jīng)常有香客問我,道長诗良,這世上最難降的妖魔是什么汹桦? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮鉴裹,結果婚禮上舞骆,老公的妹妹穿的比我還像新娘。我一直安慰自己径荔,他們只是感情好督禽,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著猖凛,像睡著了一般赂蠢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上辨泳,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天虱岂,我揣著相機與錄音玖院,去河邊找鬼。 笑死第岖,一個胖子當著我的面吹牛难菌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蔑滓,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼郊酒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了键袱?” 一聲冷哼從身側響起燎窘,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蹄咖,沒想到半個月后褐健,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡澜汤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年蚜迅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片俊抵。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡谁不,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出徽诲,到底是詐尸還是另有隱情刹帕,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布馏段,位于F島的核電站轩拨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏院喜。R本人自食惡果不足惜亡蓉,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望喷舀。 院中可真熱鬧砍濒,春花似錦、人聲如沸硫麻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拿愧。三九已至杠河,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背券敌。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工唾戚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人待诅。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓叹坦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親卑雁。 傳聞我的和親對象是個殘疾皇子募书,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容