C++ 尾部递归对传统递归有什么帮助?
我读了关于尾部递归和传统递归之间的区别,发现它提到“但是尾部递归是一种不使用任何堆栈空间的递归形式,因此是一种安全使用递归的方法。” 我很难理解怎么做 用传统和尾部递归法求一个数的阶乘的比较 传统递归C++ 尾部递归对传统递归有什么帮助?,c++,c,recursion,tail-recursion,C++,C,Recursion,Tail Recursion,我读了关于尾部递归和传统递归之间的区别,发现它提到“但是尾部递归是一种不使用任何堆栈空间的递归形式,因此是一种安全使用递归的方法。” 我很难理解怎么做 用传统和尾部递归法求一个数的阶乘的比较 传统递归 /* traditional recursion */ fun(5); int fun(int n) { if(n == 0) return 1; return n * fun(n-1); } /* tail recursion */ fun(5,1) int fun(int
/* traditional recursion */
fun(5);
int fun(int n)
{
if(n == 0)
return 1;
return n * fun(n-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;
}
在这里,调用堆栈看起来像
5 * fact(4)
|
4 * fact(3)
|
3 * fact(2)
|
2 * fact(1)
|
1 * fact(0)
|
1
尾部递归
/* traditional recursion */
fun(5);
int fun(int n)
{
if(n == 0)
return 1;
return n * fun(n-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,60120120。
但是,一旦从基本情况(递归调用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
这种情况下需要嵌套堆栈框架。有关更多信息,请参阅
在尾部递归的情况下,每次调用fun
,变量sofar
都会更新:
fun(5, 1)
fun(4, 5)
fun(3, 20)
fun(2, 60)
fun(1, 120)
120
无需保存当前递归调用的堆栈帧。您对什么是尾部递归感到困惑。看这个:我想知道这个短语。我只知道tail-chaining,如果像
return f()
那样使用,它只会跳到f
,而不是创建一个新的堆栈框架,调用f()
并返回结果。如果尾部链接,f()
然后直接返回到原始调用方。然而,这是一个编译器优化的问题;你应该相信这一点。编译器可能对如何/如果到尾链有不同的想法。尾递归是传统的递归。或者更确切地说,它是递归的一种形式;但它可能是所有形式中最传统的。请参阅,例如:翻译成伪代码,它可能是:def is_in_N(x):如果x==0,则返回true,否则返回is_in_N(x-1)
需要尾部调用才能对其进行尾部调用优化,但尾部调用本身并不保证优化。对于一个要求TCO的类似于语言的方案,这没有问题,您可以说尾部递归不使用额外的堆栈空间,但对于所有其他不要求尾部递归的语言,尾部递归本身并不意味着您不使用堆栈。C++是在这个范畴中的。只进行尾部调用的函数是一个迭代过程,而至少有一个调用不在尾部位置的递归是一个递归过程。这与传统无关。