返回課程

費氏數列

重要性:5

費氏數列 的公式為 Fn = Fn-1 + Fn-2。換句話說,下一個數字是前兩個數字的總和。

前兩個數字為 1,然後為 2(1+1),再接下來為 3(1+2)5(2+3),以此類推:1, 1, 2, 3, 5, 8, 13, 21...

費氏數列與黃金比例以及我們周遭許多自然現象有關。

撰寫一個函式 fib(n),用來傳回第 n 個費氏數列。

範例操作

function fib(n) { /* your code */ }

alert(fib(3)); // 2
alert(fib(7)); // 13
alert(fib(77)); // 5527939700884757

附註:函式必須快速。呼叫 fib(77) 應該不會花費超過一秒鐘的一小部分時間。

我們可以嘗試的第一個解法是遞迴。

費氏數列的定義就是遞迴

function fib(n) {
  return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}

alert( fib(3) ); // 2
alert( fib(7) ); // 13
// fib(77); // will be extremely slow!

…但是對於大的 n 值,它會非常慢。例如,fib(77) 可能會讓引擎掛起一段時間,並吃掉所有 CPU 資源。

這是因為這個函式會產生太多子呼叫。相同的數值會被重複評估一次又一次。

例如,我們來看一下 fib(5) 的計算片段

...
fib(5) = fib(4) + fib(3)
fib(4) = fib(3) + fib(2)
...

在這裡,我們可以看到 fib(3) 的值對於 fib(5)fib(4) 都是需要的。因此,fib(3) 將會被呼叫並評估兩次,而且完全獨立。

以下是完整的遞迴樹

我們可以清楚地注意到 fib(3) 被評估兩次,而 fib(2) 被評估三次。總計算量會比 n 成長得快很多,即使 n=77 也會變得非常龐大。

我們可以透過記住已經評估過的值來最佳化它:如果說 fib(3) 的值已經計算過一次,那麼我們就可以在未來的計算中重複使用它。

另一個變體是放棄遞迴,並使用完全不同的基於迴圈的演算法。

我們可以從 n 開始,而不是從 n 往下到較小的值,我們可以建立一個從 12 開始的迴圈,然後將 fib(3) 作為它們的總和,然後將 fib(4) 作為前兩個值的總和,然後是 fib(5),並一直往上,直到得到需要的數值。在每一步,我們只需要記住前兩個值。

以下是新演算法的詳細步驟。

開始

// a = fib(1), b = fib(2), these values are by definition 1
let a = 1, b = 1;

// get c = fib(3) as their sum
let c = a + b;

/* we now have fib(1), fib(2), fib(3)
a  b  c
1, 1, 2
*/

現在我們想要得到 fib(4) = fib(2) + fib(3)

讓我們轉移變數:a,b 將會得到 fib(2),fib(3),而 c 將會得到它們的總和

a = b; // now a = fib(2)
b = c; // now b = fib(3)
c = a + b; // c = fib(4)

/* now we have the sequence:
   a  b  c
1, 1, 2, 3
*/

下一步給出另一個序列號碼

a = b; // now a = fib(3)
b = c; // now b = fib(4)
c = a + b; // c = fib(5)

/* now the sequence is (one more number):
      a  b  c
1, 1, 2, 3, 5
*/

…以此類推,直到我們得到需要的數值。這比遞迴快很多,而且不會涉及重複計算。

完整的程式碼

function fib(n) {
  let a = 1;
  let b = 1;
  for (let i = 3; i <= n; i++) {
    let c = a + b;
    a = b;
    b = c;
  }
  return b;
}

alert( fib(3) ); // 2
alert( fib(7) ); // 13
alert( fib(77) ); // 5527939700884757

迴圈從 i=3 開始,因為第一個和第二個序列值被硬編碼到變數 a=1b=1 中。

這種方法稱為 動態規劃自底向上