【问题标题】:How do I write a generic memoize function?如何编写通用的 memoize 函数?
【发布时间】:2010-09-12 21:26:21
【问题描述】:

我正在编写一个函数来查找triangle numbers,而编写它的自然方式是递归:

function triangle (x)
   if x == 0 then return 0 end
   return x+triangle(x-1)
end

但尝试计算前 100,000 个三角形数会在一段时间后因堆栈溢出而失败。这是memoize 的理想函数,但我想要一个能够记住我传递给它的任何函数的解决方案。

【问题讨论】:

  • 查看this blog post 了解通用 Scala 解决方案,最多 4 个参数。
  • 为什么每个人都在编写复杂、缓慢的递归函数?它只是 n*(n-1)/2。请看下面我的回答。它是在 java 中,而不是 Lua,但仍然可以理解。
  • @Fractaly:非常正确。然而,triangle 函数不是问题的主旨,它实际上是在询问一个稍微深奥的概念。尽管如此,这仍然是解决问题的最佳方法。太好了,事实上,它已经是suggested
  • 感谢您指出这一点。有很多答案,我想我错过了。这似乎是一个糟糕的记忆示例案例,因为它实际上并不需要它。
  • @Fractaly:那时我还年轻又傻……

标签: optimization recursion lua closures memoization


【解决方案1】:

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) 好很多。事实上,您甚至可以编造一个场景,即 memoized 实现比封闭式实现具有更好的性能!

【讨论】:

    【解决方案2】:

    您还针对您的原始问题提出了错误的问题;)

    对于这种情况,这是一个更好的方法:

    三角形(n) = n * (n - 1) / 2

    此外,假设公式没有如此简洁的解决方案,那么记忆化在这里仍然是一种糟糕的方法。在这种情况下,您最好只编写一个简单的循环。请参阅this answer 进行更全面的讨论。

    【讨论】:

    • 玩弄这个函数似乎很明显会有一个更简单的算法。谢谢!
    • @onebyone.livejournal.com:我确信当我解决问题时,笔记会揭示这个数学解决方案。 ;-)
    【解决方案3】:

    我敢打赌,这样的事情应该适用于 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() 转换参数列表。哦,可能性。

    【讨论】:

    • 干得好!我还没有看过 Lua 中的变量参数列表,所以这是一个很好的例子。
    • 有没有办法将 args 转换为值比转换为字符串更有效?
    • 可以做一个N维数组,这样可以解决逗号问题,但是缓存访问效率可能会低一些。数学和递归函数是记忆的最佳候选者,所以我不认为这些是大问题。
    • 你应该添加一个选项来使缓存成为一个弱表(弱键和弱值),这样缓存可以偶尔清理一次,避免内存膨胀
    • 在 varg_tostring 函数中使用字符串连接确实会破坏性能,在每个连接处创建新字符串。更好的是简单地使用 table.concat({...},"whateverSeperatorYou'dWant") 代替。
    【解决方案4】:

    在 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));
    

    【讨论】:

      【解决方案5】:

      在 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 的函数,但通过 currying 您可以使其工作。更微妙的问题是memoize(f) != memoize(f) 对于任何函数f。解决此问题的一种非常偷偷摸摸的方法如下:

      val correctMem = memoize(memoize _)
      

      我认为这不会编译,但它确实说明了这个想法。

      【讨论】:

      • 大声笑好点,我的陈述还不够充分。我会解决的。
      • 至少在我看来,scala 看起来像是 Python、c# 和 c++ 的科学怪人。
      【解决方案6】:

      更新:评论者指出记忆是优化递归的好方法。诚然,我以前没有考虑过这一点,因为我通常使用一种语言 (C#),在这种语言中,通用的记忆化并不是那么容易构建的。牢记这一点,阅读下面的帖子。

      我认为Luke likely has the most appropriate solution 解决了这个问题,但记忆化通常不是任何堆栈溢出问题的解决方案。

      堆栈溢出通常是由于递归超出平台可以处理的深度引起的。语言有时支持“tail recursion”,它重新使用当前调用的上下文,而不是为递归调用创建新的上下文。但是很多主流语言/平台不支持这一点。例如,C# 没有对尾递归的固有支持。 .NET JITter 的 64 位版本可以将其用作 IL 级别的优化,如果您需要支持 32 位平台,这几乎是无用的。

      如果您的语言不支持尾递归,那么避免堆栈溢出的最佳选择是转换为显式循环(不太优雅,但有时​​是必要的),或者找到非迭代算法,例如 Luke 提供的这个问题。

      【讨论】:

      • 其实这个函数应该被memoized,即使尾递归有效。为了说服自己,想象一下用两个非常大的数字调用它两次。如果第一次调用的结果被缓存,第二次调用会快得多。
      • 还有语言实现尾调用优化吗?我以为那些微软的人做了 F#,他们肯定到处都实现了。
      【解决方案7】:
      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);
      

      请注意,为避免堆栈溢出,三角形仍需要播种。

      【讨论】:

      • 一个有趣(但无用)的通用 memoize 构造:在 memoize 上调用 memoize
      • @Adam Rosenfield:嗯……时髦!
      【解决方案8】:

      这里有一些不需要将参数转换为字符串的方法。 唯一需要注意的是它无法处理 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
      

      【讨论】:

        【解决方案9】:

        我受到这个问题的启发,在 Lua 中实现(又一个)灵活的 memoize 功能。

        https://github.com/kikito/memoize.lua

        主要优点:

        • 接受可变数量的参数
        • 不使用tostring;相反,它以树状结构组织缓存,使用参数对其进行遍历。
        • 适用于返回 multiple values 的函数。

        把代码贴在这里作为参考:

        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
        

        【讨论】:

          【解决方案10】:

          这是一个通用的 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;
                          };
              }
          }
          

          (引自french blog article

          【讨论】:

            【解决方案11】:

            在以不同语言发布备忘录的同时,我想用一个不改变语言的 C++ 示例回复@onebyone.livejournal.com。

            首先,单个 arg 函数的 memoizer:

            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_;
            };
            

            只需创建一个 memoizer 实例,将您的函数和参数提供给它。只需确保不要在两个不同的函数之间共享同一个备忘录(但您可以在同一函数的不同实现之间共享它)。

            接下来,一个驱动函数和一个实现。只有驱动功能需要公开 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);
            }
            

            Permalink showing output 在 codepad.org 上。测量调用次数以验证正确性。 (在此处插入单元测试...)

            这只会记住一个输入函数。概括多个参数或不同的参数作为练习留给读者。

            【讨论】:

              【解决方案12】:

              在 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

              【讨论】:

                【解决方案13】:

                扩展思路,还可以用两个输入参数来记忆函数:

                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 来看看是否可以加快用于查找最大公约数的递归 Euclidean algorithm

                function gcd (a, b) 
                   if b == 0 then return a end
                   return gcd(b, a%b)
                end
                

                事实证明,gcd 对记忆的反应并不好。它所做的计算比缓存算法便宜得多。对于大量数据,它会很快终止。一段时间后,缓存变得非常大。这个算法可能已经尽可能快了。

                【讨论】:

                • 你不能在 memoize 函数返回的闭包中使用可变参数吗?在 Lua 中,您可以执行 t = {...} 之类的操作将变量参数列表打包到表中,或者直接调用函数并传递 f(...)。然后只需将可变参数列表打包为字符串以用作缓存索引。
                • 注意:如果参数在转换为字符串时包含“,”逗号,这将中断。例如,f("1", "2,3") 的计算结果与 f("1,2", "3") 相同,即使结果不正确。
                【解决方案14】:

                不需要递归。第n个三角形数是n(n-1)/2,所以……

                public int triangle(final int n){
                   return n * (n - 1) / 2;
                }
                

                【讨论】:

                • 当然,但问题实际上是关于如何编写memoize 函数。 triangle 函数只是一个示例,用于演示该函数的使用。
                【解决方案15】:

                请不要递归。要么使用 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];
                

                【讨论】:

                  猜你喜欢
                  • 2017-11-03
                  • 1970-01-01
                  • 2014-06-06
                  • 1970-01-01
                  • 2010-09-13
                  • 1970-01-01
                  • 1970-01-01
                  • 2022-01-10
                  相关资源
                  最近更新 更多