劍指offer 動畫圖解 | 斐波那契數列 3種實現方法

斐波那契數列是一道非常經典的面試題,因爲它考察了面試者是否理解遞歸的缺點,以及如何分析遞歸的效率。

本文將結合動畫詳細分析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) 的效率。

然而缺點也是顯而易見的,母函數的公式較爲複雜,並且計算時存在浮點運算與誤差值的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章