与传统递归相比,尾部递归究竟有什么帮助
How does tail recursion really help over traditional recursion?
我读到了尾部递归和传统递归之间的区别,发现其中提到"然而,尾部递归是一种不使用任何堆栈空间的递归形式,因此是安全使用递归的一种方式。">
我很难理解怎么做。
传统和尾部递归求数阶乘的比较
传统递归
/* 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
无需保存当前递归调用的堆栈帧。
- 通过递归进行因子分解
- 递归函数计算序列中的平方和(并输出过程)
- 使用递归的数组的最小值.这是怎么回事
- 递归列出所有目录中的C++与Python与Ruby的性能
- 递归计数给定目录的文件和所有目录
- 如何在BST的这个简单递归实现中消除警告
- C++:正在检查LinkedList中的回文-递归方法-错误
- 递归模板化函数不能分配给具有常量限定类型"const tt &"的变量"state"
- 递归无序映射
- TSP递归解的迭代形式
- 如何在Elixir中调用递归函数并行
- 返回递归调用和仅递归调用的区别
- 数组元素打印的递归方法
- 使用递归时获取变量的奇怪值
- 如何在C++中递归地按相反顺序打印集合
- 到连接组件算法的问题(递归)
- 如何使用递归打印修改后的星号三角形图案
- 极小极大递归究竟是如何工作的
- 在 c++ 中扩展欧几里得算法的递归中究竟发生了什么?
- 与传统递归相比,尾部递归究竟有什么帮助