我是一名大学生,学习球拍/计划和C作为我的CS学位的入门课程。
我在网上看过,通常最好使用迭代而不是C中的递归,因为递归是昂贵的,因为将堆栈帧保存到callstack等...
现在,在像Scheme这样的函数式语言中,一直使用递归。我知道尾部递归在Scheme中是一个巨大的好处,我的理解是它只需要一个堆栈帧(任何人都可以澄清这个吗?)无论递归有多深。
我的问题是:非尾递归怎么样?每个函数应用程序是否都保存在callstack上?如果我能够简要概述一下这是如何工作的,或者将我指向一个资源,我将不胜感激;我无法在任何明确说明这一点的地方找到一个。
答案 0 :(得分:3)
Scheme需要消除尾部呼叫。非尾调用递归的代码将需要额外的堆栈帧。
让我们假设javascript支持尾调用优化,这些函数定义中的第二个将仅使用1个堆栈帧,而第一个,由于+
将需要额外的堆栈帧。< / p>
function sum(n) {
if (n === 0)
return n;
return n + sum(n - 1);
}
function sum(n) {
function doSum(total, n) {
if (n === 0)
return total;
return doSum(total + n, n - 1);
}
return doSum(0, n);
}
通过将计算结果放在堆栈上,可以为尾调用优化编写许多递归函数
第一个定义的概念性调用看起来像这样
3 + sum(2) 3 + sum(2) = 3 + 2 + sum(1) 3 + sum(2) = 3 + 2 + sum(1) = 3 + 2 + 1 + sum(0) 3 + sum(2) = 3 + 2 + sum(1) = 3 + 2 + 1 + sum(0) = 3 + 2 + 1 + 0 3 + sum(2) = 3 + 2 + sum(1) = 3 + 2 + 1 + sum(0) = 6 3 + sum(2) = 3 + 2 + sum(1) = 6 3 + sum(2) = 6 6
第二个定义的调用看起来像这样
sum(3, sum(2)) = sum(5, sum(1)) = sum(6, sum(0)) = 6
答案 1 :(得分:2)
是的,非尾部位置的调用需要向堆栈添加内容,以便它知道如何在调用返回时恢复工作。 (有关堆栈,尾调用和非尾调用的更全面解释,请参阅Steele的论文揭穿'昂贵的过程调用'神话,或者,程序调用实现被认为是有害的,或者,Lambda:终极GOTO 从lambda papers page at readscheme.org链接。)
但是Racket(以及许多其他Scheme和其他一些语言)实现了“堆栈”,这样即使你有很长的递归,你也不会耗尽堆栈空间。换句话说,Racket没有堆栈溢出。其中一个原因是支持深度递归的技术与支持第一类连续的技术一致,Scheme标准也需要这些技术。您可以通过Clinger等人的Implementation Strategies for First-Class Continuations了解它们。