Recursion Scheme中的递归和调用堆栈

Recursion Scheme中的递归和调用堆栈,recursion,stack,scheme,callstack,tail-recursion,Recursion,Stack,Scheme,Callstack,Tail Recursion,我是一名大学生,学习Racket/Scheme和C作为CS学位的入门课程 我在网上读到,在C语言中使用迭代而不是递归通常是最佳实践,因为递归由于将堆栈帧保存到调用堆栈上而代价高昂 现在在Scheme之类的函数式语言中,递归一直在使用。我知道尾部递归在Scheme中是一个巨大的优势,据我所知,它只需要一个堆栈框架(有人能澄清这一点吗?),不管递归有多深 我的问题是:非尾部递归呢?每个函数应用程序是否保存在调用堆栈上?如果我能得到一个如何工作的简要概述或指向我的资源,我将不胜感激;我似乎在任何地方都

我是一名大学生,学习Racket/Scheme和C作为CS学位的入门课程

我在网上读到,在C语言中使用迭代而不是递归通常是最佳实践,因为递归由于将堆栈帧保存到调用堆栈上而代价高昂

现在在Scheme之类的函数式语言中,递归一直在使用。我知道尾部递归在Scheme中是一个巨大的优势,据我所知,它只需要一个堆栈框架(有人能澄清这一点吗?),不管递归有多深


我的问题是:非尾部递归呢?每个函数应用程序是否保存在调用堆栈上?如果我能得到一个如何工作的简要概述或指向我的资源,我将不胜感激;我似乎在任何地方都找不到明确说明这一点的方案。

方案要求消除尾部调用。非尾部调用递归的代码将需要额外的堆栈帧

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);
}
暂时让我们假设javascript支持尾部调用优化,第二个函数定义将只使用一个堆栈帧,而第一个函数定义由于
+
将需要额外的堆栈帧

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 3+总和(2) 3+和(2)=3+2+和(1) 3+和(2)=3+2+和(1)=3+2+1+和(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+和(2)=3+2+和(1)=6 3+和(2)=6 6. 第二个定义的调用如下所示

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 和(3,和(2))=和(5,和(1))=和(6,和(0))=6
是的,处于非尾部位置的调用需要向堆栈中添加一些内容,以便在调用返回时知道如何恢复工作。(有关堆栈、尾部调用和非尾部调用的更全面的解释,请参阅Steele的论文,该论文揭穿了“昂贵的过程调用”的神话,或被认为有害的过程调用实现,或Lambda:与


但是Racket(以及许多其他方案和一些其他语言)实现了“堆栈”,这样即使有深度递归,也不会耗尽堆栈空间。换句话说,Racket没有堆栈溢出。这样做的一个原因是,支持深度递归的技术与支持一级连续体的技术是一致的,Scheme标准也要求这种技术。你可以在Clinger等人的文章中读到它们。

当你说:“第一个,由于+将需要额外的堆栈帧”时,任何时候都只会有一个额外的堆栈帧?我相信C为每个函数调用添加了一个新的堆栈框架,所以这是否意味着Scheme中的递归以这种方式更有效?如果我遗漏了什么,请告诉我;这只是一个奇怪的想法,我要到明年才能参加任何深入的课程。顺便说一下,谢谢你的回答。尾部调用优化是编译器的魔法。Scheme标准要求编译器实现它。中的尾部调用优化使用跳转指令而不是调用来实现。因此,尾部调用优化递归效率更高。啊,如果没有尾部递归(比如在第一个函数中),那么Scheme中的递归效率在C中是相同的?有更多细节,Scheme被解释,C被编译。假设所有条件都相等,跳转指令将比函数调用更快,或者没有尾部TCO的递归将更糟糕。正在编译的GCC可能实现了比Scheme解释器更快的非TCO递归。@Bhaskar Scheme不需要解释,并且存在多个编译器。我不知道Racket不能有堆栈溢出。。。谢谢我将仔细阅读你这个周末提供的一些文件。