求職乾貨:再也不怕面試官問斐波那契數列了! - CSDN

2021-01-22 CSDN

作者 | 守望

責編 | 胡巍巍

前言

假如面試官讓你編寫求斐波那契數列的代碼時,是不是心中暗喜?不就是遞歸麼,早就會了。如果真這麼想,那就危險了。

遞歸解法

遞歸,在數學與計算機科學中,是指在函數的定義中使用函數自身的方法。

斐波那契數列的計算表達式很簡單:

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

因此,我們能很快根據表達式寫出遞歸版的代碼:

/*fibo.c*/#include<stdio.h>#include<stdlib.h>/*求斐波那契數列遞歸版*/unsignedlongfibo(unsignedlongint n){if(n <= 1)return n;elsereturn fibo(n-1) + fibo(n-2);}intmain(int argc,char *argv[]){if(1 >= argc){printf("usage:./fibo num\n");return-1; }unsignedlong n = atoi(argv[1]);unsignedlong fiboNum = fibo(n);printf("the %lu result is %lu\n",n,fiboNum);return0;}

關鍵代碼只有4行。簡潔明了,一氣呵成。

編譯:

gcc-ofibofibo.c

運行計算第5個斐波那契數:

$ time ./fibo 5the 5 result is 5real0m0.001suser 0m0.001ssys 0m0.000s

看起來並沒有什麼不妥,運行時間也很短。

繼續計算第50個斐波那契數列:

$ time ./fibo 50the 50 result is 12586269025real1m41.655suser 1m41.524ssys 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 -s8192

可以看到,默認棧空間大小只有8M。

一般來說,8M的棧空間對於一般程序完全足夠。如果8M的棧空間不夠使用,那麼就需要重新審視你的代碼設計了。

遞歸改進版

既然我們知道最初版本的遞歸存在大量的重複計算,那麼我們完全可以考慮將已經計算的值保存起來,從而避免重複計算,該版本代碼實現如下:

/*fibo0.c*/#include<stdio.h>#include<stdlib.h>/*求斐波那契數列,避免重複計算版本*/unsignedlongfiboProcess(unsignedlong *array,unsignedlong n){if(n < 2)return n;else{/*遞歸保存值*/array[n] = fiboProcess(array,n-1) + array[n-2];returnarray[n]; }}unsignedlongfibo(unsignedlong n){if(n <= 1)return n;unsignedlong ret = 0;/*申請數組用於保存已經計算過的內容*/unsignedlong *array = (unsignedlong*)calloc(n+1,sizeof(unsignedlong));if(NULL == array) {return-1; }array[1] = 1; ret = fiboProcess(array,n);free(array);array = NULL;return ret;}/**main函數部分與fibo.c相同,這裡省略*/

效率如何呢?

$ gcc -o fibo0 fibo0.c$ time ./fibo0 50the 50 result is 12586269025real0m0.002suser 0m0.000ssys 0m0.002s

可見其效率還是不錯的,時間複雜度為O(n)。

但是特別注意的是,這種改進版的遞歸,雖然避免了重複計算,但是調用鏈仍然比較長。

迭代解法

既然遞歸法不夠優雅,我們換一種方法。如果不用計算機計算,讓你去算第n個斐波那契數,你會怎麼做呢?

我想最簡單直接的方法應該是:知道第一個和第二個後,計算第三個;知道第二個和第三個後,計算第四個,以此類推。

最終可以得到我們需要的結果。這種思路,沒有冗餘的計算。基於這個思路,我們的C語言實現如下:

/*fibo1.c*/#include<stdio.h>#include<stdlib.h>/*求斐波那契數列迭代版*/unsignedlongfibo(unsignedlong n){unsignedlong preVal = 1;unsignedlong prePreVal = 0;if(n <= 2)return n;unsignedlong loop = 1;unsignedlong 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 50the 50 result is 12586269025real0m0.002suser 0m0.000ssys 0m0.002s

可以看到,計算第50個斐波那契數只需要0.002s!時間複雜度為O(n)。

尾遞歸解法

同樣的思路,但是採用尾遞歸的方法來計算。

要計算第n個斐波那契數,我們可以先計算第一個,第二個,如果未達到n,則繼續遞歸計算,尾遞歸C語言實現如下:

/*fibo2.c*/#include<stdio.h>#include<stdlib.h>/*求斐波那契數列尾遞歸版*/unsignedlongfiboProcess(unsignedlong n,unsignedlong prePreVal,unsignedlong preVal,unsignedlong begin){/*如果已經計算到我們需要計算的,則返回*/if(n == begin)return preVal+prePreVal;else{ begin++;return fiboProcess(n,preVal,prePreVal+preVal,begin); }}unsignedlongfibo(unsignedlong n){if(n <= 1)return n;elsereturn fiboProcess(n,0,1,2);}/**main函數部分與fibo.c相同,這裡省略*/

效率如何呢?

$ gcc -o fibo2 fibo2.c$ time ./fibo2 50the 50 result is 12586269025real0m0.002suser 0m0.001ssys 0m0.002s

可見,其效率並不遜於迭代法。尾遞歸在函數返回之前的最後一個操作仍然是遞歸調用。

尾遞歸的好處是,進入下一個函數之前,已經獲得了當前函數的結果,因此不需要保留當前函數的環境,內存佔用自然也是比最開始提到的遞歸要小。時間複雜度為O(n)。

矩陣快速冪解法

這是一種高效的解法,需要推導,對此不感興趣的可直接看最終推導結果。

下面的式子成立是顯而易見的,不多做解釋。

如果a為矩陣,等式同樣成立,後面我們會用到它。假設有矩陣2*2矩陣A,滿足下面的等式:

可以得到矩陣A:

因此也就可以得到下面的矩陣等式:

再進行變換如下:

以此類推,得到:

實際上f(n)就是矩A^(n-1)中的A[0][0],或者是矩A^n中的A[0][1]。

那麼現在的問題就歸結為,如何求A^n,其中A為2*2的矩陣。

根據我們最開始的公式,很容易就有思路,代碼實現如下:

/*fibo3.c*/#include <stdio.h>#include <stdlib.h>#include <string.h>#define MAX_COL 2#define MAX_ROW 2typedef unsigned long MatrixType;/*計算2*2矩陣乘法,這裡沒有寫成通用形式,有興趣的可以自己實現通用矩陣乘法*/int matrixDot(MatrixType A[MAX_ROW][MAX_COL],MatrixType B[MAX_ROW][MAX_COL],MatrixType C[MAX_ROW][MAX_COL]){/*C為返回結果,由於A可能和C相同,因此使用臨時矩陣存儲*/ MatrixType tempMa[MAX_ROW][MAX_COL] ; memset(tempMa,0,sizeof(tempMa)); /*這裡簡便處理*/ tempMa[0][0] = A[0][0] * B[0][0] + A[0][1] * B [1][0]; tempMa[0][1] = A[0][0] * B[0][1] + A[0][1] * B [1][1]; tempMa[1][0] = A[1][0] * B[0][0] + A[1][1] * B [1][0]; tempMa[1][1] = A[1][0] * B[0][1] + A[1][1] * B [1][1]; memcpy(C,tempMa,sizeof(tempMa)); return 0;}MatrixType fibo(int n){ if(n <=1)returnn;MatrixTyperesult[][MAX_COL] = {1,0,0,1};MatrixTypeA[][2] = {1,1,1,0};while (n > 0) { /*判斷最後一位是否為1,即可知奇偶*/ if (n&1) { matrixDot(result,A,result); } n /= 2; matrixDot(A,A,A); } return result[0][1];}/**main函數部分與fibo.c相同,這裡省略*/

該算法的關鍵部分在於對A^n的計算,它利用了我們開始提到的等式,對奇數和偶數分別處理。

假設n為9,初始矩陣為INIT則計算過程如下:

9為奇數,則計算INIT*A,隨後A變為A*A,n變為9/2,即為44為偶數,則結果仍為INIT*A,隨後A變為

,n變為4/2,即22為偶數,則結果仍未INIT*A,隨後變A變為

,n變為2/2,即11為奇數,則結果為INIT*(A^8)*A可以看到,計算次數類似與二分查找次數,其時間複雜度為O(logn)。運行試試看:

$ gcc -o fibo3 fibo3.c$ time ./fibo3 50the 50 result is 12586269025real0m0.002suser 0m0.002ssys 0m0.000s

通項公式解法

斐波那契數列的通項公式為:

關於通項公式的求解,可以當成一道高考數列大題,有興趣的可以嘗試一下(提示:兩次構造等比數列)。C語言代碼實現如下:

/*fibo4.c*/#include<stdio.h>#include<stdlib.h>#include<math.h>unsignedlongfibo(unsignedlong n){if(n <=1 )return n;return (unsignedlong)((pow((1+sqrt(5))/2,n)-pow((1-sqrt(5))/2,n))/sqrt(5));}/**main函數部分與fibo.c相同,這裡省略*/

來看一下效率:

$ gcc -o fibo4 fibo4.c -lm$ time ./fibo4the 50 result is 12586269025real0m0.002suser 0m0.002ssys 0m0.000s

計算第50個,速度還不錯。

列表法

如果需要求解的斐波那契數列的第n個在有限範圍內,那麼完全可以將已知的斐波那契數列存儲起來,在需要的時候讀取即可,時間複雜度可以為O(1)。

斐波那契數列應用

關於斐波那契數列在實際中很常見,數學上也有很多奇特的性質,有興趣的可在百科中查看。

總結

總結一下遞歸的優缺點:優點:

實現簡單可讀性好缺點:

遞歸調用,佔用空間大遞歸太深,易發生棧溢出可能存在重複計算可以看到,對於求斐波那契數列的問題,使用一般的遞歸併不是一種很好的解法。所以,當你使用遞歸方式實現一個功能之前,考慮一下使用遞歸帶來的好處是否抵得上它的代價。

篇幅有限,不在此介紹,更多使用方法可以通過man命令名的方式去了解。

作者簡介:守望,一名好文學,好技術的開發者。在個人公眾號【編程珠璣】分享原創技術文章和學習資源,期待一起交流學習。聲明:本文為作者投稿,版權歸作者個人所有。

【End】

相關焦點

  • python實戰14遞歸算法實現斐波那契數列(BAT面試題)
    斐波那契數列簡介斐波那契數列(Fibonacci sequence),又稱黃金分割數列、因數學家萊昂納多·斐波那契(Leonardoda Fibonacci)以兔子繁殖為例子而引入,故又稱為「兔子數列」,指的是這樣一個數列:0、1、1、2、3
  • 面試官》朝陽產業求職難度大?面試官現場教你專業技巧
    面試官》節目中三位求職者都與這個行業有著或多或少的聯繫,第二現場更請來了專業的面試官為大家帶來做自媒體的絕對乾貨。  第一位求職者郭煜,來自陝西西安,畢業於北京現代音樂學院音樂劇專業,求職方向帶貨主播。 求職者一上場,就讓主持人大吃一驚,原來兩個人曾經在一個主播類節目中相識,沒想到會在求職類節目再度相遇,也讓大家更加好奇求職者為何放棄已經不錯的工作重新來求職。
  • 最美的數列-斐波那契數列
    今天跟大家一起分享一下斐波那契數列。斐波那契數列簡介斐波那契數列, 就是由這位義大利著名數學家萊昂納多·斐波那契在《計算之書》中以兔子繁殖為例子而提出的數列,故又稱為「兔子數列」。斐波那契數列:1、1、2、3、5、8、13、21、34、56……這個數列的特點是從第3項開始,每一項都是前兩項的和。例如 3=2+1,5=3+2,8=5+3等。省略號後面有無數項。斐波那契數列美在哪裡呢?
  • Fibonacci 斐波那契數列的幾種寫法、時間複雜度對比
    我看在家修煉編程技術是不錯的選擇,「用Python實現斐波那契數列」是我們在知識星球中每周給大家安排的一道題,你也可以先思考一下有哪些實現方法,說不定哪天面試就能派上用場,終有一日當上CTO贏取白富美從此走上人生巔峰。
  • 朝陽產業求職難度大?面試官現場教你專業技巧
    面試官》節目中三位求職者都與這個行業有著或多或少的聯繫,第二現場更請來了專業的面試官為大家帶來做自媒體的絕對乾貨!趕緊跟著小編一起來看看吧!求職者現場帶貨活力四射竟引得面試官爭執不下第一位求職者郭煜,來自陝西西安,畢業於北京現代音樂學院音樂劇專業,求職方向【帶貨主播】
  • 小學生也可以寫出的,複雜數列-斐波那契數列
    有一個數列與黃金分割比就有著不可名狀的關係,而且它的特性非常的多,關鍵這個數列還非常簡單,小學生都可以寫出來。今天我們就來著重講一下這個數列,斐波那契數列的4個特性。1.數列前兩項之和等於第三項如果設F(n)為該數列的第n項(n∈N*),那麼這句話可以寫成如下形式::F(n)=F(n-1)+F(n-2)以上就是斐波那契數列的官方定義。其實只要把數列擺出來,我們就可以很明確的看到特性了。1,1,2,3,5,8,13,21,34...
  • 斐波那契數列
    斐波那契數列的增長速度非常快,像這個數:4109266378488062431228061757602275200488546350691404731331209059476699865525985814512330794573159713192993537023560937664480427471312780415869653296
  • 奇妙的斐波那契數列
    義大利的數學家列昂那多·斐波那契在1202年研究兔子產崽問題時發現了此數列。斐波那契在《計算之書》中提出了一個有趣的兔子問題:假設一對初生兔子要一個月才到成熟期,而一對成熟兔子每月會生一對兔子,那麼,由一對小兔子開始,12個月後會有多少對兔子呢?兔子繁殖的過程可以通過一棵「家族樹」來表示,如圖所示。
  • 斐波那契數列與自然
    斐波那契數列與自然   斐波那契數列在自然界中的出現是如此地頻繁,人們深信這不是偶然的..葉子在一個循回中旋轉的圈數也是斐波那契數.在一個循回中葉子數與葉子旋轉圈數的比稱為葉序(源自希臘詞,意即葉子的排列)比.多數的葉序比呈現為斐波那契數的比.
  • 交易玄學:斐波那契數列
    斐波那契數列。  斐波那契數列是什麼呢?  1202年,那時印刷術還沒有出現,斐波那契的書《算盤書》在義大利出版,受到當時羅馬君主的支持,而得以幸運地傳播開來。《算盤書》將阿拉伯世界的阿拉伯數字引進了當時的西方,其中一篇短文最為著名。
  • 斐波那契數列有多神奇?
    1202 年,生於義大利比薩的數學家 萊昂納多·斐波那契 完成了他的傳世名著《算盤書》,書中對一個有趣的 「兔子繁殖問題」 進行了研究,斐波那契數列便由此而來。斐波那契 首先加以研究的,後人就將其稱為 斐波那契數列。
  • python邏輯控制總結——斐波那契數列
    斐波那契數列斐波那契數列在數學上,是一個特殊的數列,它的特徵如下:1. 第一項和第二項均為1。2. 從第三項開始,每一項均是前面兩項的和。如下:我們嘗試在控制臺輸出一個斐波那契數列的前10項。新建一個test1.py,代碼如下:
  • 斐波那契數列實戰解析
    斐波那契數列在實際操作過程中有兩個重要意義:第一個實戰意義在於數列本身。使用斐波那契數列時可以由市場中某個重要的階段變盤點向未來市場趨勢進行推演,到達時間窗口時市場發生方向變化的概率較大。案例如圖,近期上證指數的實時走勢,就是很經典的斐波那契數列周期第二個實戰意義在於本數列的衍生數字是市場中縱向時間周期計算未來市場變盤時間的理論基礎。
  • 王者技術之斐波那契數列
    「斐波那契數列(Fibonacci)」的發明者,是義大利數學家列昂納多·斐波那契(Leonardo
  • 斐波那契數列在自然界中如何表達
    這個數列被稱為斐波那契數列。數字之間的比率(1.618034)經常被稱為黃金比率或黃金數字。乍一看,斐波那契的實驗似乎提供了超出世界範圍的投機性兔子繁殖。但是這個序列經常出現在自然世界中,這一事實幾個世紀以來一直吸引著科學家。這些迷人的數字是如何在自然界中表達的呢?
  • 面試官總說:回去等消息,就再也沒有下文了,為什麼?
    01、一面試,就讓等消息,這是常規操作?年前我一朋友小張的公司因為出現了巨額虧損,根本沒辦法給員工發出工資,於是他們使出了殺手鐧——裁員。小張就是其中被裁的一個人,沒辦法,他只好再次踏上了漫長的求職道路。這麼一來二去折騰了好幾個月,一直也沒有找到讓自己滿意的工作。那些看不上的工作吧,積極地讓他去上班,可是那些自己中意的呢?
  • 斐波那契數列——隱藏在自然界的數學美
    即為「斐波那契數列(Fibonacci sequence)」1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……斐波那契數列中的任一個數,都叫斐波那契數斐波那契數是大自然的一個基本模式只要我們認真觀察斐波那契數存在於自然界的萬物中
  • 生命曲線與神聖的斐波那契數列
    同樣讓他感到驚訝的是,最讓世人津津樂道是以他命名的這個斐波那契數列:0,1,1,2,3,5,8,13……,而並非本人更偉大的數學成就——將阿拉伯數字和乘數的位值表示法系統引入了歐洲。斐波那契為這些商人編寫了《計算之書》,其中就涉及到大量的實際問題,並舉例說明,與笨拙的羅馬數字相比,這套新的數字系統可以多麼簡單、高效地進行商業和數學計算。透過斐波那契的這本書將十進位數字影響傳播開來是他最偉大的數學成就。然而,本人卻是因為《計算之書》中列舉的斐波那契數列被世人所熟知的。
  • 面試必問6大問題,自我介紹和職業規劃這樣回答,面試官滿意錄取
    這6大面試官必問面試問題,這樣回答可以拿到一份滿意的offer: 1、自我介紹 面試官早就聽膩了大家一樣格式的自我介紹,你不妨換一種模式,比如在介紹完自己的硬性情況(姓名、畢業院校、專業等等),用貼標籤的形式介紹自己,提前給自己準備3個符合自己卻又特別的標籤,不僅能引起面試官的好奇,也能讓面試官更好的記住你!
  • 面試官問:「你的興趣愛好是什麼?」這樣回答,面試官最滿意
    1 面試官為什麼要問這道題?面試官時間是很寶貴的,在短短一個小時之內,需要對候選人深入的了解,所以問任何問題都是有目的性的,那面試官問:「你的興趣愛好是什麼?」有什麼目的呢?1) 為了緩和氣氛,讓你為放鬆下來面試的時候,面試官和候選人一直在交鋒,如果一直氣氛很嚴肅,怕候選人進展,進而發揮不好,於是中間會穿插一些輕鬆的話題。