【问题标题】:How does tail recursion really help over traditional recursion?尾递归如何真正帮助传统递归?
【发布时间】:2015-09-20 09:22:10
【问题描述】:

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

我很难理解如何。

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

传统递归

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

【问题讨论】:

  • 你对什么是尾递归感到困惑。看到这个:stackoverflow.com/questions/2693683/tail-recursion-in-c
  • 我想知道这句话。我只知道尾链,如果像return f() 一样使用它只会跳转到f,而不是创建一个新的堆栈帧,调用F() 并返回结果。如果是尾链,f() 然后直接返回给原始调用者。但是,这是编译器优化的问题;你应该依靠它。编译器可能有一个非常不同的想法如何/如果尾链。
  • 尾递归是传统的递归。或者更确切地说,它是一种递归形式;但它可能是所有形式中最传统的。参见,例如,en.wikipedia.org/wiki/Recursion#Recursively_defined_sets:翻译成伪代码,它可能是:def is_in_N(x): if x == 0 then return true else return is_in_N(x-1)
  • 需要尾调用才能对其进行尾调用优化,但尾调用本身并不能保证优化。对于像 Scheme 这样的语言,它要求 TCO 没有问题,你可以说尾递归不使用额外的堆栈空间,但对于所有其他不需要尾递归的语言,它 itelf 并不意味着你不会使用堆栈. C++ 就属于这一类。只进行尾部调用的函数是一个迭代过程,而至少一个调用不在尾部位置的递归是一个递归过程。这与传统无关。

标签: c++ c recursion tail-recursion


【解决方案1】:

诀窍是,如果编译器注意到尾递归,它可以编译 goto 代替。它会生成类似下面的代码:

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

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

如您所见,每次迭代都会重复使用堆栈空间。

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

【讨论】:

    【解决方案2】:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-06-21
      • 2011-02-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-21
      • 1970-01-01
      相关资源
      最近更新 更多