斐波那契數列是一道非常經典的面試題,因爲它考察了面試者是否理解遞歸的缺點,以及如何分析遞歸的效率。
本文將結合動畫詳細分析3種常見的實現生成斐波那契數列函數的方法。
題目描述
大家都知道斐波那契數列,現在要求輸入一個整數n,請你輸出斐波那契數列的第n項(從0開始,第0項爲0)。
n<=39
舉例:
第 1 個斐波那契數列爲 1
輸入: 1
輸出: 1
第 7 個斐波那契數爲 13
輸入: 7
輸出: 13
什麼是斐波那契數列
所謂的斐波那契數列,就是數列中的每個數都是前兩數的和。
根據定義,我們可以得到如下數列:
[0, 1, 1, 2, 3, 5, 8, 13, ...]
實現方法1:遞歸 O(2^n)
從上面的斐波那契數列定義中可以發現,這是一個遞歸定義。因此,我們也可以寫一個遞歸函數來計算斐波那契數列的值。
// 遞歸求斐波那契數列
public int getFib(int n) {
// 基本情況
if (n <= 0) {
return 0;
} else if (n == 1) {
return 1;
}
// 遞歸公式
return getFib(n-1) + getFib(n-2);
}
從上述代碼中我們可以看出,使用遞歸方式寫代碼的好處就是簡潔易懂,同時也是最自然的一種寫法,但是運行效率非常低。
遞歸效率: O(2^n)
如果我們展開遞歸樹就會發現,有很多斐波那契數被重新計算了許多遍。因此,遞歸的效率是很低的,如果我們計算的 n 值非常大,那麼就得花費很長的時間才能得到結果。大家可以試試看計算 n = 45。
效率推導過程如下:
求第 n 個斐波那契數所需的時間等於 取第 n - 1 個與第 n - 2 個的時間之和
T(n) = T(n-1) + T(n-2) + O(1)
由於 T(n - 1) ~= T(n - 2)
得
T(n) = 2T(n-1) + O(1)
根據遞歸公式,我們知道 T(n-1) = 2T(n-2) + O(1)
因此
T(n)
= 2T(n-1) + O(1)
= 2(2T(n-2)) + O(1)
= 4(2T(n-3)) + O(1)
= 8(2T(n-4)) + O(1)
= 2^n
實現方法2:從底層開始循環計算 O(n)
給定:
- f(0) = 0
- f(1) = 1
我們從 f(2) 開始使用循環一路往上疊加計算。
循環效率: O(n)
由圖中可見,從下自上的循環不會出現重複計算的情況,每個數字會遍歷一次,因此效率爲 O(n)。
// 循環
public int getFib(int n) {
// 首兩個斐波那契數數
int f0 = 0;
int f1 = 1;
if (n == 0) {
return f0;
} else if (n == 1) {
return f1;
}
int j = f0;
int k = f1;
int result = 0;
// 一路循環往上疊加
for (int i = 0; i < n - 1; i++) {
result = j + k;
j = k;
k = result;
}
return result;
}
實現方法3:動態規劃 O(n)
動態規劃就是將之前求到的解存起來,以後還需要再用到的時候直接從內存提取結果,而不需要再次計算。
如果我們多次計算斐波那契數列的話,則可以儲存之前已經計算過的結果,避免重新計算。如此一來,平均效率會達到 O(n)。缺點則是會犧牲一些空間來儲存之前的計算結果。
// 動態規劃
// Map 儲存之前的結果
private Map<Integer, Integer> fibNumbers = new HashMap();
public int getFibDp(int n) {
if (n == 0) {
fibNumbers.put(0, 0);
return 0;
} else if (n == 1) {
fibNumbers.put(1, 1);
return 1;
}
if (fibNumbers.get(n) != null) {
// 如果之前已經計算過結果,直接返回
return fibNumbers.get(n);
} else {
// 否則,進行遞歸計算並儲存結果
int result = getFibDp(n-1) + getFibDp(n-2);
fibNumbers.put(n, result);
return result;
}
}
其他實現方法
其實還由很多其他的實現方法,但比較少用所以就不在這裏描述。
值得一提的時,我們也可以利用斐波那契數列的母函數 (Generating Function) 直接套公式求出值來。這個方法的優點是效率高,無論 n 值有多大,計算速度都不會發生改變,也就是 O(1) 的效率。
然而缺點也是顯而易見的,母函數的公式較爲複雜,並且計算時存在浮點運算與誤差值的問題。