前言
在計算機領域,記憶(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)將函式的引數和結果值,儲存在物件當中,用一部分的記憶體開銷來提高程式計算的效能,常用在遞迴和重複運算較多的場景。