学习什么是Memoization?

324 阅读4分钟

非常喜欢解释的一种编程概念是有一个令人生畏的名字,但一旦你学会了它,实际上是一个相当简单的概念。这就是我对记忆化的感觉。

不是记忆-记忆化。让我们先看看维基百科是如何描述记忆化的[1]。

在计算机领域,记忆化或备忘化是一种优化技术,主要用于加快计算机程序的速度,通过存储昂贵的函数调用的结果,并在相同的输入再次出现时返回缓存的结果。

我通常觉得维基百科的描述很密集,而且乍一看有点没有帮助,但实际上我觉得这个描述超级有帮助!

基本思路

假设你有一个函数,它接受一些输入,做一些计算上很昂贵的事情,然后返回一个输出。你调用这个函数一次,这很昂贵。如果你用同样的输入再次调用这个函数,为什么要再次进行昂贵的计算?就用一个缓存的结果来代替吧

一个使用JavaScript的简单例子

让我们创建一个超级低效的函数,在JavaScript中对一个数字进行平方运算。我们将其称为inefficientSquare

function inefficientSquare(num) {
  let result = 0;
  for (let i = 0; i < num; i++) {
    for (let j = 0; j < num; j++) {
      result++;
    }
  }
  return result;
}

这个函数会对一个数字进行平方运算,但是效率非常低。让我们用这个函数对数字10,000进行平方运算,并使用节点性能计时API计算所需时间。

const { performance } = require('perf_hooks');
const start = performance.now();
inefficientSquare(10000);
const finish = performance.now();
console.log(finish - start);
// 115.8375

因此,我花了大约115毫秒来运行,但根据你的环境,可能会更长或更短。重要的是,这是一个相当昂贵的运行函数。

把它记下来!

我们知道,每次我们对一个数字进行平方时,都会得到相同的结果。因此,它是一个很好的记忆化的候选者。如果我们计算了一次数字的平方,为什么还要再做一次昂贵的计算呢?让我们把计算结果保存起来,如果我们需要它的话。

让我们创建一个memoizedSquare 函数,返回一个函数。这一次,我们将跟踪我们已经看到的结果。

function inefficientSquare(num) {
  let result = 0;
  for (let i = 0; i < num; i++) {
    for (let j = 0; j < num; j++) {
      result++;
    }
  }
  return result;
}

function memoizedSquare() {
  const cache = {};
  return function (num) {
    if (cache[num] === undefined) {
      cache[num] = inefficientSquare(num);
    }
    return cache[num];
  };
}

现在,我们可以看到,当我们用相同的值多次调用memoized函数时,我们的性能是如何提高的(显著地)。

const squareFn = memoizedSquare();

// Call the first time: ineffience
const start = performance.now();
squareFn(10000);
const finish = performance.now();
console.log(finish - start);
// 117.0957

// Call the second time: speedy!
const start2 = performance.now();
squareFn(10000);
const finish2 = performance.now();
console.log(finish2 - start2);
// 0.005

而这是有道理的--第二次我们用参数10000 调用squareFn ,我们只是从我们的cache 对象中检索值,而不是再次进行计算。

这就是记忆化!

制作一个通用记忆器

让我们做一个一般的备忘器,它也可以用于其他函数。

function memoizer(fn) {
  const cache = {};
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache[key] === undefined) {
      cache[key] = fn(...args);
    }
    return cache[key];
  };
}

我们可以通过将其应用于我们的inefficientSquare 函数来证明这个通用记忆器的工作。

function inefficientSquare(num) {
  let result = 0;
  for (let i = 0; i < num; i++) {
    for (let j = 0; j < num; j++) {
      result++;
    }
  }
  return result;
}

const squareFn = memoizer(inefficientSquare);

const start = performance.now();
squareFn(10000);
const finish = performance.now();
console.log(finish - start);
// 121.0662

const start2 = performance.now();
squareFn(10000);
const finish2 = performance.now();
console.log(finish2 - start2);
// 0.010

很好!我们现在有了一个memoizer ,该函数将另一个函数作为参数,并将其结果备忘化。

这个泛化记忆器的局限性

这个通用记忆器的最大限制是,我们使用JSON.stringify 把我们的参数变成一个对象键。JSON.stringify 是相当有限的:它只能对某些原始和对象类型进行字符串化,所以如果我们有不能被字符串化的参数,这个简单的方法是不够的

如果你在生产中需要一个备忘器,我建议使用像fast-memoize这样经过严格审查的库,它对那些不容易被字符串化的数据类型使用其他更复杂的备忘策略。

关于函数纯洁性的说明

要使用记忆化,你的函数应该是纯洁的如果你不熟悉纯函数的概念,它们基本上有两个限制:纯函数应该在给定相同的输入时有相同的输出,而且纯函数不应该有副作用。

总结

Memoization可能听起来是一个令人生畏的概念,但实际上它只是存储了一个昂贵的计算结果,以便在必要时使用。