递归函数中堆栈溢出的原因

时间:2015-12-16 17:49:09

标签: java recursion scheme iteration sicp

在28分钟左右的this video中,学生要求Brian Harvey在编写程序时是否应该在递归过程中使用迭代过程。他说不,因为

你的程序不会遇到空间限制。就内存中的内容的位置而言,你必须比你解释程序真正影响它的方式有更多的控制权。

由于这不是一个计划课程,我以为他一般都在谈论编程语言。当他说""你的程序不会遇到空间限制。",他是否忽视堆栈溢出?我对他的回答很困惑,因为没有堆栈溢出意味着你已经用完函数调用了空间?而且我不会从地方性的角度理解任何事情"部分。堆栈溢出可能发生在scheme,java和其他语言中。我是正确还是我误解了他的陈述?

3 个答案:

答案 0 :(得分:1)

您所指的视频是计算机科学讲座。计算机科学在很大程度上是理论性的,并且解决了许多与实用性无关的计算细节。在这种情况下,正如他在讲座开始时说的那样,今天的计算机规模庞大且速度足够快,性能很少成为问题。

内存位置与任何语言的StackOverflowException无关。实际上,内存局部性指的是SRAM(静态RAM),它保存了当总线从内存中检索数据时可以带入的相邻数据的缓存(可以是磁盘或RAM)。从此缓存中获取数据比从内存中获取数据要快,因此如果多个连续操作所需的所有数据都在缓存中,程序将运行得更快。

现在这一切都非常低级。在大多数(如果不是全部)现代语言(如Java)的背后,有一个编译器正在进行许多低级优化。这首先意味着,您可以做很少的事情来优化低级别的代码,尤其是在不干扰编译器优化的情况下。其次,(就像他在你所指的片段之后说的那样),除非你是一个资源密集型游戏,否则不值得花时间去担心性能(除非你有明显的性能问题,但是#39;更可能表示代码中的其他问题。)

答案 1 :(得分:1)

如今,我们巨大的内存堆栈溢出通常是无休止递归的标志,就像迭代一样,非停止程序是无限循环的标志。

所以他是对的。

答案 2 :(得分:1)

我的递归过程会导致堆栈溢出吗? 这取决于你设计的递归过程的类型,根据问题,大多数天真的重新调整可以转换为尾调用(尾调用优化' TCO'语言),允许递归运行不使用突变或其他有状态的事物来保持不变的存储空间。

在方案中,一个迭代过程:

(let ((i 0)
      (max 10))
  (let loop ()
    (cond ((< i max)
           (printf "~A~N" i)
           (set! i (+ i 1))
           (loop))
          (else i))))

此过程使用常量内存,该内存等于在堆栈上存储循环调用所需的空间。这个过程不是一个函数,它使用变异来迭代(它也是一个递归;)但是..)。

在计划中,有两次递归:

(define (fact-1 n)
  (cond ((eq? n 1) n)
        (else (* n (fact-1 (- n 1))))))

(define (fact-2 n carry)
  (cond ((eq? n 1) carry)
        (else (fact-2 (- n 1) (* carry n)))))

Fact-1是一个正常的递归,功能非常强大,没有状态变化,相反,随着每个fact-1调用创建新的词法闭包,内存使用量增加,最终耗尽堆栈。它像

一样增长
=>(fact-1 10)
..(* 10 (fact-1 9))
..(* 10 (* 9 (fact-1 8)))
..(* 10 (* 9 (* 8 (fact-1 7))))
..     .....
..(* 10 (... (* 2 1) ...))
..     .....
..(* 10 362880)
=>3628800

虽然Fact-2是递归的,但是以尾部形式,所以不是构建堆栈,而是在基本情况下折叠调用,值会向前传递,我们得到这个:

=>(fact-2 10 1)
..(fact-2 9 10)
..(fact-2 8 90)

..(fact-2 7 720)
.. .......(fact-2 1 362880)
=>3628800

这相当于将事实1变成一个交互过程,但没有变异,因为值是向前传递而不是赋值。请注意,每个调用仍会产生一个新的词法闭包,但由于该函数不会返回到原始调用者,而是返回到原始调用者堆栈位置,编译器可以丢弃先前的闭包而不是将它们嵌套在彼此内部,重新绑定每个递归级别的变量。

那么我应该在哪里使用递归与迭代 这完全取决于要设计的过程和使用的语言。如果您的语言不支持TCO,那么您将只需要使用浅递归,并以有状态的方式编写循环(递归或迭代)过程。如果你确实有TCO,那么最好使用递归,尾调用或有状态的东西,或者它们的组合。并非所有递归过程都可以以尾部形式编写,并非所有迭代过程都可以写为递归。如果您担心内存使用情况,并且希望进行深度递归,则必须使用Tail-calls。

注意: 你们中的一些人可能已经注意到了,但是第一个程序实际上也是一个尾调用,但是这个例子仍然说明了正常迭代的状态!无论所有有效输入如何,都在恒定的最大内存中运行。