与传统递归相比,尾部递归究竟有什么帮助

How does tail recursion really help over traditional recursion?

本文关键字:递归 究竟 什么 尾部 帮助 传统      更新时间:2023-10-16

我读到了尾部递归和传统递归之间的区别,发现其中提到"然而,尾部递归是一种不使用任何堆栈空间的递归形式,因此是安全使用递归的一种方式。">

我很难理解怎么做。

传统和尾部递归求数阶乘的比较

传统递归

/* traditional recursion */
fun(5);

int fun(int n)
{
 if(n == 0)
  return 1;

 return n * fun(n-1);
}

在这里,调用堆栈看起来像

5 * fact(4)
      |
   4 * fact(3)
          |
       3 * fact(2)
             |
         2 * fact(1)
               |
            1 * fact(0)
                  |
                  1

尾部递归

/* tail recursion */
fun(5,1)

int fun(int n, int sofar)
{
 int ret = 0;

 if(n == 0)
  return sofar;

 ret = fun(n-1,sofar*n);

 return ret;
}

然而,即使在这里,变量"sofar"在不同的点上也会保持-5,20,6010120。但是,一旦从递归调用#4的基本情况调用return,它仍然必须返回120到递归调用#3,然后返回#2、#1,然后返回main。所以,我的意思是说,堆栈被使用了,每次你返回到上一个调用时,都可以看到那个时间点的变量,这意味着它在每一步都被保存。

除非尾递归是这样写的,否则我无法理解它是如何节省堆栈空间的。

/* tail recursion */
fun(5,1)
int fun(int n, int sofar)
{
 int ret = 0;

 if(n == 0)
  return 'sofar' back to main function, stop recursing back; just a one-shot return

 ret = fun(n-1,sofar*n);

 return ret;
}

PS:我读过一些关于SO的线程,并逐渐理解了什么是尾部递归,然而,这个问题更多地与它为什么节省堆栈空间有关。我找不到类似的问题在哪里讨论。

诀窍是,如果编译器注意到尾部递归,它可以编译goto。它将生成如下代码:

int fun_optimized(int n, int sofar)
{
start:
    if(n == 0)
       return sofar;
    sofar = sofar*n;
    n = n-1;
    goto start;
}

正如您所看到的,堆栈空间在每次迭代中都被重用。

请注意,只有当递归调用是函数中的最后一个操作,即尾部递归时,才能进行此优化(尝试手动对非尾部情况进行此操作,您会发现这是不可能的(。

当函数调用(递归(作为最终操作执行时,函数调用是尾部递归的由于当前递归实例已在该点执行完毕,因此无需维护其堆栈帧

在这种情况下,在当前堆栈帧的顶部创建堆栈帧只不过是浪费
当编译器将递归识别为尾部递归时,它不会为每个调用创建嵌套堆栈帧,而是使用当前堆栈帧。这实际上相当于goto语句。这使得该函数调用是迭代的,而不是递归的。

注意,在传统递归中,每个递归调用都必须在编译器执行乘法运算之前完成:

fun(5)
5 * fun(4)
5 * (4 * fun(3))
5 * (4 * (3 * fun(2)))
5 * (4 * (3 * (2 * fun(1))))
5 * (4 * (3 * (2 * 1)))
120  

这种情况下需要嵌套堆栈框架。有关详细信息,请查看wiki。

在尾部递归的情况下,每次调用fun,变量sofar都会更新:

fun(5, 1)
fun(4, 5)
fun(3, 20)
fun(2, 60)
fun(1, 120)
120  

无需保存当前递归调用的堆栈帧。