将递归算法转换为迭代算法的设计模式

Posted

技术标签:

【中文标题】将递归算法转换为迭代算法的设计模式【英文标题】:Design patterns for converting recursive algorithms to iterative ones 【发布时间】:2010-12-05 17:11:57 【问题描述】:

是否有任何通用的启发式方法、提示、技巧或通用设计范例可用于将递归算法转换为迭代算法?我知道这是可以做到的,我想知道这样做时是否有值得牢记的做法。

【问题讨论】:

查看 Eric Lippert 关于递归的精彩系列:blogs.msdn.com/ericlippert/archive/tags/Recursion/…(从零部分开始。) 好吧,我的心都融化了。 【参考方案1】:

您通常可以完全保留递归算法的原始结构,但避免堆栈,方法是使用尾调用并更改为 continuation-passing,正如 this blog entry 所建议的那样。 (我真的应该编写一个更好的独立示例。)

【讨论】:

+1 表示当您想要消除递归时,您最有可能首先要避免堆栈。 链接到“博客条目”似乎不再存在。请更新它 链接对我仍然有效(重定向),但更新为lorgonblog.wordpress.com/2008/06/07/catamorphisms-part-seven BDotA:尾调用是指返回语句是对另一个函数的调用。例如,factorial(x) 的递归版本可能在其中某处包含 return x*factorial(x-1)。这不是尾声。相反,它可以转换为return factorial(state*x, x-1),其中 state 是目前的值。在所有 return 语句都转换为调用之后,每个作为尾调用的 return 都可以转换为 state = state*x; x = x-1; goto start; 实际上,您不需要 goto,因为您会使用 while 循环。 @Brian: 链接 + 重定向现已损坏【参考方案2】:

在用迭代算法替换递归算法的过程中,我使用的一种常用技术通常是使用堆栈,将传递给递归函数的参数推送给递归函数。

查看以下文章:

Replacing Recursion With a Stack Stacks and Recursion Elimination (pdf)

【讨论】:

如果您使用堆栈来消除递归,那么您所做的就是使用您自己的自定义堆栈而不是使用 the 编程语言的堆栈,对吗?这不是违背了目的吗? 是的,我会问为什么发帖人想要通用算法,因为这真的是唯一的一个 @ldog:这是否违背了目的?不,不是。该程序的堆栈在大小上受到严格限制,而用户实现的堆栈很可能会分配在有更多空间的免费存储上。我认为堆栈空间不足是从递归转换为迭代的最可能原因,这解决了这个问题。 (是的,我意识到这是 2 岁,但最近的一个问题只是与之相关) @ldog 有时您需要将算法转换为不支持递归的语言(即 OpenCL)。【参考方案3】:

一种常见的做法是管理一个 LIFO 堆栈,该堆栈保留一个运行列表,其中包含“还有待完成的工作”,并在一个 while 循环中处理整个过程,该循环一直持续到列表为空. 使用这种模式,真正递归模型中的递归调用被替换为 - 将当前(部分完成)任务的“上下文”推送到堆栈上, - 将新任务(提示递归的任务)推入堆栈 - 并“继续”(即跳到开头)while 循环。 在循环头部附近,逻辑弹出最近插入的上下文,并在此基础上开始工作。

实际上这只是将原本保存在“系统”堆栈上的嵌套堆栈帧中的信息“移动”到应用程序管理的堆栈容器。然而,这是一个改进,因为这个堆栈容器可以分配到任何地方(递归限制通常与“系统”堆栈中的限制相关联)。因此基本上完成了相同的工作,但是“堆栈”的显式管理允许这发生在单个循环构造中,而不是递归调用。

【讨论】:

【参考方案4】:

一般递归通常可以用尾递归代替,方法是在累加器中收集部分结果并通过递归调用将其传递下去。尾递归本质上是迭代的,递归调用可以实现为跳转。

例如,阶乘的标准通用递归定义

factorial(n) = if n = 0 then 1 else n * factorial(n - 1)

可以替换为

 factorial(n) = f_iter(n, 1)

 f_iter(n, a) = if n = 0 then a else f_iter(n - 1, n * a)

这是尾递归。是一样的

a = 1;
while (n != 0) 
    a = n * a;
    n = n - 1;

return a;

【讨论】:

分支调用的情况如何,例如你每次调用递归两次,例如树遍历 - 有没有一种技术可以做到这一点?还是必须使用堆栈方法? 不,在这种情况下你必须使用一般递归,因为在第一次调用之后你将不得不返回给调用者,然后再进行第二次调用。当然,您可以通过迭代和堆栈来替换一般递归。【参考方案5】:

查看这些链接以获取性能示例

Recursion VS Iteration (Looping) : Speed & Memory Comparison

Replace Recursion with Iteration

Recursion vs Iteration

问:通常是递归版本吗 快点? A:不——它通常更慢(由于维护的开销 堆栈)

  Q: Does the recursive version usually use less memory?
  A: No -- it usually uses more memory (for the stack).

  Q: Then why use recursion??
  A: Sometimes it is much simpler to write the recursive version (but

我们需要等到我们完成 讨论树看看真的很好 例子...)

【讨论】:

【参考方案6】:

我通常从基本情况(每个递归函数都有一个)开始,然后向后工作,如有必要,将结果存储在缓存(数组或哈希表)中。

您的递归函数通过解决较小的子问题并使用它们来解决问题的较大实例来解决问题。每个子问题也被进一步分解,以此类推,直到子问题小到解决方案微不足道(即基本情况)。

我们的想法是从基本案例(或多个基本案例)开始,并使用它为更大的案例构建解决方案,然后使用这些案例来构建更大的案例等等,直到整个问题得到解决。这不需要堆栈,可以通过循环来完成。

一个简单的例子(Python):

#recursive version
def fib(n):
     if n==0 or n==1:
             return n
     else:
             return fib(n-1)+fib(n-2)

#iterative version
def fib2(n):
     if n==0 or n==1:
             return n
     prev1,prev2=0,1 # start from the base case
     for i in xrange(n):
             cur=prev1+prev2 #build the solution for the next case using the previous solutions
             prev1,prev2=cur,prev1
     return cur

【讨论】:

【参考方案7】:

一个模式是Tail Recursion:

函数调用被称为尾部 如果无事可做递归 函数返回后除了 返回它的值。

Wiki.

【讨论】:

-1 不是对如何将递归问题转换为迭代问题(实际上是如何将递归问题转换为尾递归问题)的一般问题的答案,并且因为上下文引用不是很清楚(如果函数 Y 在调用 X 之后除了返回该调用的结果之外什么都不做,那么函数 X 在函数 Y 的尾部位置;如果函数是尾部递归的,则其中所有递归调用都在尾部位置)

以上是关于将递归算法转换为迭代算法的设计模式的主要内容,如果未能解决你的问题,请参考以下文章

背包问题看贪心算法原理

贪心算法及其理论依据——拟阵

回溯尾递归算法可以转换为迭代吗?

每日辞典 常用算法设计技术

将函数从递归转换为迭代

递归如何转换为非递归