所有的迭代算法都可以递归表达吗?
Posted
技术标签:
【中文标题】所有的迭代算法都可以递归表达吗?【英文标题】:Can all iterative algorithms be expressed recursively? 【发布时间】:2011-01-06 19:24:20 【问题描述】:如果没有,是否有一个很好的反例来说明不存在递归对应的迭代算法?
如果是所有迭代算法都可以递归表示的情况,有没有更难做到的情况?
另外,编程语言在这一切中扮演什么角色?我可以想象,与纯 Java 程序员相比,Scheme 程序员对迭代(=尾递归)和堆栈使用有不同的看法。
【问题讨论】:
mathoverflow.com 【参考方案1】:对此有一个简单的临时证明。由于您可以使用严格的迭代结构构建图灵完备语言,而仅使用递归结构构建图灵完备语言,因此两者是等价的。
【讨论】:
哇,值得深思。我羡慕所有比我消化得更快的支持者。是时候阅读这篇文章了。 等等,这不是逻辑谬误吗?循环推理? C Bauer:其实不是。证明很容易做到:假设语言 IT(仅具有迭代构造)和 REC(仅具有递归构造)。使用 IT 模拟通用图灵机,然后使用 REC 模拟通用图灵机。模拟器程序的存在保证了 IT 和 REC 都可以计算所有可计算的函数。此属性已在 lambda 演算中得到证明,其中所有函数都是部分递归的。 或者,使用严格(迭代/递归)构建图灵完备语言,并在其中为严格使用(递归/迭代)的图灵完备语言编写解释器。瞧,您编写的任何程序都将同时迭代和递归地执行!【参考方案2】:就像你说的,每一种迭代方法都可以变成一种“递归”方法,并且通过尾调用,堆栈也不会爆炸。 :-) 事实上,这就是 Scheme 实现所有常见循环形式的方式。 Scheme中的示例:
(define (fib n)
(do ((x 0 y)
(y 1 (+ x y))
(i 1 (+ i 1)))
((> i n) x)))
在这里,虽然函数看起来是迭代的,但它实际上在一个内部 lambda 上递归,该 lambda 接受三个参数,x
、y
和 i
,并在每次迭代时使用新值调用自身。
这是宏扩展函数的一种方式:
(define (fib n)
(letrec ((inner (lambda (x y i)
(if (> i n) x
(inner y (+ x y) (+ i 1))))))
(inner 0 1 1)))
这样,递归的性质在视觉上变得更加明显。
【讨论】:
请注意,任何迭代算法都可以变成尾递归算法。例如,只需将其转换为 continuation-passing 样式。 我只想补充一点,并非每种语言编译器都会优化尾调用,因此堆栈确实可以在那些使用尾递归(例如 C#)的语言中“爆炸”(溢出)。【参考方案3】:将迭代定义为:
function q(vars):
while X:
do Y
可以翻译为:
function q(vars):
if X:
do Y
call q(vars)
在大多数情况下,Y 将包括增加一个由 X 测试的计数器。在执行递归路由时,这个变量必须以某种方式在“vars”中传递。
【讨论】:
【参考方案4】:Prolog 是唯一的递归语言,你可以在其中做几乎所有事情(我不建议你这样做,但你可以:))
【讨论】:
【参考方案5】:正如 plinth 在their answer 中所指出的,我们可以构建证明,证明recursion 和迭代是等价的,并且都可以用来解决相同的问题;然而,即使我们知道这两者是等价的,使用其中一个也有缺点。
在未针对递归进行优化的语言中,您可能会发现使用迭代的算法比递归算法执行得更快,同样,即使在优化的语言中,您可能会发现使用不同语言编写的迭代算法比递归算法运行得更快一。此外,可能没有一种明显的方法可以使用递归与迭代来编写给定算法,反之亦然。这可能会导致代码难以阅读,从而导致可维护性问题。
【讨论】:
【参考方案6】:所有的迭代算法都可以递归表达吗?
是的,但证明并不有趣:
将程序及其所有控制流转换为包含单个 case 语句的单个循环,其中每个分支都是直线控制流,可能包括 break
、return
、exit
、raise
等在。引入一个新变量(称为“程序计数器”),case 语句使用它来决定接下来要执行哪个块。
这种结构是在 1960 年代伟大的“结构化编程战争”中发现的,当时人们正在争论各种控制流结构的相对表达能力。
将循环替换为递归函数,并将每个可变局部变量替换为该函数的参数。瞧!迭代替换为递归。
这个过程相当于为原始函数编写一个解释器。正如您可能想象的那样,它会导致无法阅读的代码,并且这不是一件有趣的事情。 但是,对于具有命令式编程背景且第一次学习使用函数式语言进行编程的人来说,其中一些技术可能很有用。
【讨论】:
噢,@Norman,这是一件有趣的事情……对于编译器。实际上这个过程是:将命令式代码转换为函数式代码,然后,将函数式代码转换为命令式代码。为什么这很有趣?因为函数式代码语义简单但无法执行,而命令式输出难以理解但适合执行。特别是函数式代码很容易针对高级事物进行优化,而命令式代码针对低级事物进行优化(但初始组合很难用于任何目的)。 真正无趣的是那些通过递归函数调用来模拟的控制原语的语义。虽然有时他们可能会使源代码背后的意图更加清晰,但除了在原始上下文中模拟执行之外,通常难以推理他们实际所做的事情。 OTOH,函数调用有众所周知的方法可以为有经验的用户快速减少。区别就像您是否有权使用某些方程式(而不是做所有算术运算)来解决问题。【参考方案7】: 与迭代解决方案相比,递归解决方案通常效率相对较低。 但是,需要注意的是,有些问题只能通过递归来解决,并且可能不存在等效的迭代解决方案,或者编程起来非常复杂(例如,阿克曼函数无法在没有递归的情况下表达) 虽然递归很优雅,但易于编写和理解。
【讨论】:
“没有递归就无法表达阿克曼函数”这不是真的。你认为递归是如何在计算机中实现的? CPU 对一系列指令进行迭代操作。为了支持函数调用,包括递归调用,它管理一个stack。使用递归只是让语言(操作系统、运行时等)为您管理堆栈。任何递归算法都可以替换为您自己管理堆栈的迭代算法,包括 Ackermann。以上是关于所有的迭代算法都可以递归表达吗?的主要内容,如果未能解决你的问题,请参考以下文章
C语言的两个问题: 所有的递归程序均可以用非递归算法实现?递归函数中的形式参数是自动变量吗? c语言中