JS專題之memoization

南波發表於2019-02-08

前言

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

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

另外,Memoization 只適用於確定性演算法,對於相同的輸入總是生成相同的輸出,即純函式。

一、簡單實現

通過 Memoization 的定義和原理,我們就可以初步實現 Memoization 了。

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

是不是很簡單~ 函式記憶其實就是利用閉包,將函式引數作為儲存物件的鍵(key),函式結果作為儲存物件的 value 值。

二、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;
};
複製程式碼

三、應用 - 判斷素數

質數為在大於 1 的自然數中,除了 1 和它本身以外不再有其他因數。

我們通過判斷素數的函式,看看使用了函式記憶後的效果。

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) // 第二次執行,沒有列印日誌!
複製程式碼

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

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

指的是這樣一個數列:1、1、2、3、5、8、13、21、……在數學上,斐波那契數列以如下被以遞迴的方法定義: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專題之函式記憶

相關文章