斐波那契數列求和的js方案以及優化

codewars上作了一道斐波那契數列求和的題目,作完以後作了一些簡單的優化和用另外一種方法實現。javascript

題目

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

以上函數使用遞歸的方式進行斐波那契數列求和,但效率十分低,不少值會重複求值。題目要求使用 memoization方案進行優化。java

My Solution

memoization方案在《JavaScript模式》和《JavaScript設計模式》都有提到。memoization是一種將函數執行結果用變量緩存起來的方法。當函數進行計算以前,先看緩存對象中是否有次計算結果,若是有,就直接從緩存對象中獲取結果;若是沒有,就進行計算,並將結果保存到緩存對象中。es6

let fibonacci = (function() {
  let memory = []
  return function(n) {
      if(memory[n] !== undefined) {
        return memory[n]
    }
    return memory[n] = (n === 0 || n === 1) ? n : fibonacci(n-1) + fibonacci(n-2)
  }
})()

使用閉包實現的memoization函數。測試經過以後,忽然我有一個小疑問,若是將memory的類型由數組換成對象,它的運算效率會有什麼變化?因而,我將memory的類型換成了對象,並寫了一個函數測試兩種數據類型的運算效率。算法

function speed(n) {
    let start = performance.now()
    fibonacci(n)
    let end = performance.now()
    console.log(end - start)
}

全部測試只在Chrome控制檯測試,而且測試次數很少,結果不嚴謹,請多多包涵。segmentfault

memory類型爲數組時(單位:毫秒):設計模式

speed(500)      // 0.8150000050663948
speed(5000)     // 3.1799999997019768
speed(7500)     // 4.234999991953373
speed(10000)    // 8.390000000596046

memory類型爲對象時(單位:毫秒):數組

speed(500)      // 0.32499999552965164
speed(5000)     // 1.6499999985098839
speed(7500)     // 2.485000006854534
speed(10000)    // 2.9999999925494194

雖然測試過程不嚴謹,但仍是能夠說明一點問題的。memory類型爲對象是明顯比類型爲數組時,運算速度快不少。至於爲何對象操做比數組操做的速度快,請原諒我水平有限,暫時答不上來。(先挖好坑,之後回來填坑,逃)如今回來填坑,例如咱們調用fibonacci(100),這時候,fibonacci函數在第一次計算的時候會設置memory[100]=xxx,此時數組長度爲101,而前面100項會初始化爲undefined。正由於如此,memory的類型爲數組的時候比類型是對象的時候慢。(這裏藥感謝hsfzxjy的提醒)緩存

Best Solution

別人的解決方案給了我靈感,讓我想出了一個緩存效率高不少的方案。閉包

var fibonacci = (function () {
  var memory = {}
  return function(n) {
    if(n==0 || n == 1) {
      return n
    }
    if(memory[n-2] === undefined) {
      memory[n-2] = fibonacci(n-2)
    }
    if(memory[n-1] === undefined) {
      memory[n-1] = fibonacci(n-1)
    }
    return memory[n] = memory[n-1] + memory[n-2]
  }
})()

測試結果就不放了(由於我發如今Chrome控制檯中運行測試代碼時,輸出結果不穩定)。不過,這裏的緩存效率的確是提升了,前面的方案,一次計算最多緩存一個結果,而這個方案,一次計算最多緩存三個結果。從這個方面考慮,運算速度理論上是會比前面的方案快的。函數

動態規劃解決方案

斐波那契數列求和除了能夠用遞歸的方法解決,還能夠用動態規劃的方法解決。因爲我是算法渣,對動態規劃瞭解很少,只懂一點點皮毛,因此這裏就不解釋動態規劃的概念了。(一不當心又挖了一個坑,逃)

直接貼代碼好了:

function fibonacci(n) {
    let n1 = 1,
        n2 = 1,
        sum = 1
    for(let i = 3; i <= n; i += 1) {
        sum = n1 + n2
        n1 = n2
        n2 = sum
    }
    return sum
}

尾遞歸方案

在ES6規範中,有一個尾調用優化,能夠實現高效的尾遞歸方案。(感謝李引證的提醒)

'use strict'
function fibonacci(n, n1, n2) {
    if(n <= 1) {
        return n2
    }
    return fibonacci(n - 1, n2, n1 + n2)
}

ES6的尾調用優化只在嚴格模式下開啓,正常模式是無效的。

通項公式方案

斐波那契數列是有通項公式的,但通項公式中有開方運算,在js中會存在偏差,而fib函數中的Math.round正式解決這一問題的。(感謝公子的指導)

function fibonacci(n){
    var sum = 0
    for(let i = 1; i <= n; i += 1) {
        sum += fib(i)
    }
    return sum

    function fib(n) {
      const SQRT_FIVE = Math.sqrt(5);
      return Math.round(1/SQRT_FIVE * (Math.pow(0.5 + SQRT_FIVE/2, n) - Math.pow(0.5 - SQRT_FIVE/2, n)));
    }
}

結語

只要注意細節,咱們的代碼仍是有很大的優化空間的。有時候,你可能會疑惑,優化先後的性能沒有明顯的變化。我認爲,那是你的應用規模或者數據量不夠大而已,當它們大到必定程度的時候,優化的效果就很明顯了。優化仍是要堅持的,萬一哪一天咱們接手大型應用呢?

相關文章
相關標籤/搜索