记忆函数的实战应用
力扣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 没有此问题)。