每次递归都可以转换成迭代吗?

时间:2009-05-31 09:48:05

标签: language-agnostic recursion iteration

reddit thread提出了一个显然有趣的问题:

  

尾递归函数可以简单地转换为迭代函数。其他的,可以通过使用显式堆栈进行转换。可以将每个递归转换为迭代吗?

帖子中的(计数器?)示例是对:

(define (num-ways x y)
  (case ((= x 0) 1)
        ((= y 0) 1)
        (num-ways2 x y) ))

(define (num-ways2 x y)
  (+ (num-ways (- x 1) y)
     (num-ways x (- y 1))

19 个答案:

答案 0 :(得分:174)

您是否可以将递归函数转换为迭代函数?是的,绝对,教会 - 图灵论文证明了记忆是否适用。在非专业术语中,它指出通过递归函数可计算的内容可由迭代模型(例如图灵机)计算,反之亦然。论文并没有准确地告诉你如何进行转换,但它确实说它绝对可能。

在许多情况下,转换递归函数很容易。 Knuth在“计算机编程艺术”中提供了几种技术。通常,递归计算的事物可以通过完全不同的方法在更短的时间和空间内计算。这方面的典型例子是斐波纳契数或其序列。你肯定在学位计划中遇到了这个问题。

在这个硬币的另一面,我们当然可以想象一个编程系统如此先进,以便将公式的递归定义视为回忆先前结果的邀请,从而提供速度优势,而无需准确告诉计算机的麻烦在使用递归定义计算公式时要遵循哪些步骤。 Dijkstra几乎肯定想象过这样一个系统。他花了很长时间试图将实现与编程语言的语义分开。然后,他的非确定性和多处理编程语言在练习专业程序员之上。

归根结底,许多函数以递归形式更容易理解,读取和写入。除非有令人信服的理由,否则您可能不应(手动)将这些函数转换为显式迭代算法。您的计算机将正确处理该作业。

我可以看到一个令人信服的理由。假设你有一个超高级语言的原型系统,如[穿石棉内衣] Scheme,Lisp,Haskell,OCaml,Perl或Pascal。假设您需要使用C或Java实现的条件。 (也许这是政治。)然后你肯定会有一些递归编写的函数,但字面上翻译会爆炸你的运行时系统。例如,在Scheme中可以进行无限尾递归,但是相同的习惯用法会导致现有C环境出现问题。另一个例子是使用词法嵌套函数和静态范围,Pascal支持但C不支持。

在这种情况下,您可能会尝试克服对原始语言的政治抵制。你可能会发现自己重新实现了Lisp,就像在格林斯普(格言中)的第十条法则中那样。或者您可能只是找到一种完全不同的解决方案。但无论如何,肯定有办法。

答案 1 :(得分:37)

  

是否总是可以为每个递归函数编写一个非递归形式?

是。一个简单的形式证明就是表明µ recursion和非递归演算(如GOTO)都是图灵完备的。由于所有图灵完全计算的表达能力都是严格等价的,所有递归函数都可以通过非递归图灵完全计算来实现。

不幸的是,我无法在网上找到GOTO的正式定义,所以这里有一个:

GOTO程序是在register machine上执行的一系列命令 P ,因此 P 是以下之一:

  • HALT,停止执行
  • r = r + 1其中r是任何注册
  • r = r – 1其中r是任何注册
  • GOTO x其中x是标签
  • IF r ≠ 0 GOTO x其中r是任何注册,x是标签
  • 标签,后面是上述任何命令。

但是,递归函数和非递归函数之间的转换并不总是微不足道的(除非通过盲目的手动重新实现调用堆栈)。

有关详细信息,请参阅this answer

答案 2 :(得分:27)

递归在实际的解释器或编译器中实现为堆栈或类似的构造。因此,您当然可以将递归函数转换为迭代对应,因为这是它始终如何完成(如果自动)。你只是在ad-hoc中复制编译器的工作,可能是一种非常丑陋和低效的方式。

答案 3 :(得分:12)

基本上是的,实质上你最不得不做的是将方法调用(隐式地将状态推入堆栈)替换为显式堆栈推送以记住“先前调用”已经到达的位置,然后执行'称为方法'而不是。

我想通过基本模拟方法调用,可以将循环,堆栈和状态机的组合用于所有场景。这种情况是否会“更好”(在某种意义上更快或更有效)实际上并不是一般可以说的。

答案 4 :(得分:8)

  • 递归函数执行流程可以表示为树。

  • 循环可以完成相同的逻辑,循环使用数据结构来遍历该树。

  • 深度优先遍历可以使用堆栈完成,广度优先遍历可以使用队列完成。

所以答案是:是的。为什么:https://stackoverflow.com/a/531721/2128327

  

任何递归都可以在一个循环中完成吗?是的,因为

     

图灵机通过执行单个循环完成它所做的一切:

     
      
  1. 获取指令,
  2.   
  3. 评估它,
  4.   
  5. 转到1。
  6.   

答案 5 :(得分:6)

是的,明确使用堆栈(但是递归更令人愉快,恕我直言)。

答案 6 :(得分:6)

是的,总是可以写一个非递归版本。简单的解决方案是使用堆栈数据结构并模拟递归执行。

答案 7 :(得分:3)

原则上,总是可以删除递归,并用对数据结构和调用堆栈都具有无限状态的语言中的迭代替换它。这是Church-Turing论文的基本结果。

鉴于实际的编程语言,答案并不那么明显。问题是,很可能有一种语言,其中可以在程序中分配的内存量是有限的,但可以使用的调用堆栈的数量是无限的(32位C,其中堆栈变量的地址不可访问)。在这种情况下,递归更强大,因为它有更多的内存可以使用;没有足够的显式可分配内存来模拟调用堆栈。有关此问题的详细讨论,请参阅this discussion

答案 8 :(得分:1)

所有可计算函数都可以通过图灵机计算,因此递归系统和图灵机(迭代系统)是等效的。

答案 9 :(得分:1)

有时替换递归比这更容易。递归曾经是1990年代CS中教授的时髦之物,因此很多普通开发人员认为,如果你通过递归解决了问题,那么这是一个更好的解决方案。所以他们会使用递归而不是向后循环来反转顺序,或者像这样愚蠢的事情。因此,有时删除递归是一种简单的“duh,这是显而易见的”运动类型。

现在这已成为一个问题,因为时尚已转向其他技术。

答案 10 :(得分:0)

递归只是在堆栈上调用相同的函数,一旦函数消失,它就会从堆栈中删除。因此,始终可以使用显式堆栈来管理使用迭代的同一操作的此调用。 所以,是的,全递归代码可以转换为迭代。

答案 11 :(得分:0)

  

可以将任何递归算法转换为非递归算法   一,但通常逻辑要复杂得多,这样做需要   使用堆栈。实际上,递归本身使用了一个堆栈:   功能堆栈。

更多详情:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions

答案 12 :(得分:0)

查看维基百科上的以下条目,您可以将它们作为起点来查找问题的完整答案。

按照一段可能会给你一些启示的提示:

  

求解递归关系意味着获得closed-form solution:n的非递归函数。

另请查看this entry的最后一段。

答案 13 :(得分:0)

我会说是 - 函数调用只不过是goto和堆栈操作(粗略地说)。您需要做的就是模仿在调用函数时构建的堆栈,并执行与goto类似的操作(您可以使用未明确拥有此关键字的语言模仿gotos)。

答案 14 :(得分:0)

来自显式堆栈的公寓,另一种将递归转换为迭代的模式是使用蹦床。

这里,函数要么返回最终结果,要么关闭它原本会执行的函数调用。然后,启动(trampolining)函数不断调用返回的闭包,直到达到最终结果。

这种方法适用于相互递归的函数,但我担心它只适用于尾调用。

http://en.wikipedia.org/wiki/Trampoline_(computers)

答案 15 :(得分:0)

删除递归是一个复杂的问题,在明确定义的情况下是可行的。

下面的案例很简单:

答案 16 :(得分:-1)

tazzego,递归意味着无论你喜欢与否,函数都会调用自己。当人们在谈论是否可以在没有递归的情况下完成任务时,他们就是这个意思,你不能说“不,这不是真的,因为我不同意递归的定义”作为一个有效的陈述。

考虑到这一点,你所说的其他一切都是无稽之谈。你说的唯一不是废话的是你不能想象没有callstack编程的想法。这是几十年来所做的事情,直到使用callstack变得流行。旧版本的FORTRAN缺少一个callstack,他们工作得很好。

顺便说一下,存在图灵完备语言,它们只实现递归(例如SML)作为循环方式。还存在图灵完备语言,其仅实现迭代作为循环的手段(例如,FORTRAN IV)。 Church-Turing论文证明,在递归语言中任何可能的东西都可以用非递归语言完成,反之亦然,因为它们都具有图灵完备性。

答案 17 :(得分:-3)

这是一个迭代算法:

def howmany(x,y)
  a = {}
  for n in (0..x+y)
    for m in (0..n)
      a[[m,n-m]] = if m==0 or n-m==0 then 1 else a[[m-1,n-m]] + a[[m,n-m-1]] end
    end
  end
  return a[[x,y]]
end

答案 18 :(得分:-5)

一个问题:如果函数首先在随机的无效内存空间中复制自身,然后调用自身而不是调用副本,这还是递归吗?(1)我会说是。

显式使用堆栈是否是一种删除递归的真正方法? (2)我会说不。基本上,我们不是在模仿当我们使用显式递归时会发生什么吗?我相信我们不能简单地将递归定义为“调用自身的函数”,因为我在“复制代码”(1)和“显式使用堆栈”(2)中也看到了递归。

此外,我没有看到CT如何证明所有递归算法都可以变成迭代。它似乎只对我说,具有图灵机的“力量”的“一切”可以表达这可以表达的所有算法。 如果图灵机无法递归,我们确信每个递归算法都有其交互式翻译......图灵机可以递归吗?据我说,如果它可以“实施”(任何意思),那么我们可以说它有它。有吗?我不知道。

我知道所有真正的CPU都可以递归。老实说,如果没有调用堆栈,我无法看到如何编程实现真正的编程,我认为这是首先使递归成为可能的原因。

避免复制(1)和“模仿堆栈”(2),我们是否已证明每个递归算法可以在实际机器上迭代表达?!我无法看到我们在哪里展示它。