尾调用和尾递归有什么区别?

时间:2012-08-20 21:15:25

标签: lisp scheme

我理解尾递归,是一个特殊情况,函数对它自己进行尾调用。 但我不明白尾调用和尾递归是如何不同的。 在具有已实现的TCO(尾调用优化)的“正确尾递归”语言中,如Scheme,这意味着尾调用和尾递归不消耗堆栈或其他资源。 在编译器无法优化尾递归的语言中,程序可能会耗尽堆栈并崩溃。 在“正确的尾递归”语言中,实现尾循环的循环效率并不比使用循环有效,我猜想。

3 个答案:

答案 0 :(得分:25)

让我们首先消除“尾调用”的歧义。

尾部位置的调用是一个函数调用,其结果立即作为封闭函数的值返回。尾部位置是静态属性。

尾部位置的调用可以在不将任何东西推入堆栈的情况下实现,因为旧的堆栈帧基本上是无用的(假设在函数式语言中通常是正确的,但不一定在C中等等)。正如Guy Steele所说,尾调用是一个通过参数的跳跃。

粗略地说,语言实现是正确的尾递归,如果它具有相同的渐近空间使用,就像在没有堆栈增长的情况下实现跳转到尾部位置的所有调用一样。这是一个非常粗略的简化。如果您想要完整的故事,请参阅Clinger的Proper Tail Recursion and Space Efficiency

请注意,仅仅处理尾递归函数不足以实现正确的尾递归(任何尾部调用必须专门处理)。这个术语有点误导。

另请注意,还有其他方法可以实现渐近空间效率,而无需将尾调用实现为跳转。例如,您可以将它们实现为普通调用,然后通过删除无用的帧(以某种方式)定期压缩堆栈。见Baker的Cheney on the MTA

答案 1 :(得分:13)

正如你所说,尾递归是尾调用的特例。因此,任何实现一般TCO的语言都是“正确的尾递归”。

然而,反过来并不成立。有很多语言只能优化尾递归,因为这非常简单 - 您可以直接将其转换为循环,并且不需要以新方式操作堆栈的特定“尾调用”操作。例如,这就是编译为没有尾调用指令的JVM的语言通常只优化尾(自)递归的原因。 (有一些技巧可以解决缺乏这种指导的问题,例如蹦床,但它们非常昂贵。)

全尾调用优化不仅适用于自(或相互)递归调用,而且适用于尾部位置的任何调用。特别是,它扩展到目标不是静态知道的呼叫,例如,调用第一类函数或动态调度方法时!因此,它需要更精细(但众所周知)的实现技术。

许多函数式编程技术 - 以及一些流行的OO模式(参见例如Felleisen's ECOOP'04 presentationGuy Steele's blog post) - 要求完全TCO才能实际使用。

答案 2 :(得分:3)

嗯,两者在某种程度上是相关的 - 因为它们都有“尾巴”这个词 - 但它们完全不同......

Tail recursion是一个带有一些特定约束的递归,而tails调用是函数调用。你的问题有点像“动物与猫之间有什么区别?”......

尾调用是尾部位置的函数调用。 示例:f(x)中的f(x)g(★)中的g(f(x)) 反例:f(x)1+f(x)

中的g(f(x))

尾递归是递归调用是尾调用的递归。 示例:f(★)f(x) = f(x)中“=”的右侧f(x,y) = if x == 0 then y else f(x-1,y+x),{{1}} 我已经定义了两个递归函数,它们通过尾调用来调用自己。就是这样。

在使用TCO的语言中,尾部调用不需要任何费用,因此(尾部)递归在常量堆栈中工作,每个人都很高兴。