我应该对算法使用递归或memoization吗?

时间:2009-01-26 15:11:29

标签: algorithm recursion memoization

如果我可以选择使用递归或memoization来解决我应该使用的问题?换句话说,如果它们都是可行的解决方案,因为它们提供了正确的输出并且可以在我正在使用的代码中合理地表达,我何时会使用其中一个?

14 个答案:

答案 0 :(得分:22)

它们不是相互排斥的。你可以同时使用它们。

就个人而言,我首先构建基本递归函数,然后添加memoization作为优化步骤。

答案 1 :(得分:13)

使用的经验法则是基于子问题具有的重叠的数量。如果你正在计算斐波那契数(经典的递归例子),那么如果使用递归就会进行大量不必要的重新计算。

例如,要计算F(4),我需要知道F(3)和F(2),所以我通过计算F(2)和F(1)来计算F(3),依此类推。如果我使用递归,我只计算了F(2)和大多数其他F(n)两次。如果我使用memoization,我可以查看值。

如果您正在进行二分查找,则子问题之间没有重叠,因此递归是可以的。在每个步骤将输入数组拆分成两半会产生两个唯一的数组,它们代表两个没有重叠的子问题。在这种情况下,记忆化不会带来好处。

答案 2 :(得分:8)

递归具有与堆栈帧的创建相关的性能损失,memoization的惩罚是结果的缓存,如果性能是一个关注的唯一方法,确定将在您的应用程序中进行测试。

在我个人看来,我会先使用最容易使用和理解的方法,在我看来是递归。直到你证明需要进行记忆化。

答案 3 :(得分:7)

Memoization只是一种恰好用于优化递归的缓存方法。它不能取代递归。

答案 4 :(得分:6)

我不知道在不知道问题的情况下我可以说。通常,您希望使用带递归的memoization 。如果您实际上可以将其用作替代解决方案,那么记忆可能比递归明显更快。它们都存在性能问题,但它们随着问题的性质/输入的大小而有所不同。

答案 5 :(得分:3)

我选择memoization,因为它通常可以访问堆栈内存而不是堆栈内存。

也就是说,如果你的算法是在大量数据上运行的,那么在大多数语言中你都会用尽堆栈空间的递归,然后才能在堆上节省数据的空间用完。

答案 6 :(得分:3)

我相信你可能会混淆memoization(正如其他人所说的,递归算法的优化策略)dynamic programming模拟一个递归解决方案但是实际上并不使用递归)。如果这是你的问题,我会说这将取决于你的优先级:高运行时效率(动态编程)或高可读性(memoization,因为问题的递归解决方案仍然存在于代码中)。

答案 7 :(得分:2)

这取决于你的目标。动态编程(memoization)几乎总是更快。通常很多。 (即,立方到平方,或指数到多边形),但根据我的经验,递归更容易阅读和调试。

然后,很多人都像瘟疫一样避免递归,所以他们觉得不容易阅读......

另外,(第三只?)我发现在我提出递归算法之后找到动态解决方案是最容易的,所以我最终做到了这两点。但如果你已经有了这两种解决方案,动态可能是你最好的选择。

我不确定我是否有帮助,但你去了。与许多算法选择一样,YMMV。

答案 8 :(得分:2)

如果你的问题是一个递归问题,你有什么选择,只能递归?

您可以使用记忆短路的方式编写递归函数,以获得第二次调用的最大速度。

答案 9 :(得分:1)

如果可以使用尾递归技术解决问题,则递归不需要使用大量的堆栈空间。如前所述,取决于问题。

答案 10 :(得分:1)

在通常情况下,您面临两个有助于您做出决定的标准:

  • 运行时间
  • 可读性

递归代码通常较慢但更具可读性(并非总是如此,但最常见的情况。正如所说的那样,如果你的语言支持它,尾递归会有所帮助 - 如果没有,你可以做的就不多了。)

递归问题的迭代版本通常在运行时方面更快,但代码很难理解,因此很脆弱。

如果两个版本具有相同的运行时间和相同的可读性,则没有理由选择其中一个版本。在这种情况下,您必须检查其他事项:此代码是否会更改?维修怎么样?你对递归算法感到满意吗,还是他们给你做恶梦?

答案 11 :(得分:1)

var memoizer = function (fund, memo) {
    var shell = function (arg) {
        if (typeof memo[arg] !== 'number') {
            memo[arg] = fund(shell, arg);
        }
        return memo[arg];
    };
    return shell;
};

var fibonacci = memoizer(function (recur, n) { return recur(n - 1) + recur(n - 2); }, [0, 1]);

同时使用!

答案 12 :(得分:1)

我不同意Tomalak的断言,即递归问题你别无选择只能进行递归?

最好的例子是上面提到的Fibonacci系列。 在我的计算机上,F(45)的递归版本(斐波那契为F)需要15秒才能增加2269806339,迭代版本需要43次添加并在几微秒内执行。

另一个着名的例子是河内塔。在关于这个主题的课程之后,似乎递归是解决它的唯一方法。但即便在这里也有一个迭代解决方案,尽管它并不像递归那样明显。即便如此,迭代速度也更快,主要是因为不需要昂贵的堆栈操作。

如果你对Hamoi的非递归版本感兴趣,这里是Delphi的源代码:

procedure TForm1.TowersOfHanoi(Ndisks: Word);
var
  I: LongWord;
begin
  for I := 1 to (1 shl Ndisks) do
    Memo1.Lines.Add(Format('%4d: move from pole %d to pole %d',
      [I, (I and (I - 1)) mod 3, (I or (I - 1) + 1) mod 3]));
  Memo1.Lines.Add('done')
end;

答案 13 :(得分:0)

结合两者。使用memoization优化您的递归解决方案。这就是备忘录的意义所在。用于使用内存空间来加速递归。