Scheme中的递归和调用堆栈

时间:2014-01-24 00:06:01

标签: recursion stack scheme callstack tail-recursion

我是一名大学生,学习球拍/计划和C作为我的CS学位的入门课程。

我在网上看过,通常最好使用迭代而不是C中的递归,因为递归是昂贵的,因为将堆栈帧保存到callstack等...

现在,在像Scheme这样的函数式语言中,一直使用递归。我知道尾部递归在Scheme中是一个巨大的好处,我的理解是它只需要一个堆栈帧(任何人都可以澄清这个吗?)无论递归有多深。

我的问题是:非尾递归怎么样?每个函数应用程序是否都保存在callstack上?如果我能够简要概述一下这是如何工作的,或者将我指向一个资源,我将不胜感激;我无法在任何明确说明这一点的地方找到一个。

2 个答案:

答案 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了解它们。