当我开始学习口齿不清时,我遇到了“尾部递归”这个词。这到底是什么意思?
当前回答
术语文件对尾部递归的定义有这样的说法:
尾部递归/n/
如果您还没有厌倦它,请参阅尾部递归。
其他回答
这是《计算机程序的结构和解释》中关于尾部递归的摘录。
在对比迭代和递归时,我们必须小心不要将递归过程的概念与递归过程。当我们将过程描述为递归时指过程定义所指的句法事实(直接或间接)到程序本身。但当我们将过程描述为遵循一种模式,即线性递归,我们谈论的是过程如何演变,而不是如何编写过程的语法。这似乎令人不安我们将递归过程(如事实iter)称为生成迭代过程。然而,这个过程实际上是迭代的:它的状态被其三个状态变量完全捕获解释器只需要跟踪三个变量执行该过程。过程和程序之间的区别可能是令人困惑的是,大多数通用语言的实现(包括Ada、Pascal和C) 以这样一种方式设计过程消耗的内存量会随着过程调用,迭代。因此,这些语言可以描述迭代仅通过使用专用的“循环构造”来处理例如do、repeat、until、for和while。实施方案不具有此缺陷。它将在恒定空间中执行迭代过程,即使迭代过程由递归过程描述。一具有此属性的实现称为尾部递归。用一个尾部递归实现,可以使用普通过程调用机制,使特殊迭代构造只作为句法糖有用。
许多人已经在这里解释了递归。我想引用Riccardo Terrell的《.NET中的并发性,并发和并行编程的现代模式》一书中关于递归的一些优点的一些想法:
“函数递归是FP中迭代的自然方式,因为它避免状态突变。在每次迭代期间,都会传递一个新值而不是被更新(变异)。在里面此外,可以编写递归函数,使您的程序更加模块化,并引入了开发机会并行化。"
以下是同一本书中关于尾部递归的一些有趣注释:
尾部调用递归是一种转换规则递归的技术函数转换为可处理大型输入的优化版本没有任何风险和副作用。注:尾部调用作为优化的主要原因是提高数据位置、内存使用率和缓存使用率。通过做尾巴调用时,被调用者使用与调用者相同的堆栈空间。这减少了记忆压力。它略微改善了缓存,因为存储器被后续调用方重用,并且可以留在缓存中,而不是驱逐旧的缓存线,为新的缓存腾出空间线
这里有一个例子,而不是用文字来解释。这是阶乘函数的Scheme版本:
(define (factorial x)
(if (= x 0) 1
(* x (factorial (- x 1)))))
下面是一个阶乘的尾部递归版本:
(define factorial
(letrec ((fact (lambda (x accum)
(if (= x 0) accum
(fact (- x 1) (* accum x))))))
(lambda (x)
(fact x 1))))
在第一个版本中,您会注意到对事实的递归调用被馈送到乘法表达式中,因此在进行递归调用时,状态必须保存在堆栈中。在尾部递归版本中,没有其他S表达式等待递归调用的值,并且由于没有进一步的工作要做,状态不必保存在堆栈上。通常,Scheme尾部递归函数使用常数堆栈空间。
这个问题有很多很好的答案。。。但我忍不住提出了另一种看法,即如何定义“尾部递归”,或者至少是“正确的尾部递归”。即:是否应该将其视为程序中特定表达式的属性?还是应该将其视为编程语言实现的属性?
关于后一种观点,Will Clinger的一篇经典论文“正确的尾部递归和空间效率”(PLDI 1998)将“正确的尾递归”定义为编程语言实现的属性。该定义被构造为允许忽略实现细节(例如调用堆栈实际上是通过运行时堆栈还是通过堆分配的帧链接列表表示的)。
为了实现这一点,它使用了渐近分析:不是人们通常看到的程序执行时间,而是程序空间使用情况。这样,堆分配的链接列表与运行时调用堆栈的空间使用最终是渐近等价的;因此,人们会忽略编程语言实现的细节(这一细节在实践中当然非常重要,但当试图确定给定的实现是否满足“属性尾部递归”的要求时,可能会让事情变得一团糟)
该论文值得仔细研究,原因如下:
它给出了程序尾部表达式和尾部调用的归纳定义。(这样的定义,以及为什么这样的电话很重要,似乎是这里给出的大多数其他答案的主题。)以下是这些定义,只是为了提供文本的味道:定义1以核心方案编写的程序的尾部表达式归纳如下。lambda表达式的主体是尾部表达式如果(如果E0 E1 E2)是尾部表达式,则E1和E2都是尾部表达式。其他的都不是尾部表达式。定义2尾部调用是作为过程调用的尾部表达式。
(尾部递归调用,或者正如论文所说,“self-tail调用”是尾部调用的一种特殊情况,其中过程本身被调用。)
它为评估核心方案的六个不同“机器”提供了正式定义,其中每个机器都具有相同的可观察行为,除了每个机器所处的渐近空间复杂性类。例如,在为分别为1。基于堆栈的内存管理,2。垃圾收集,但没有尾部调用。垃圾收集和尾部调用,本文继续介绍更高级的存储管理策略,如4。“evlis尾部递归”,在尾部调用的最后一个子表达式参数求值期间不需要保存环境,5。将闭包的环境减少到该闭包的自由变量,以及6。Appel和Shao定义的所谓“空间安全”语义。为了证明这些机器实际上属于六个不同的空间复杂性类,本文针对每对被比较的机器,提供了程序的具体示例,这些程序将揭示一台机器上的渐近空间爆炸,而不是另一台机器。
(现在仔细阅读我的答案,我不确定我是否真的抓住了克林格论文的关键点。但是,唉,我现在不能花更多的时间来研究这个答案。)
尾部递归是你现在的生活。您不断重复使用相同的堆栈帧,因为没有理由或方法返回到“先前”帧。过去已经结束,可以抛弃。你得到一帧,永远走向未来,直到你的过程不可避免地消亡。
当您考虑到某些进程可能会使用额外的帧,但如果堆栈没有无限增长,则仍然被认为是尾部递归时,这种类比就失败了。