当我开始学习口齿不清时,我遇到了“尾部递归”这个词。这到底是什么意思?
当前回答
尾部递归函数是一个递归函数,其中递归调用是函数中最后执行的事情。
常规递归函数,我们有堆栈,每次调用递归函数中的递归函数时,都会向调用堆栈添加另一层。在正常递归中空间:O(n)尾递归使空间复杂性从
O(N)=>O(1)
尾部调用优化意味着可以从另一个函数调用函数,而不增加调用堆栈。我们应该在递归解中编写尾部递归。但某些语言实际上不支持其引擎中的尾部递归,该引擎将语言向下编译。自从ecma6以来,规范中就有了尾部递归。但编译js的引擎都没有实现尾部递归。你无法在js中实现O(1),因为编译器本身不知道如何实现这种尾部递归。截至2020年1月1日,Safari是唯一支持尾部呼叫优化的浏览器。Haskell和Java具有尾部递归优化
正则递归阶乘
function Factorial(x) {
//Base case x<=1
if (x <= 1) {
return 1;
} else {
// x is waiting for the return value of Factorial(x-1)
// the last thing we do is NOT applying the recursive call
// after recursive call we still have to multiply.
return x * Factorial(x - 1);
}
}
我们的调用堆栈中有4个调用。
Factorial(4); // waiting in the memory for Factorial(3)
4 * Factorial(3); // waiting in the memory for Factorial(2)
4 * (3 * Factorial(2)); // waiting in the memory for Factorial(1)
4 * (3 * (2 * Factorial(1)));
4 * (3 * (2 * 1));
我们正在进行4次Factorial()调用,空间为O(n)这可能会导致堆栈溢出
尾部递归因子
function tailFactorial(x, totalSoFar = 1) {
//Base Case: x===0. In recursion there must be base case. Otherwise they will never stop
if (x === 0) {
return totalSoFar;
} else {
// there is nothing waiting for tailFactorial to complete. we are returning another instance of tailFactorial()
// we are not doing any additional computaion with what we get back from this recursive call
return tailFactorial(x - 1, totalSoFar * x);
}
}
在进行递归调用后,我们不需要记住任何内容
其他回答
我不是Lisp程序员,但我认为这会有所帮助。
基本上,这是一种编程风格,递归调用是最后一件事。
这个问题有很多很好的答案。。。但我忍不住提出了另一种看法,即如何定义“尾部递归”,或者至少是“正确的尾部递归”。即:是否应该将其视为程序中特定表达式的属性?还是应该将其视为编程语言实现的属性?
关于后一种观点,Will Clinger的一篇经典论文“正确的尾部递归和空间效率”(PLDI 1998)将“正确的尾递归”定义为编程语言实现的属性。该定义被构造为允许忽略实现细节(例如调用堆栈实际上是通过运行时堆栈还是通过堆分配的帧链接列表表示的)。
为了实现这一点,它使用了渐近分析:不是人们通常看到的程序执行时间,而是程序空间使用情况。这样,堆分配的链接列表与运行时调用堆栈的空间使用最终是渐近等价的;因此,人们会忽略编程语言实现的细节(这一细节在实践中当然非常重要,但当试图确定给定的实现是否满足“属性尾部递归”的要求时,可能会让事情变得一团糟)
该论文值得仔细研究,原因如下:
它给出了程序尾部表达式和尾部调用的归纳定义。(这样的定义,以及为什么这样的电话很重要,似乎是这里给出的大多数其他答案的主题。)以下是这些定义,只是为了提供文本的味道:定义1以核心方案编写的程序的尾部表达式归纳如下。lambda表达式的主体是尾部表达式如果(如果E0 E1 E2)是尾部表达式,则E1和E2都是尾部表达式。其他的都不是尾部表达式。定义2尾部调用是作为过程调用的尾部表达式。
(尾部递归调用,或者正如论文所说,“self-tail调用”是尾部调用的一种特殊情况,其中过程本身被调用。)
它为评估核心方案的六个不同“机器”提供了正式定义,其中每个机器都具有相同的可观察行为,除了每个机器所处的渐近空间复杂性类。例如,在为分别为1。基于堆栈的内存管理,2。垃圾收集,但没有尾部调用。垃圾收集和尾部调用,本文继续介绍更高级的存储管理策略,如4。“evlis尾部递归”,在尾部调用的最后一个子表达式参数求值期间不需要保存环境,5。将闭包的环境减少到该闭包的自由变量,以及6。Appel和Shao定义的所谓“空间安全”语义。为了证明这些机器实际上属于六个不同的空间复杂性类,本文针对每对被比较的机器,提供了程序的具体示例,这些程序将揭示一台机器上的渐近空间爆炸,而不是另一台机器。
(现在仔细阅读我的答案,我不确定我是否真的抓住了克林格论文的关键点。但是,唉,我现在不能花更多的时间来研究这个答案。)
与普通递归相比,尾部递归非常快。它很快,因为祖先调用的输出不会写入堆栈以保持跟踪。但在正常递归中,所有祖先调用堆栈中的输出以保持跟踪。
下面是一个使用尾部递归进行阶乘的常见Lisp示例。由于无堆栈的性质,人们可以进行疯狂的大型阶乘计算。。。
(defun ! (n &optional (product 1))
(if (zerop n) product
(! (1- n) (* product n))))
然后为了好玩,你可以尝试(格式化nil“~R”(!25))
尾部递归是指递归算法中最后一条逻辑指令中的最后一个递归调用。
通常在递归中,您有一个基本情况,即停止递归调用并开始弹出调用堆栈。使用一个经典的例子,尽管比Lisp更具C语言,阶乘函数说明了尾部递归。递归调用在检查基本情况条件后发生。
factorial(x, fac=1) {
if (x == 1)
return fac;
else
return factorial(x-1, x*fac);
}
对阶乘的初始调用将是阶乘(n),其中fac=1(默认值),n是要计算阶乘的数字。