尾递归如何真正有助于传统的递归?

时间:2015-07-02 17:57:37

标签: c++ c recursion tail-recursion

我正在阅读尾递归和传统递归之间的区别,并发现它提到“Tail Recursion然而是一种不使用任何堆栈空间的递归形式,因此是一种安全使用递归的方法。” “

我很难理解如何。

使用Traditional和尾递归比较数字的发现阶乘

传统递归

/* 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,它仍然必须返回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上阅读了几个线程,并开始了解尾递归是什么,但是,这个问题与它为什么节省堆栈空间更相关。在讨论这个问题时,我找不到类似的问题。

2 个答案:

答案 0 :(得分:6)

诀窍是如果编译器注意到尾递归,它可以编译<ul> <li><a href="#">First link</a> </li> <li><a href="#">First link</a> </li> <li><a href="#">First link</a> </li> <li><a href="#">First link</a> </li> <li><a href="#">First link</a> </li> <li><a href="#">First link</a> </li> </ul>。它将生成类似以下代码的内容:

goto

正如您所看到的,堆栈空间将在每次迭代时重复使用。

请注意,只有在递归调用是函数中的最后一个操作时才能进行此优化,即尾递归(尝试手动执行非尾部情况,并且您需要执行此操作) ;看到那是不可能的。)

答案 1 :(得分:1)

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

在这种情况下,在当前堆栈框架顶部创建堆栈框架只不过是浪费 当编译器将递归识别为尾递归时,它不会为每个调用创建嵌套堆栈帧,而是使用当前堆栈帧。这相当于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  

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