什么是尾递归?


52

我知道递归的一般概念。在研究quicksort算法时,我遇到了尾递归的概念。在MIT 18:30秒的视频中,快速排序算法的视频中,教授说这是一种尾递归算法。我不清楚尾递归到底意味着什么。

有人可以举例说明这个概念吗?

SO社区在此处提供了一些答案。


告诉我们更多有关术语“ 尾递归”的上下文。链接?引用?
A.Schulz 2012年

@ A.Schulz我已将链接指向上下文。
极客

5
看看“ 什么是尾递归? ”在计算器

2
@ajmartin问题在堆栈溢出方面处于临界点,但在计算机科学上确实是热门话题,因此原则上计算机科学应该产生更好的答案。在这里还没有发生过,但是在这里重新提问以寻求更好的答案仍然是可以的。极客,您应该已经提到过您先前关于SO的问题,这样人们就不会重复已经说过的话。
吉尔斯(Gilles)'所以别再邪恶了'

1
另外,您应该说什么是模棱两可的部分,或者为什么您对以前的答案不满意,我认为人们提供了很好的答案,但是是什么导致您再次提出这个问题呢?

Answers:


52

尾递归是递归的一种特殊情况,其中调用函数在进行递归调用后不再进行任何计算。例如功能

int f(int x,int y){
  如果(y == 0){
    返回x;
  }

  返回f(x * y,y-1);
}

是尾递归的(因为最终指令是递归调用),而此函数不是尾递归的:

int g(int x){
  如果(x == 1){
    返回1;
  }

  整数y = g(x-1);

  返回x * y;
}

因为它会在递归调用返回后进行一些计算。

尾递归很重要,因为它可以比常规递归更有效地实现。当我们进行普通的递归调用时,我们必须将返回地址压入调用堆栈,然后跳转到被调用的函数。这意味着我们需要一个调用堆栈,其大小在递归调用的深度上是线性的。当我们有尾递归时,我们知道一旦从递归调用返回,我们也将立即返回,因此我们可以跳过整个递归函数链返回,直接返回到原始调用者。这意味着我们完全不需要所有递归调用的调用栈,并且可以将最终调用实现为简单的跳转,从而节省了空间。


2
您写道:“这意味着我们完全不需要所有递归调用的调用栈”。调用堆栈将一直存在,只是不需要将返回地址写入调用堆栈中,对吗?
怪胎

2
它在某种程度上取决于您的计算模型:)但是,是的,在真实计算机上,调用堆栈仍然存在,我们只是没有使用它。
马特·路易斯

如果这是最后一次调用,但在for循环中该怎么办?所以,你做你的计算上面,但他们中的一些for循环一样def recurse(x): if x < 0 return 1; for i in range 100{ (do calculations) recurse(x)}
thed0ctor

13

简而言之,尾部递归是一种递归,其中编译器可以用“ goto”命令替换递归调用,因此编译后的版本将不必增加堆栈深度。

有时设计尾部递归函数需要您创建带有附加参数的辅助函数。

例如,这不是尾递归函数:

int factorial(int x) {
    if (x > 0) {
        return x * factorial(x - 1);
    }
    return 1;
}

但这是一个尾递归函数:

int factorial(int x) {
    return tailfactorial(x, 1);
}

int tailfactorial(int x, int multiplier) {
    if (x > 0) {
        return tailfactorial(x - 1, x * multiplier);
    }
    return multiplier;
}

因为编译器可以使用以下代码(伪代码)将递归函数重写为非递归函数:

int tailfactorial(int x, int multiplier) {
    start:
    if (x > 0) {
        multiplier = x * multiplier;
        x--;
        goto start;
    }
    return multiplier;
}

编译器的规则非常简单:找到“ return thisfunction(newparameters);”时,将其替换为“ parameters = newparameters; goto start;”。但是,只有在直接返回递归调用返回的值的情况下,才可以这样做。

如果可以像这样替换函数中的所有递归调用,则它是尾递归函数。


13

我的答案是基于《计算机程序的结构和解释》一书中的解释。我强烈推荐这本书给计算机科学家。

方法A:线性递归过程

(define (factorial n)
 (if (= n 1)
  1
  (* n (factorial (- n 1)))))

方法A的过程形状如下:

(factorial 5)
(* 5 (factorial 4))
(* 5 (* 4 (factorial 3)))
(* 5 (* 4 (* 3 (factorial 2))))
(* 5 (* 4 (* 3 (* 2 (factorial 1)))))
(* 5 (* 4 (* 3 (* 2 (* 1)))))
(* 5 (* 4 (* 3 (* 2))))
(* 5 (* 4 (* 6)))
(* 5 (* 24))
120

方法B:线性迭代过程

(define (factorial n)
 (fact-iter 1 1 n))

(define (fact-iter product counter max-count)
 (if (> counter max-count)
  product
  (fact-iter (* counter product)
             (+ counter 1)
             max-count)))

方法B的流程形状如下所示:

(factorial 5)
(fact-iter 1 1 5)
(fact-iter 1 2 5)
(fact-iter 2 3 5)
(fact-iter 6 4 5)
(fact-iter 24 5 5)
(fact-iter 120 6 5)
120

线性迭代过程(方法B)在恒定空间中运行,即使该过程是递归过程也是如此。还应该注意的是,在这种方法中,设置变量定义了在任意点viz的过程状态。{product, counter, max-count}。这也是尾递归允许编译器优化的一项技术。

在方法A中,解释器维护着更多的隐藏信息,这些信息基本上是延迟操作的链条。


5

尾递归是递归的一种形式,其中递归调用是函数中的最后一条指令(这是尾部来自何处)。此外,递归调用不能由对存储先前值的存储单元的引用(该函数的参数以外的引用)组成。这样,我们不必关心先前的值,并且所有递归调用都只需一个堆栈框架就足够了。尾递归是优化递归算法的一种方法。另一个优点/优化是,有一种简便的方法可以将尾递归算法转换为使用迭代而不是递归的等效算法。是的,快速排序的算法确实是尾递归的。

QUICKSORT(A, p, r)
    if(p < r)
    then
        q = PARTITION(A, p, r)
        QUICKSORT(A, p, q–1)
        QUICKSORT(A, q+1, r)

这是迭代版本:

QUICKSORT(A)
    p = 0, r = len(A) - 1
    while(p < r)
        q = PARTITION(A, p, r)
        r = q - 1

    p = 0, r = len(A) - 1
    while(p < r)
        q = PARTITION(A, p, r)
        p = q + 1
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.