面試官問你斐波那契數列的時候不要高興得太早
前言
假如面試官讓你編寫求斐波那契數列的程式碼時,是不是心中暗喜?不就是遞迴麼,早就會了。如果真這麼想,那就危險了。
遞迴求斐波那契數列
遞迴,在數學與電腦科學中,是指在函式的定義中使用函式自身的方法。
斐波那契數列的計算表示式很簡單:
1F(n) = n; n = 0,1
2F(n) = F(n-1) + F(n-2),n >= 2;
因此,我們能很快根據表示式寫出遞迴版的程式碼:
1/*fibo.c*/
2#include <stdio.h>
3#include <stdlib.h>
4/*求斐波那契數列遞迴版*/
5unsigned long fibo(unsigned long int n)
6{
7 if(n <= 1)
8 return n;
9 else
10 return fibo(n-1) + fibo(n-2);
11}
12int main(int argc,char *argv[])
13{
14 if(1 >= argc)
15 {
16 printf("usage:./fibo numn");
17 return -1;
18 }
19 unsigned long n = atoi(argv[1]);
20 unsigned long fiboNum = fibo(n);
21 printf("the %lu result is %lun",n,fiboNum);
22 return 0;
23}
關鍵程式碼為3~9行。簡潔明瞭,一氣呵成。
編譯:
1gcc -o fibo fibo.c
執行計算第5個斐波那契數:
1$ time ./fibo 5
2the 5 result is 5
3
4real 0m0.001s
5user 0m0.001s
6sys 0m0.000s
看起來並沒有什麼不妥,執行時間也很短。
繼續計算第50個斐波那契數列:
1$ time ./fibo 50
2the 50 result is 12586269025
3
4real 1m41.655s
5user 1m41.524s
6sys 0m0.076s
計算第50個斐波那契數的時候,竟然花了一分多鐘!
遞迴分析
為什麼計算第50個的時候竟然需要1分多鐘。我們仔細分析我們的遞迴演算法,就會發現問題,當我們計算fibo(5)的時候,是下面這樣的:
1 |--F(1)
2 |--F(2)|
3 |--F(3)| |--F(0)
4 | |
5 |--F(4)| |--F(1)
6 | |
7 | | |--F(1)
8 | |--F(2)|
9 | |--F(0)
10F(5)|
11 | |--F(1)
12 | |--F(2)|
13 | | |--F(0)
14 |--F(3)|
15 |
16 |--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中,我們可以透過下面的命令檢視棧空間的軟限制:
1$ ulimit -s
28192
可以看到,預設棧空間大小隻有8M。一般來說,8M的棧空間對於一般程式完全足夠。如果8M的棧空間不夠使用,那麼就需要重新審視你的程式碼設計了。
迭代解法
既然遞迴法不夠優雅,我們換一種方法。如果不用計算機計算,讓你去算第n個斐波那契數,你會怎麼做呢?我想最簡單直接的方法應該是:知道第一個和第二個後,計算第三個;知道第二個和第三個後,計算第四個,以此類推。最終可以得到我們需要的結果。這種思路,沒有冗餘的計算。基於這個思路,我們的C語言實現如下:
1/*fibo1.c*/
2#include <stdio.h>
3#include <stdlib.h>
4/*求斐波那契數列迭代版*/
5unsigned long fibo(unsigned long n)
6{
7 unsigned long preVal = 1;
8 unsigned long prePreVal = 0;
9 if(n <= 2)
10 return n;
11 unsigned long loop = 1;
12 unsigned long returnVal = 0;
13 while(loop < n)
14 {
15 returnVal = preVal +prePreVal;
16 /*更新記錄結果*/
17 prePreVal = preVal;
18 preVal = returnVal;
19 loop++;
20 }
21 return returnVal;
22}
23/**main函式部分與fibo.c相同,這裡省略*/
編譯並計算第50個斐波那契數:
1$ gcc -o fibo1 fibo1.c
2$ time ./fibo1 50
3the 50 result is 12586269025
4
5real 0m0.002s
6user 0m0.001s
7sys 0m0.002s
可以看到,計算第50個斐波那契數只需要0.002s!時間複雜度為O(n)。
尾遞迴解法
同樣的思路,但是採用尾遞迴的方法來計算。要計算第n個斐波那契數,我們可以先計算第一個,第二個,如果未達到n,則繼續遞迴計算,尾遞迴C語言實現如下:
1/*fibo2.c*/
2#include <stdio.h>
3#include <stdlib.h>
4/*求斐波那契數列尾遞迴版*/
5unsigned long fiboProcess(unsigned long n,unsigned long prePreVal,unsigned long preVal,unsigned long begin)
6{
7 /*如果已經計算到我們需要計算的,則返回*/
8 if(n == begin)
9 return preVal+prePreVal;
10 else
11 {
12 begin++;
13 return fiboProcess(n,preVal,prePreVal+preVal,begin);
14 }
15}
16
17unsigned long fibo(unsigned long n)
18{
19 if(n <= 1)
20 return n;
21 else
22 return fiboProcess(n,0,1,2);
23}
24
25/**main函式部分與fibo.c相同,這裡省略*/
效率如何呢?
1$ gcc -o fibo2 fibo2.c
2$ time ./fibo2 50
3the 50 result is 12586269025
4
5real 0m0.002s
6user 0m0.001s
7sys 0m0.002s
可見,其效率並不遜於迭代法。尾遞迴在函式返回之前的最後一個操作仍然是遞迴呼叫。尾遞迴的好處是,進入下一個函式之前,已經獲得了當前函式的結果,因此不需要保留當前函式的環境,記憶體佔用自然也是比最開始提到的遞迴要小。時間複雜度為O(n)。
遞迴改進版
既然我們知道最初版本的遞迴存在大量的重複計算,那麼我們完全可以考慮將已經計算的值儲存起來,從而避免重複計算,該版本程式碼實現如下:
1/*fibo3.c*/
2#include <stdio.h>
3#include <stdlib.h>
4/*求斐波那契數列,避免重複計算版本*/
5unsigned long fiboProcess(unsigned long *array,unsigned long n)
6{
7 if(n < 2)
8 return n;
9 else
10 {
11 /*遞迴儲存值*/
12 array[n] = fiboProcess(array,n-1) + array[n-2];
13 return array[n];
14 }
15}
16
17unsigned long fibo(unsigned long n)
18{
19 if(n <= 1)
20 return n;
21 unsigned long ret = 0;
22 /*申請陣列用於儲存已經計算過的內容*/
23 unsigned long *array = (unsigned long*)calloc(n+1,sizeof(unsigned long));
24 if(NULL == array)
25 {
26 return -1;
27 }
28 array[1] = 1;
29 ret = fiboProcess(array,n);
30 free(array);
31 array = NULL;
32 return ret;
33}
34/**main函式部分與fibo.c相同,這裡省略*/
效率如何呢?
1$ gcc -o fibo3 fibo3.c
2$ time ./fibo3 50
3the 50 result is 12586269025
4
5real 0m0.002s
6user 0m0.002s
7sys 0m0.001s
可見效率是不遜於其他兩種最佳化演算法的。但是特別注意的是,這種改進版的遞迴,雖然避免了重複計算,但是呼叫鏈仍然比較長。
其他解法
其他兩種時間複雜度為O(logn)的矩陣解法以及O(1)的通項表示式解法本文不介紹。歡迎留言補充。
總結
總結一下遞迴的優缺點:
優點:
實現簡單
可讀性好
缺點:
遞迴呼叫,佔用空間大
遞迴太深,易發生棧溢位
可能存在重複計算
可以看到,對於求斐波那契數列的問題,使用一般的遞迴併不是一種很好的解法。
所以,當你使用遞迴方式實現一個功能之前,考慮一下使用遞迴帶來的好處是否抵得上它的代價。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2370/viewspace-2819893/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 斐波那契數列
- 斐波那契數列(Java)Java
- 著名的斐波那契數列
- 斐波那契數列 (C#)C#
- PHP 與斐波那契數列PHP
- 斐波那契數列詳解
- 斐波那契數
- js實現斐波那契數列JS
- 斐波那契數列演算法演算法
- 第十題:斐波那契數列
- [C103] 斐波那契數列
- 力扣之斐波那契數列力扣
- 劍指offer——斐波那契數列
- 斐波那契數列js 實現JS
- 斐波那契數列Ⅳ【矩陣乘法】矩陣
- 斐波那契數列的來源——數兔子
- 大數斐波那契數列的演算法演算法
- 使用Python實現斐波那契數列Python
- 演算法(1)斐波那契數列演算法
- 斐波那契數列數與等冪和
- 面試官:用“尾遞迴”優化斐波那契函式面試遞迴優化函式
- Leedcode-斐波那契數
- 509. 斐波那契數
- LeetCode 509[斐波那契數]LeetCode
- 一千位斐波那契數
- 計算斐波那契數列的演算法演算法
- 【演算法】Fibonacci(斐波那契數列)相關問題演算法
- js迭代器實現斐波那契數列JS
- offer通過--9斐波那契數列-2
- 演算法一:斐波那契阿數列演算法
- JavaScript 實現:輸出斐波那契數列JavaScript
- 面試:老師講的遞迴解決斐波那契數列真的好嗎面試遞迴
- fibonacci斐波那契數列詳解 遞迴求Fn非遞迴求Fn求n最近的斐波那契數遞迴
- 斐波那契數列的通項公式及證明公式
- 開啟斐波那契數列的新研究大門
- 斐波那契數列:7數5層魔法塔(3)
- 斐波那契數列:7數5層魔法塔(2)
- 斐波那契數列:7數5層魔法塔(5)