记忆函数的实战应用
力扣2623.記憶函數
今天在力扣做了一道題:使用JavaScript實現記憶函數,所謂記憶函數就是一個對于相同的輸入永遠不會被調用兩次的函數。相反,它將返回一個緩存值。
以下是使用哈希表實現的方法:
/**
* @param {Function} fn
* @return {Function}
*/
function memoize(fn) {
const map = new Map();
return function(...args) {
const item = args.join(',')
if(!map.has(item)) {
map.set(item, fn(...args))
}
return map.get(item)
}
}
/**
* let callCount = 0;
* const memoizedFn = memoize(function (a, b) {
* callCount += 1;
* return a + b;
* })
* memoizedFn(2, 3) // 5
* memoizedFn(2, 3) // 5
* console.log(callCount) // 1
*/
需要說明的是,記憶函數只對純函數(Pure function)有效,也就是對那些給定相同的輸入,始終返回相同的輸出,并且沒有任何副作用的函數。
假如忽略這一點,可能會導致使用具有副作用的函數,會執行相應的過程,但每次后續調用都不會再得到新的結果。
Web開發中的記憶化
記憶化作為一種重要的思想,在Web開發中有很多實戰:
緩存網站文件
大型網站通常由許多 JavaScript 文件組成,在用戶訪問不同頁面時會動態下載這些文件。有時會采用一種模式,其中文件名基于文件內容的哈希值。這樣,當 Web 瀏覽器請求已經在之前請求過的文件名時,它可以從磁盤上本地加載文件,而不必重新下載它。
React 組件
React 是一個非常流行的用于構建用戶界面的庫,尤其適用于單頁面應用程序。其核心原則之一是將應用程序分解為單獨的 組件。每個組件負責渲染應用程序HTML的不同部分。
例如,你可能有一個組件如下:
const TitleComponent = (props) => {
return <h1>{props.title}</h1>;
};
上面的函數將在每次父組件渲染時調用,即使 title 沒有更改。通過在其上調用 React.memo,可以提高性能,避免不必要的渲染。
const TitleComponent = React.memo((props) => {
return <h1>{props.title}</h1>;
});
現在,TitleComponent 只有在 title 發生變化時才會重新渲染,從而提高了應用程序的性能。
緩存 API 調用
假設你有一個函數,用于向API發送網絡請求以訪問數據庫中的鍵值對。
async function getValue(key) {
// 數據庫請求邏輯
}
const getValueMemoized = memoize(getValue);
現在,getValueMemoized 將僅為每個鍵進行一次網絡請求,可能大大提高性能。需要注意的是,由于 getValue 是異步的,它將返回一個 Promise 而不是實際值。對于這種用例,這實際上是最理想的,因為即使在第一次請求返回值之前調用兩次,它仍然只會進行一次網絡請求。
記憶化網絡請求的一個潛在缺點是數據陳舊的風險。如果數據庫中與特定鍵關聯的值發生更改,記憶化函數可能仍然返回舊的緩存結果,使用戶無法看到更新。
處理這種情況的幾種方法:
- 始終向 API 發送請求,詢問值是否已更改。
- 使用 WebSocket 訂閱數據庫中值的更改。
- 為值提供 過期時間,以使用戶至少不會看到太過時的數據。
算法中的記憶化
記憶化的一個經典應用是在動態規劃中,將問題分解為若干子問題。這些子問題可以表示為函數調用,其中許多函數調用多次且使用相同的輸入,因此可以進行優化。
動態規劃極大提高效率的一個經典示例是計算斐波那契數。
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
fib(100); // 耗時多年
但是,通過不再使用相同的輸入兩次調用 fib,我們可以在 O(n)的時間內計算斐波那契數。
const cache = new Map();
function fib(n) {
if (n <= 1) return n;
if (cache.has(n)) {
return cache.get(n);
}
const result = fib(n - 1) + fib(n - 2);
cache.set(n, result);
return result;
}
fib(100); // 幾乎立即解決
我們是否可以只是調用了fib的第一個實現,然后在其上寫了memoizedFib = memoize(fib);以獲得相同的性能優化?不幸的是,不能。fib 的原始實現引用了自身(未記憶化版本)。因此,如果調用 memoizedFib(100),緩存只會添加一個鍵(100),仍然需要數年時間才能計算。這是 JavaScript 的一個基本限制(Python 沒有此問題)。
總結
- 上一篇: 简单剖析Hashmap
- 下一篇: bitcask论文翻译/笔记