算法技巧: 如何使用JavaScript編寫高效的fabonacci數列

<br />

斐波那契數列,(意大利語:Successione di Fibonacci),又譯做費波拿契數列、費氏數列、黃金分割數列。發明者,是意大利數學家列昂納多·斐波那契(Leonardo Fibonacci)。

斐波那契數列指的是這樣一個數列:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, ...

在數學上,斐波那契數列是以遞歸的方式定義:

  • f(0) = 0
  • f(1) = 1
  • f(n) = f(n - 1) + f(n - 2) // n >= 2

1202年,斐波那契完成了巨著《計算之書》(Liber Abaci),斐波那契數列便是出自這本著作,它來自一個“兔子繁殖”問題:

假定兔子在出生兩個月后就有繁殖能力,一對兔子每個月能生出一對小兔子。如果所有兔子都不死,那么一年后可以繁殖多少對兔子?

兔子繁殖
兔子繁殖

程序員的信仰

作為一個Programmer,多年經驗給我的印象,我們不是純的數學家,我們也解決數學問題,并用數學的知識武裝自己。然而有時候,我們還要考慮一些非純數學的問題。比如性能,維護,可擴展,...

接下來,我們不再過多的關注斐波那契數列的歷史,和其在物理化學上的貢獻,把關注點集中在編程實現上。

第一個fabonacci程序

根據fabonacci的定義,我們可以很輕松的編寫出實現代碼:


function fabonacci(n) {
    if (n === 0) {
        return 0;
    }
    if (n === 1) {
        return 1;
    }
    return fabonacci(n - 1) + fabonacci(n - 2);
}

讓我們來測試一下所編寫的程序,測試計算第50個值fabonacci(50):


var start  = new Date();
var result = fabonacci(50);
var end    = new Date();

console.log('fabonacci(%d) = %d, use time %dms.', 
            50, 
            result,
            end.getTime() - start.getTime());

打開Shell,運行Node.js:
$ node

然后,復制上面的代碼,回車,得到如下的控制臺輸出:
> fabonacci(50) = 12586269025, use time 242702ms.

天啊!
在我的筆記本上計算fabonacci(50),竟然花費了242秒(4分鐘)!
一定是出現了什么問題!

讓我們仔細的推導整個計算過程,來找出潛在的問題:


* f(0) = 0

* f(1) = 1

* f(2) = f(1) + f(0)

* f(3) = f(2) + f(1) 
       = (f(1) + f(0)) + f(1)

* f(4) = f(3) + f(2) 
       = (f(2) + f(1)) + (f(1) + f(0))
       = ((f(1) + f(0)) + f(1)) + (f(1) + f(0))

* f(5) = f(4) + f(3)
       = (f(3) + f(2)) + (f(2) + f(1))
       = ((f(2) + f(1)) + (f(1) + f(0))) + ((f(1) + f(0)) + f(1))
       = (((f(1) + f(0)) + f(1)) + (f(1) + f(0))) + ((f(1) + f(0)) + f(1))

* ...

看出來了嗎?

  • 當我們計算f(0)的時候,計算了1次f(0) => f(0)
  • 當我們計算f(1)的時候,計算了1次f(1) => f(1)
  • 當我們計算f(2)的時候,計算了1次f(1) + f(0) => f(2)
  • 當我們計算f(3)的時候,根據遞歸過程,實際計算了
    • 1次f(1) + f(0) => f(2)
    • 1次f(2) + f(1) => f(3)
  • 當我們計算f(4)的時候,根據遞歸過程,實際計算了
    • 2次f(1) + f(0) => f(2)
    • 1次f(2) + f(1) => f(3)
    • 1次f(3) + f(2) => f(4)
  • 當我們計算f(5)的時候,根據遞歸過程,實際計算了
    • 3次f(1) + f(0) => f(2)
    • 2次f(2) + f(1) => f(3)
    • 1次f(3) + f(2) => f(4)
    • 1次f(4) + f(3) => f(5)
  • ...

通過這個規律,我們發現

  • 計算f(4)的時候,f(2)的值被重復計算了2次
  • 計算f(5)的時候,f(2)的值被重復計算了3次,f(3)的值被重復計算了2次
  • ...

計算的數字越大,重復的計算就會越多。

怎么解決重復計算的問題?

通過上面的過程推導,我們可以這樣思考:

既然是計算過的數字值被重復計算,那么我們可以使用緩存的方式,把計算過的結果保存起來,更大的數字計算直接從緩存中取,不就可以省去計算過程了嗎?

因此,我們可以設定一個緩存對象:

var cache = {};

用來保存我們已經計算過的值,cache的使用過程將會是這樣的:
假設我們已經計算過了0~9的結果

cache = {
    0: 0,
    1: 1,
    2: 1,
    3: 2,
    4: 3,
    5: 5,
    6: 8,
    7: 13,
    8: 21,
    9: 34
};

當我們要計算fabonacci(5)的時候,我們就能夠1次從緩存中取出結果:

return cache[5];

是不是十分完美?
這樣就只計算了1次cache[5]!

那么,當我們計算fabonacci(10)的時候,我們只需要:

var result = cache[10] = cache[8] + cache[9];
return result;

只計算了1次cache[8] + cache[9],同時把結果保存進了緩存cache!

新版本的,性能高效的fabonacci程序

經過我們的努力,實現了如下性能高效的fabonacci程序:

var cache = {
    0: 0,
    1: 1
};

function fabonacci(n) {
    if (typeof cache[n] === 'number') { 
        return cache[n];
    }
    var result = cache[n] = fabonacci(n - 1) + fabonacci(n - 2);
    return result;
}

這個代碼,我們還可以再寫的優雅些,像下面這樣:

var cache = {
    0: 0,
    1: 1
};

function fabonacci(n) {
    return typeof cache[n] === 'number'
           ? cache[n]
           : cache[n] = fabonacci(n - 1) + fabonacci(n - 2);
}

OK!

現在,用我們上面的測試代碼測試一下新版本的fabonacci程序吧:

var start  = new Date();
var result = fabonacci(50);
var end    = new Date();

console.log('fabonacci(%d) = %d, use time %dms.', 
            50, 
            result,
            end.getTime() - start.getTime());

// fabonacci(50) = 12586269025, use time 2ms.

!!!Perfect!!!這次計算fabonacci(50)只用了2ms的時間!

你喜歡函數式嗎?

上面的是C語言的風格,cache放在外部,你喜歡函數式編程嗎?
我們也可以編寫函數式風格的fabonacci程序,有助于減少變量混亂:

function fabonacci() {
    var cache = {
        0: 0,
        1: 1
    };
    return function __fabonacci(n) {
        return typeof cache[n] === 'number'
               ? cache[n]
               : cache[n] = __fabonacci(n - 1) + __fabonacci(n - 2);
    };
}

var fb = fabonacci();
fb(50);

另外,你會發現cache的鍵都是數字,而且是從0開始遞增計數,
所以,cache也可以用數組代替:

function fabonacci() {
    var cache = [0, 1];
    return function __fabonacci(n) {
        return typeof cache[n] === 'number'
               ? cache[n]
               : cache[n] = __fabonacci(n - 1) + __fabonacci(n - 2);
    };
}

使用純C風格的代碼好,還是函數式風格的代碼好?
我覺得這兩個都很好,很直觀,容易維護,容易理解。
在Node.js的模塊下,你可以很安全的放置你的變量,所以C風格也不是問題。

這完全取決于你的風格!!!


當然,我們也可以奉上面向對象的風格:

function Fabonacci() {
    if (!(this instanceof Fabonacci)) {
        return new Fabonacci();
    }
    this._cache = [0, 1];
}
Fabonacci.prototype.compute = function (n) {
    return typeof this._cache[n] === 'number'
           ? this._cache[n]
           : this._cache[n] = this.compute(n - 1) + this.compute(n - 2);
};


Fabonacci().compute(50); 
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容