提问人:Jon 'links in bio' Ericson 提问时间:9/25/2008 更新时间:4/6/2013 访问量:9336
如何编写通用的记忆函数?
How do I write a generic memoize function?
问:
我正在编写一个函数来查找三角形数字,并且自然的编写方式是递归:
function triangle (x)
if x == 0 then return 0 end
return x+triangle(x-1)
end
但是尝试计算前 100,000 个三角形数字会失败,一段时间后堆栈会溢出。这是一个理想的记忆函数,但我想要一个解决方案来记住我传递给它的任何函数。
答:
function memoize (f)
local cache = {}
return function (x)
if cache[x] then
return cache[x]
else
local y = f(x)
cache[x] = y
return y
end
end
end
triangle = memoize(triangle);
请注意,为了避免堆栈溢出,仍然需要对三角形进行播种。
评论
这是一个通用的 C# 3.0 实现,如果它可以帮助的话:
public static class Memoization
{
public static Func<T, TResult> Memoize<T, TResult>(this Func<T, TResult> function)
{
var cache = new Dictionary<T, TResult>();
var nullCache = default(TResult);
var isNullCacheSet = false;
return parameter =>
{
TResult value;
if (parameter == null && isNullCacheSet)
{
return nullCache;
}
if (parameter == null)
{
nullCache = function(parameter);
isNullCacheSet = true;
return nullCache;
}
if (cache.TryGetValue(parameter, out value))
{
return value;
}
value = function(parameter);
cache.Add(parameter, value);
return value;
};
}
}
(引自一篇法国博客文章)
在 Scala 中(未经测试):
def memoize[A, B](f: (A)=>B) = {
var cache = Map[A, B]()
{ x: A =>
if (cache contains x) cache(x) else {
val back = f(x)
cache += (x -> back)
back
}
}
}
请注意,这仅适用于 arity 1 的函数,但通过咖喱,您可以使其工作。更微妙的问题是,对于任何函数。解决此问题的一种非常狡猾的方法是如下所示:memoize(f) != memoize(f)
f
val correctMem = memoize(memoize _)
我不认为这会编译,但它确实说明了这个想法。
评论
你也问了错误的问题,因为你原来的问题;)
对于这种情况,这是一种更好的方法:
三角形(n) = n * (n - 1) / 2
此外,假设公式没有如此简洁的解决方案,记忆在这里仍然是一种糟糕的方法。在这种情况下,你最好只写一个简单的循环。有关更全面的讨论,请参阅此答案。
评论
更新:评论者指出,记忆是优化递归的好方法。诚然,我以前没有考虑过这一点,因为我通常使用一种语言(C#),在这种语言中,通用记忆的构建并不是那么容易。牢记这一点,请看下面的帖子。
我认为 Luke 可能对这个问题有最合适的解决方案,但记忆通常不是解决任何堆栈溢出问题的方法。
堆栈溢出通常是由递归超出平台可以处理的深度引起的。语言有时支持“尾递归”,它重用当前调用的上下文,而不是为递归调用创建新的上下文。但是很多主流语言/平台不支持这一点。例如,C# 对尾递归没有固有的支持。64 位版本的 .NET JITter 可以将其作为 IL 级别的优化应用,如果需要支持 32 位平台,这几乎是无用的。
如果你的语言不支持尾递归,那么避免堆栈溢出的最佳选择是转换为显式循环(不那么优雅,但有时是必要的),或者找到一个非迭代算法,比如 Luke 为这个问题提供的算法。
评论
扩展这个想法,还可以使用两个输入参数来记忆函数:
function memoize2 (f)
local cache = {}
return function (x, y)
if cache[x..','..y] then
return cache[x..','..y]
else
local z = f(x,y)
cache[x..','..y] = z
return z
end
end
end
请注意,参数顺序在缓存算法中很重要,因此,如果参数顺序在要记忆的函数中无关紧要,则在检查缓存之前对参数进行排序会增加缓存命中的几率。
但需要注意的是,有些功能无法盈利地记住。我写了 memoize2 来看看是否可以加快用于查找最大公约数的递归欧几里得算法。
function gcd (a, b)
if b == 0 then return a end
return gcd(b, a%b)
end
事实证明,gcd 对记忆的反应并不好。它所做的计算远比缓存算法便宜。对于大数字,它很快就会终止。一段时间后,缓存变得非常大。这个算法可能尽可能快。
评论
我敢打赌,这样的事情应该适用于 Lua 中的变量参数列表:
local function varg_tostring(...)
local s = select(1, ...)
for n = 2, select('#', ...) do
s = s..","..select(n,...)
end
return s
end
local function memoize(f)
local cache = {}
return function (...)
local al = varg_tostring(...)
if cache[al] then
return cache[al]
else
local y = f(...)
cache[al] = y
return y
end
end
end
您可能还可以使用带有 __tostring 的元表做一些聪明的事情,以便可以使用 tostring() 转换参数列表。哦,可能性。
评论
本着以不同语言发布记忆的脉络,我想用一个不改变语言的C++示例来回应@onebyone.livejournal.com。
首先,单个 arg 函数的记忆器:
template <class Result, class Arg, class ResultStore = std::map<Arg, Result> >
class memoizer1{
public:
template <class F>
const Result& operator()(F f, const Arg& a){
typename ResultStore::const_iterator it = memo_.find(a);
if(it == memo_.end()) {
it = memo_.insert(make_pair(a, f(a))).first;
}
return it->second;
}
private:
ResultStore memo_;
};
只需创建一个记忆器的实例,向它输入您的函数和参数即可。只要确保不要在两个不同的函数之间共享同一个备忘录(但您可以在同一函数的不同实现之间共享它)。
接下来,一个驱动程序函数和一个实现。只有驱动程序函数需要是公开的 int fib(int);司机 int fib_(int);实现
实现:
int fib_(int n){
++total_ops;
if(n == 0 || n == 1)
return 1;
else
return fib(n-1) + fib(n-2);
}
和司机,要记住
int fib(int n) {
static memoizer1<int,int> memo;
return memo(fib_, n);
}
显示 codepad.org 输出的永久链接。测量呼叫次数以验证正确性。(在此处插入单元测试...)
这仅记住一个输入函数。对多个参数或不同参数进行概括,作为读者的练习。
Mathematica 有一种特别巧妙的记忆方法,它依赖于哈希和函数调用使用相同的语法这一事实:
triangle[0] = 0;
triangle[x_] := triangle[x] = x + triangle[x-1]
就是这样。它之所以有效,是因为模式匹配函数调用的规则是这样的,它总是在更通用的定义之前使用更具体的定义。
当然,如前所述,此示例有一个封闭形式的解决方案:。斐波那契数列是添加记忆如何大幅加速的典型例子:triangle[x_] := x*(x+1)/2
fib[0] = 1;
fib[1] = 1;
fib[n_] := fib[n] = fib[n-1] + fib[n-2]
虽然这也有一个封闭形式的等价物,尽管更混乱:http://mathworld.wolfram.com/FibonacciNumber.html
我不同意有人认为这不适合记忆,因为你可以“只使用一个循环”。记住的要点是,任何重复的函数调用都是 O(1) 时间。这比 O(n) 好得多。事实上,您甚至可以编造一个场景,其中记忆的实现比封闭形式的实现具有更好的性能!
在 Perl 中,通用记忆很容易获得。Memoize 模块是 perl 核心的一部分,具有高度可靠、灵活且易于使用的功能。
手册页中的示例:
# This is the documentation for Memoize 1.01
use Memoize;
memoize('slow_function');
slow_function(arguments); # Is faster than it was before
您可以在运行时添加、删除和自定义函数的记忆!您可以为自定义纪念品计算提供回调。
Memoize.pm 甚至具有使纪念品缓存持久化的工具,因此不需要在每次调用程序时重新填充它!
文档如下:http://perldoc.perl.org/5.8.8/Memoize.html
这是在不将参数转换为字符串的情况下工作的东西。
唯一需要注意的是,它不能处理 nil 参数。但是公认的解决方案无法区分值和字符串,所以这可能没问题。nil
"nil"
local function m(f)
local t = { }
local function mf(x, ...) -- memoized f
assert(x ~= nil, 'nil passed to memoized function')
if select('#', ...) > 0 then
t[x] = t[x] or m(function(...) return f(x, ...) end)
return t[x](...)
else
t[x] = t[x] or f(x)
assert(t[x] ~= nil, 'memoized function returns nil')
return t[x]
end
end
return mf
end
我受到这个问题的启发,在 Lua 中实现了(另一个)灵活的记忆功能。
https://github.com/kikito/memoize.lua
主要优点:
- 接受可变数量的参数
- 不使用 tostring;相反,它将缓存组织在树结构中,并使用参数遍历它。
- 适用于返回多个值的函数。
将代码粘贴到此处作为参考:
local globalCache = {}
local function getFromCache(cache, args)
local node = cache
for i=1, #args do
if not node.children then return {} end
node = node.children[args[i]]
if not node then return {} end
end
return node.results
end
local function insertInCache(cache, args, results)
local arg
local node = cache
for i=1, #args do
arg = args[i]
node.children = node.children or {}
node.children[arg] = node.children[arg] or {}
node = node.children[arg]
end
node.results = results
end
-- public function
local function memoize(f)
globalCache[f] = { results = {} }
return function (...)
local results = getFromCache( globalCache[f], {...} )
if #results == 0 then
results = { f(...) }
insertInCache(globalCache[f], {...}, results)
end
return unpack(results)
end
end
return memoize
在 C# 3.0 中 - 对于递归函数,您可以执行如下操作:
public static class Helpers
{
public static Func<A, R> Memoize<A, R>(this Func<A, Func<A,R>, R> f)
{
var map = new Dictionary<A, R>();
Func<A, R> self = null;
self = (a) =>
{
R value;
if (map.TryGetValue(a, out value))
return value;
value = f(a, self);
map.Add(a, value);
return value;
};
return self;
}
}
然后,您可以创建一个记忆的斐波那契函数,如下所示:
var memoized_fib = Helpers.Memoize<int, int>((n,fib) => n > 1 ? fib(n - 1) + fib(n - 2) : n);
Console.WriteLine(memoized_fib(40));
递归不是必需的。第 n 个三角形数是 n(n-1)/2,所以......
public int triangle(final int n){
return n * (n - 1) / 2;
}
评论
memoize
triangle
请不要重复这个。要么使用 x*(x+1)/2 公式,要么简单地迭代值并边走边记住。
int[] memo = new int[n+1];
int sum = 0;
for(int i = 0; i <= n; ++i)
{
sum+=i;
memo[i] = sum;
}
return memo[n];
下一个:Java 中闭包的现状如何?
评论
triangle