加载中...

记忆函数的实战应用

力扣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 而不是实际值。对于这种用例,这实际上是最理想的,因为即使在第一次请求返回值之前调用两次,它仍然只会进行一次网络请求。

记忆化网络请求的一个潜在缺点是数据陈旧的风险。如果数据库中与特定键关联的值发生更改,记忆化函数可能仍然返回旧的缓存结果,使用户无法看到更新。

处理这种情况的几种方法:

  1. 始终向 API 发送请求,询问值是否已更改。
  2. 使用 WebSocket 订阅数据库中值的更改。
  3. 为值提供 过期时间,以使用户至少不会看到太过时的数据。

算法中的记忆化

记忆化的一个经典应用是在动态规划中,将问题分解为若干子问题。这些子问题可以表示为函数调用,其中许多函数调用多次且使用相同的输入,因此可以进行优化。

动态规划极大提高效率的一个经典示例是计算斐波那契数。

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 没有此问题)。

posted @ 2024-01-20 21:33  辜负寒彻骨  阅读(138)  评论(0编辑  收藏  举报