可以将每个递归转换为迭代吗?


181

一个reddit线程提出了一个看似有趣的问题:

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

帖子中的(counter?)示例是一对:

(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))

3
我不认为这是一个反例。堆栈技术将起作用。它不会很漂亮,我也不会写,但这是可行的。似乎akdas在您的链接中承认了这一点。
马修·弗拉申

您的(num-way xy)只是(x + y)choosex =(x + y)!/(x!y!),不需要递归。
ShreevatsaR


我想说递归只是一种方便。
e2-e4 '18

Answers:


180

您能否始终将递归函数转换为迭代函数?是的,绝对可以。如果有记性的话,Church-Turing论文就证明了这一点。用通俗的话说,它可以由递归函数计算,而可以由迭代模型(例如图灵机)计算,反之亦然。这篇论文并没有确切告诉您如何进行转换,但是它确实说有可能。

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

另一方面,我们当然可以想象有一个如此先进的编程系统,可以将公式的递归定义作为邀请来记住以前的结果,从而提供了速度上的好处,而无需麻烦地告诉计算机准确地执行哪个步骤。遵循具有递归定义的公式的计算。迪克斯特拉几乎可以肯定确实想象过这样的系统。他花了很长时间尝试将实现与编程语言的语义分开。再说一次,他的非确定性和多处理程序设计语言比实际的专业程序员还高。

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

我可以看到一个令人信服的理由。假设您具有超高级语言的原型系统,例如[ doning石棉内衣 ] Scheme,Lisp,Haskell,OCaml,Perl或Pascal。假设情况如此,您需要用C或Java实现。(也许是出于政治考虑。)那么,您当然可以递归地编写一些函数,但是按字面意义翻译这些函数会使您的运行时系统爆炸。例如,在Scheme中可以执行无限尾递归,但是相同的习惯用法会给现有C环境带来问题。另一个示例是使用词法嵌套函数和静态作用域,Pascal支持,而C不支持。

在这种情况下,您可以尝试克服对原始语言的政治抵制。您可能会发现自己像Greenspun的第十章第十章中那样严重地重新实施Lisp。或者,您可能只是找到了一种完全不同的解决方案。但是无论如何,肯定有一种方法。


10
教会图灵还没有被证明吗?
Liran Orevi

15
@eyelidlessness:如果可以在B中实现A,则意味着B至少具有与A相同的功能。如果无法在B的A实现中执行A的某些语句,则它不是实现。如果A可以用B实现,而B可以用A实现,那么power(A)> = power(B),并且power(B)> = power(A)。唯一的解决方案是power(A)== power(B)。
Tordek 2010年

6
回复:第一段:您是在讲计算模型的等效性,而不是Church-Turing论文。等效性是由Church和/或Turing证明的AFAIR,但这不是论文。该论文是一个实验性事实,即所有直观可计算的事物都可以在严格的数学意义上进行计算(通过图灵机/递归函数等)。如果使用物理定律我们可以建造一些非经典计算机来计算图灵机无法完成的事情(例如停止问题),则可能无法得到证明。而等价是一个数学定理,并且不会被证明。
sdcvvc 2012年

7
这个答案到底是如何获得正面支持的?首先,它将Turing完整性与Church-Turing命题混合在一起,然后进行了一系列错误的手工操作,提到了“高级”系统,并丢弃了惰性无限尾递归(可以使用C或任何Turing完整语言进行此操作,因为.. 。有人知道图灵完整意味着什么吗?)。然后得出一个有希望的结论,就像这是关于奥普拉的问题,您需要的是积极向上吗?可怕的答案!
ex0du5'5

8
关于语义的bs ???真?这是一个关于句法转换的问题,它以某种方式成为命名drop Dijkstra的好方法,意味着您对pi微积分有所了解吗?让我明确地说:无论是看一门语言的指称语义还是其他某种模型都不会影响这个问题的答案。语言是汇编语言还是生成域建模语言都没有任何意义。仅关于图灵完整性和将“堆栈变量”转换为“变量堆栈”。
ex0du5 2012年

43

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

是。一个简单的形式证明就是证明µ递归和非递归演算(例如GOTO)都已经完成了Turing。由于所有图灵完备演算在表达能力上都严格相等,因此所有递归函数都可以通过非递归图灵完备演算来实现。

不幸的是,我无法在线找到良好的GOTO正式定义,所以这里是一个:

GOTO程序是在寄存器机上执行的命令P序列,因此P是以下之一:

  • HALT,这会暂停执行
  • r = r + 1r任何寄存器在哪里
  • r = r – 1r任何寄存器在哪里
  • GOTO xx标签在哪里
  • IF r ≠ 0 GOTO x哪里r是寄存器,x是标签
  • 标签,后跟上述任何命令。

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

有关更多信息,请参见此答案


好答案!但是在实践中,我很难将递归算法转换为迭代算法。例如,到目前为止,我无法将在此处community.topcoder.com/…展示的单态打字机转换为迭代算法
Nils,

31

递归在实际的解释器或编译器中以堆栈或类似结构的形式实现。因此,您当然可以将递归函数转换为迭代的对等函数,因为这始终是完成的方式(如果自动执行的话)。您只是临时地并且可能以非常丑陋和低效的方式来复制编译器的工作。


13

基本上是的,从本质上讲,您最终要做的是将方法调用(将状态隐式地推入堆栈)替换为显式堆栈推入,以记住“先前的调用”到达的位置,然后执行“被调用的方法”代替。

我想通过基本模拟方法调用,可以将循环,堆栈和状态机的组合用于所有方案。总的来说,这是否会变得“更好”(在某种意义上说是更快或更有效)是不可能的。


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

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

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

因此,答案是:是的。原因:https : //stackoverflow.com/a/531721/2128327

可以在单个循环中进行任何递归吗?是的,因为

图灵机通过执行一个循环来完成其所有工作:

  1. 取指令
  2. 评估一下,
  3. 转到1。

7

是的,显式使用堆栈(恕我直言,递归更容易阅读)。


16
我不会说阅读总是很愉快。迭代和递归都有它们的位置。
马修·弗拉申

6

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


如果在堆栈上分配了堆栈数据结构,或者在堆栈上分配了更长的时间,哪一个不能达到目的,不是吗?这对我来说听起来微不足道,但效率很低。
conradkleinespel 2014年

1
@conradk在某些情况下,如果必须对足够大以至于耗尽调用堆栈的问题执行某些树递归操作,这是很实际的事情。堆内存通常要丰富得多。
jamesdlin

4

原则上,总是可以删除递归,并用一种​​对数据结构和调用堆栈都具有无限状态的语言进行迭代替换。这是“图灵教堂”理论的基本结果。

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


2

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


1

有时替换递归要容易得多。递归曾经是1990年代在CS中教授的时髦事物,因此从那时起,许多普通开发人员都认为,如果您使用递归解决了某些问题,那将是一个更好的解决方案。因此,他们将使用递归而不是向后循环以颠倒顺序或类似的傻事。因此,有时删除递归是一种简单的“很明显”的练习。

现在,这已不再是问题,因为时尚已经转向其他技术。



0

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

在这里,这些函数要么返回最终结果,要么关闭它本来应该执行的函数调用。然后,启动(踩踏)功能将继续调用返回的闭包,直到达到最终结果为止。

这种方法适用于相互递归的函数,但是恐怕它仅适用于尾调用。

http://en.wikipedia.org/wiki/Trampoline_(计算机)


0

我会说是的-函数调用不过是goto和堆栈操作(大致而言)。您需要做的就是模仿调用函数时构建的堆栈,并执行类似于goto的操作(您可以使用没有显式使用此关键字的语言来模仿gotos)。


1
我认为OP正在寻找证明或其他实质性证据
Tim



-1

tazzego,递归意味着无论您是否喜欢,一个函数都会调用自己。当人们谈论是否无需递归就可以完成工作时,他们的意思是,您不能说“不,那是不正确的,因为我不同意递归的定义”作为有效声明。

考虑到这一点,您所说的其他所有内容都是胡说八道。您说的唯一并非废话的想法是,您无法想象没有调用栈的编程。在使用调用栈成为流行之前,这已经做了几十年了。旧版本的FORTRAN缺少调用堆栈,并且工作正常。

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


-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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.