尾递归如何真正帮助传统递归?

How does tail recursion really help over traditional recursion?

我正在阅读尾递归和传统递归之间的区别,发现它提到 "Tail Recursion however is a form of recursion that doesn’t use any stack space, and thus is a way to use recursion safely."'

我很难理解。

比较使用传统递归和尾递归查找数字的阶乘

传统递归

/* 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,60,120,120。 但是一旦从递归调用#4 的基本情况调用 return,它仍然必须 return120 到递归调用#3,然后到#2、#1 并返回到 main。 所以,我的意思是说堆栈被使用了,每次你return到上一个调用,那个时间点的变量都可以看到,这意味着它在每一步都被保存。

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

/* 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 的线程并开始理解什么是尾递归,但是,这个问题更多地与它为什么节省堆栈 space 有关。我在讨论这个的地方找不到类似的问题。

诀窍在于,如果编译器注意到尾递归,它可以编译 goto。它将生成类似于以下代码的内容:

int fun_optimized(int n, int sofar)
{
start:
    if(n == 0)
       return sofar;

    sofar = sofar*n;
    n = n-1;
    goto start;
}

如您所见,堆栈 space 被重复用于每次迭代。

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

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

在这种情况下,在当前栈帧之上创建一个栈帧无异于浪费。
当编译器将递归识别为尾递归时,它不会为每个调用创建嵌套堆栈帧,而是使用当前堆栈帧。这实际上等同于 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  

不需要保存当前递归调用的栈帧。