关于语言不可知:每次递归都可以转换为迭代吗?

Can every recursion be converted into iteration?

一条Reddit的线索提出了一个显然很有趣的问题:

Tail recursive functions can trivially be converted into iterative functions. Other ones, can be transformed by using an explicit stack. Can every recursion be transformed into iteration?

(柜台?)文章中的例子是:

1
2
3
4
5
6
7
8
(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))


你能总是把一个递归函数变成一个迭代函数吗?是的,绝对如此,而丘奇图灵理论证明了这一点,如果记忆起作用的话。在LAY术语中,它指出递归函数可计算的内容是由迭代模型(如图灵机)计算的,反之亦然。本文并没有准确地告诉您如何进行转换,但它确实说这是绝对可能的。

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

另一方面,我们可以想象一个先进的编程系统,将公式的递归定义视为对先前结果的记忆邀请,从而提供速度优势,而无需麻烦地告诉计算机在使用递归定义的公式的计算中要遵循哪些步骤。Dijkstra几乎可以肯定地想象出这样一个系统。他花了很长时间试图将实现与编程语言的语义分离开来。此外,他的非确定性和多处理编程语言在实践专业编程人员中处于领先地位。

在最后的分析中,许多函数都是简单的,易于理解、读取和以递归的形式写入。除非有令人信服的原因,否则您可能不应该(手动)将这些函数转换为显式迭代算法。您的计算机将正确处理该作业。

我可以看到一个令人信服的原因。假设您有一个超高级语言的原型系统,比如[穿石棉内衣]方案、Lisp、Haskell、Ocaml、Perl或Pascal。假设条件是这样的,您需要在C或Java中实现。(也许是政治原因)然后你当然可以有一些递归编写的函数,但如果按照字面意思翻译的话,这些函数会爆炸你的运行时系统。例如,无限尾递归在方案中是可能的,但相同的习惯用法会给现有的C环境带来问题。另一个例子是使用词汇嵌套函数和静态范围,Pascal支持但C不支持。

在这种情况下,你可能会试图克服对原语的政治阻力。你可能会发现自己很难重新实现口齿不清,就像格林斯潘的第十定律一样。或者你可以找到一个完全不同的解决方法。但无论如何,肯定有办法。


Is it always possible to write a non-recursive form for every recursive function?

对。一个简单的形式证明是μ递归和goto等非递归微积分都是图灵完备的。由于所有图灵完备演算在表达能力上都是严格等价的,因此所有递归函数都可以通过非递归图灵完备演算实现。

不幸的是,我找不到一个好的,正式的goto在线定义,所以这里有一个:

goto程序是在寄存器机器上执行的一系列命令p,其中p是以下命令之一:

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

但是,递归函数和非递归函数之间的转换并不总是简单的(除非通过无意识的手动重新实现调用堆栈)。

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


递归在实际的解释器或编译器中实现为堆栈或类似的构造。因此,您当然可以将递归函数转换为迭代函数,因为它总是这样做的(如果是自动的)。您只需临时复制编译器的工作,而且可能会以非常丑陋和低效的方式进行复制。


基本上是的,本质上,您最终要做的是将方法调用(它隐式地将状态推送到堆栈上)替换为显式堆栈推送,以记住"上一个调用"到达的位置,然后执行"被调用的方法"。

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


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

  • 同样的逻辑也可以通过一个循环来完成,它使用一个数据结构来遍历该树。

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

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

Can any recursion be done in a single loop? Yes, because

a Turing machine does everything it does by executing a single loop:

  • fetch an instruction,
  • evaluate it,
  • goto 1.

  • 是的,显式使用一个堆栈(但是递归更容易阅读,imho)。


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


    原则上,对于数据结构和调用堆栈,总是可以删除递归并用具有无限状态的语言中的迭代替换它。这是教会图灵论的一个基本结论。

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


    有时替换递归比这容易得多。递归曾经是20世纪90年代在CS中流行的东西,所以从那时起,很多普通的开发人员认为,如果用递归解决某个问题,它是一个更好的解决方案。所以他们会使用递归,而不是向后循环来颠倒顺序,或者像这样愚蠢的事情。所以有时候删除递归是一个简单的"duh,那是显而易见的"类型的练习。

    随着时尚转向了其他技术,这现在已经不再是一个问题了。


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


    It is possible to convert any recursive algorithm to a non-recursive
    one, but often the logic is much more complex and doing so requires
    the use of a stack. In fact, recursion itself uses a stack: the
    function stack.

    更多详细信息:https://developer.mozilla.org/en-us/docs/web/javascript/guide/functions


    看看维基百科上的以下条目,你可以把它们作为一个起点来找到你问题的完整答案。

    • 计算机科学中的递归
    • 递推关系

    以下段落可能会提示您从何处开始:

    Solving a recurrence relation means obtaining a closed-form solution: a non-recursive function of n.

    还可以看一下这一条的最后一段。


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

    在这里,函数要么返回最终结果,要么关闭它本来应该执行的函数调用。然后,initiating(trampolining)函数继续调用返回的闭包,直到达到最终结果。

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

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


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

    下面的例子很简单:

    • 尾部递归
    • 直接线性递归

    我会说是的-函数调用只是一个goto和一个堆栈操作(大致来说)。您所需要做的只是模仿在调用函数时构建的堆栈,并执行类似于goto的操作(您可以使用不显式包含此关键字的语言来模仿goto)。


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

    考虑到这一点,你所说的一切都是胡说八道。你说的唯一一件不是胡说八道的事情就是你无法想象没有调用堆栈的编程。这是几十年来一直在做的事情,直到使用调用堆栈变得流行。Fortran的旧版本缺少一个调用堆栈,它们工作得很好。

    顺便说一下,图灵完备的语言只实现递归(例如sml)作为循环的一种方式。也存在图灵完整语言,它们只实现迭代作为循环的一种手段(例如Fortran IV)。丘奇图灵理论证明了在递归语言中任何可能的事情都可以在非递归语言中完成,反之亦然,因为它们都具有图灵完备性的特性。


    下面是一个迭代算法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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

    一个问题:如果作为第一件事,函数在一个随机的空内存空间中复制自己,然后不调用自己,而是调用副本,那么这仍然是递归吗?(1)我同意。

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

    此外,我不知道CT如何证明所有递归算法都可以迭代。它似乎只对我说,"一切"有"权力"的图灵机器可以表达所有算法,这可以表达。如果图灵机不能递归,我们确信每一个递归算法都有它的交互翻译…图灵机能递归吗?据我所知,如果它可以"实现"(任何方式),那么我们可以说它已经实现了。是吗?我不知道。

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

    避免复制(1)和"模仿堆栈"(2),我们是否证明了在实际机器上,每个递归算法都可以迭代地表示?!我看不出我们在哪里演示的。