js實現斐波那契數列以及優化

首先解釋下什麼是斐波那契數列:

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

   在種子數字 0 和 1 之後,後續的每一個數字都是前面兩個數字之和。

解法1:

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

以上函數使用遞歸的方式進行斐波那契數列求和,我們可能首先想到用這個,使用遞歸計算大數字時,性能會非常低;

解法2:

上面遞歸執行太多相同運算,我們可以對中間求得的變量值,進行存儲的話,就會大大減少函數被調用的次數。

let fibonacci = function() {
    let arr= [0, 1];
    return function(n) {
        let result = arr[n];
        if(typeof result != 'number') {
            result = fibonacci(n - 1) + fibonacci(n - 2);
            arr[n] = result; // 將每次 fibonacci(n) 的值都緩存下來
        }
        return result;
    }
}(); // 執行函數

解法3:遞推法

function fibonacci(n) {
    let current = 0;
    let next = 1;
    let temp;
    for(let i = 0; i < n; i++) {
        temp = current;
        current = next;
        next += temp;
    }
    console.log(`fibonacci(${n}, ${next}, ${current + next})`);
    return current;
}

從下往上計算,首先根據f(0)和f(1)算出f(2),再根據f(1)和f(2)算出f(3),依次類推我們就可以算出第n項了。比遞歸的效率高很多。

上述還可以借用解構賦值省略temp中間變量

function fibonacci(n) {
    let current = 0;
    let next = 1;
    for(let i = 0; i < n; i++) {
        [current, next] = [next, current + next];
    }
    return current;
}

解法4:尾調用優化

// 在ES6規範中,有一個尾調用優化,可以實現高效的尾遞歸方案。
// ES6的尾調用優化只在嚴格模式下開啓,正常模式是無效的。
'use strict'
function fib(n, current = 0, next = 1) {
    if(n == 0) return 0;
    if(n == 1) return next; // return next
    return fib(n - 1, next, current + next);
}

解析:

什麼是尾調用?

尾調用(Tail Call)是函數式編程的一個重要概念,本身非常簡單,一句話就能說清楚,就是指某個函數的最後一步是調用另一個函數。

function f(x){
  return g(x);
}

尾調用優化?

函數調用會在內存形成一個“調用記錄”,又稱“調用幀”(call frame),保存調用位置和內部變量等信息。如果在函數A的內部調用函數B,那麼在A的調用幀上方,還會形成一個B的調用幀。等到B運行結束,將結果返回到A,B的調用幀纔會消失。如果函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。所有的調用幀,就形成一個“調用棧”(call stack)。
尾調用由於是函數的最後一步操作,所以不需要保留外層函數的調用幀,因爲調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用幀,取代外層函數的調用幀就可以了。

function g(item) {
  return item
}
// 下面是尾調用例子
function f() {
  let m = 1
  let n = 2
  return g(m + n)
}
f()
// 上面例子實際上等同於:
g(3)

上面代碼中,如果函數g不是尾調用,函數f就需要保存內部變量m和n的值、g的調用位置等信息。但由於調用g之後,函數f就結束了,所以執行到最後一步,完全可以刪除f(x)的調用幀,只保留g(3)的調用幀。

這就叫做“尾調用優化”(Tail call optimization),即只保留內層函數(即g函數)的調用幀。如果所有函數都是尾調用,那麼完全可以做到每次執行時,調用幀只有一項,這將大大節省內存。這就是“尾調用優化”的意義。

尾遞歸是什麼?

遞歸相信大家都聽過,函數調用自身,稱爲遞歸,如果尾調用自身,就稱爲尾遞歸。

(遞歸非常耗費內存,因爲需要同時保存成千上百個調用幀,很容易發生“棧溢出”錯誤(stack overflow)。但對於尾遞歸來說,由於只存在一個調用幀,所以永遠不會發生“棧溢出”錯誤。)

function fib(n, current = 0, next = 1) {
    if(n == 0) return 0;
    if(n == 1) return next; // return next
    return fib(n - 1, next, current + next);
}

 

 

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