JS專題之memoization

前言

在計算機領域,記憶(memoization)是主要用於加速程序計算的一種優化技術,它使得函數避免重複演算以前已被處理過的輸入,而返回已緩存的結果。 -- wikipediajavascript

Memoization 的原理就是把函數的每次執行結果都放入一個對象中,在接下來的執行中,在對象中查找是否已經有相應執行過的值,若是有,直接返回該值,沒有才真正執行函數體的求值部分。在對象裏找值是要比執行函數的速度要快的。java

另外,Memoization 只適用於肯定性算法,對於相同的輸入老是生成相同的輸出,即純函數。git

1、簡單實現

經過 Memoization 的定義和原理,咱們就能夠初步實現 Memoization 了。github

let memoize = function(func) {
  let cache = {};
  return function(key) {
    if (!cache[key])
      cache[key] = func.apply(this, arguments);
    return cache[key];
  }
}
複製代碼

是否是很簡單~ 函數記憶其實就是利用閉包,將函數參數做爲存儲對象的鍵(key),函數結果做爲存儲對象的 value 值。算法

2、underscore 實現

underscore 的源碼中有 Memoization 方法的封裝,它支持傳入一個 hasher 用來計算緩存對象 key 的計算方式。緩存

_.memoize = function(func, hasher) {
  var memoize = function(key) {
    // 把存儲對象的引用拿出來,便於後面代碼使用
    var cache = memoize.cache;

    // hasher 是計算 key 值的方法函數。
    // 若是傳入了 hasher,則用 hasher 函數來計算 key
    // 不然用 參數 key(即 memoize 方法傳入的第一個參數)當 key
    var address = '' + (hasher ? hasher.apply(this, arguments) : key);

    // 若是 key 尚未對應的 hash 值(意味着沒有緩存值,沒有計算過該輸入)
    // 就執行回調函數,並緩存結果值
    if (!_.has(cache, address))
      cache[address] = func.apply(this, arguments);

    // 從緩存對象中取結果值
    return cache[address];
  };

  // cache 對象被當作 key-value 鍵值對緩存中間運算結果
  memoize.cache = {};

  // 返回 momoize 函數, 因爲返回函數內部引用了 memoize.cache, 構成了閉包,變量保存在了內存中。
  return memoize;
};
複製代碼

3、應用 - 判斷素數

質數爲在大於 1 的天然數中,除了 1 和它自己之外再也不有其餘因數。閉包

咱們經過判斷素數的函數,看看使用了函數記憶後的效果。app

function isPrime(value) {
  console.log("isPrime 被執行了!");
  var prime = value != 1; // 1 不是素數,其餘數字默認是素數。
  for (var i = 2; i < value; i++) {
    if (value % i == 0) {
      prime = false;
      break;
    }
  }
  return prime
}

let momoizedIsPrime = memoize(isPrime);

momoizedIsPrime(5) // isPrime 被執行了!
momoizedIsPrime(5) // 第二次執行,沒有打印日誌!
複製代碼

4、應用 - 計算斐波那契數列

斐波那契數列的特色是後一個數等於前面兩個數的和函數

指的是這樣一個數列:一、一、二、三、五、八、1三、2一、……在數學上,斐波那契數列以以下被以遞歸的方法定義:F0=0,F1=1,Fn=Fn-1+Fn-2性能

計算斐波那契數列是用來演示函數記憶很好的例子,由於計算斐波那契數列函數裏面用了大量的遞歸。

var count = 0;
var fibonacci = function(n) {
  count++;
  return n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1);
}

for(var i = 0; i<= 10; i++) {
    console.log(`i: ${i}, ` + fibonacci(i));
}

// i: 0, 0
// i: 1, 1
// i: 2, 1
// i: 3, 2
// i: 4, 3
// i: 5, 5
// i: 6, 8
// i: 7, 13
// i: 8, 21
// i: 9, 34
// i: 10, 55

console.log(count);  // 453 !!!
複製代碼

咱們能夠看出,若是從 0 開始打印斐波那契數列,fibonacci 函數被執行了 453 次。那咱們就犧牲一小部份內存,用來緩存每次計算的值。

fibonacci = memoize(fibonacci);

for(var i = 0; i<= 10; i++) {
    console.log(`i: ${i}, ` + fibonacci(i));
}
console.log(count); // 12
複製代碼

經過 memoize 函數記憶,使得函數執行次數只須要 12 次,大大優化了函數執行計算的性能消耗。

總結

函數記憶(memoization)將函數的參數和結果值,保存在對象當中,用一部分的內存開銷來提升程序計算的性能,經常使用在遞歸和重複運算較多的場景。

參考:
冴羽 - JavaScript專題之函數記憶

相關文章
相關標籤/搜索