上下文
Memoization是一种在递归函数上运行的函数技术,具有重叠调用,旨在通过使用内部缓存来优化时间性能,该内部缓存会记住先前已使用参数的结果。典型的用例是斐波那契函数。下面显示了该功能的非记忆和记忆版本以及用于计时目的的辅助功能:
function time (fn) {
return function () {
var before = Date.now();
var result = fn.apply(this, arguments);
var after = Date.now();
return {
value : result,
time : after - before
};
};
}
var fib = function (n) {
if (n < 2) return n;
else return fib(n-1) + fib(n-2);
};
var mfib = function (n) {
var cache = {};
var memoizefib = function (n) {
if (n < 2) return n;
else {
var k1 = JSON.stringify(n-1);
var k2 = JSON.stringify(n-2);
var v1 = k1 in cache ? cache[k1] : (cache[k1] = memoizefib(n-1));
var v2 = k2 in cache ? cache[k2] : (cache[k2] = memoizefib(n-2));
return v1 + v2;
}
};
return memoizefib (n);
};
如果现在我们测试我们的功能,我们意识到memoization会大大减少执行时间:
(function test (n) {
var tfib = time(fib);
var tmfib = time(mfib);
console.log(tfib(n)); // -> { value: 433494437, time: 5780 }
console.log(tmfib(n)); // -> { value: 433494437, time: 1 }
})(43);
问题。
正如在函数式编程中经常发生的那样,当在更高阶次应用时,memoization成为一个有用的工具,以允许定义可以转换为泛型函数memoize
的{{1}}函数。类似于下一个的典型解决方案可以在Web上找到[1] [2] [3]:
fn
问题。
尽管如此,令人惊讶的是这些解决方案都不起作用!围绕代码旋转之后。我认为问题出现在(1)中,因为递归不会应用于function memoize (fn) {
var cache = {};
return function () {
var args = [].slice.call (arguments);
var key = JSON.stringify(args);
return key in cache ?
cache[key] :
cache[key] = fn.apply(this, args); (1)
};
}
的记忆版本,而是应用于原始fn
,因此只能应用一次memoization。以下是我的结果:
fn
似乎在Javascript中无法以更高的顺序应用此技术。我对吗?有没有人有任何解决方案或替代代码来获得更高阶的记忆功能?
答案 0 :(得分:7)
有趣的问题。为什么不把这个功能记忆到自己身上?
function factorial(n) { return n ? n * factorial(n-1) : 1; }
// simple memoization with one argument and console reporting
function memoize(fn) {
var cache = {};
return function(x) {
if (x in cache) { console.log('retrieved value from cache for', x); }
return x in cache ? cache[x] : cache[x] = fn.apply(this, arguments);
};
}
// redefine factorial to be its memoized version
factorial = memoize(factorial);
执行此操作后,factorial
现在将调用其记忆版本。
> factorial(6)
720
> factorial(7)
retrieved value from cache for 6
5040
将此应用于您的案例(无需mfib
):
(function test (n) {
var tfib = time(fib);
console.log(tfib(n));
fib = memoize(fib); // <-- memoize on top of itself
var tmfib = time(fib);
console.log(tmfib(n));
})(30);
结果:
Object {value: 832040, time: 714}
Object {value: 832040, time: 22}
请注意,此解决方案非常适用于&#34;内部记忆&#34;在单个递归计算中使用,而不仅仅是在上面的阶乘情况下对函数的附加外部调用。通过使用memoized版本重新定义函数,现在对memoized函数进行内部递归调用。这导致了从714到22的戏剧性时间改善。
答案 1 :(得分:0)
如果您在单个递归计算中寻找“内部”记忆,那么阶乘函数(在当前接受的答案中给出 - 我认为这也是错误的BTW)是一个不好的候选人并且不能用来展示它。这是因为,有一个递归链,所以一旦你计算,例如5!
的值你不会再需要它在同一个计算中。为了展示你所要求的东西,你确实需要使用像Fibonnaci序列这样的例子(你自己在问题中使用它)。
您编写的高阶memoize
函数在调用非记忆版本的同一计算内部无法正常工作。目前接受的答案建议将该函数“重新定义”为其备忘版本。不过我认为这是错误的。 memoize
函数关闭它作为参数提供的函数(函数是值)。将用于指向该函数值的变量更改为某个其他函数值(memoized
版本)无效。
所以,我认为不可能概括它。也就是说,我不相信有可能实现一个memoize
函数来外部记忆一个函数,并使它也适用于单个递归计算。
以下是 对Fibonnaci的作用(就像我说的那样,不可推广):
var fib = (function() {
function fib(n) {
if ((n===1) || (n==2))
return 1;
else
return memoizedFibonacci(n-1)+memoizedFibonacci(n-2);
}
var cache = {};
function memoizedFibonacci(n) {
if (n in cache) { console.log('retrieved value from cache for', n); }
return n in cache ? cache[n] : cache[n] = fib(n);
};
return fib;
})();
console.log(fib(10));
console.log(fib(10));
以上代码在输出上生成:
retrieved value from cache for 2
retrieved value from cache for 3
retrieved value from cache for 4
retrieved value from cache for 5
retrieved value from cache for 6
retrieved value from cache for 7
retrieved value from cache for 8
55
retrieved value from cache for 9
retrieved value from cache for 8
55
...这与第一次递归计算的“内部记忆”的期望一致。没有缓存10的值对于这个例子来说是微不足道的,并且可以简单地修复但是它会增加很少(基本上你也需要从fib
函数访问缓存)。