递归函数导致堆栈溢出

Posted

技术标签:

【中文标题】递归函数导致堆栈溢出【英文标题】:Recursive function causing a stack overflow 【发布时间】:2011-02-26 04:01:44 【问题描述】:

我正在尝试编写一个简单的筛子函数来计算 clojure 中的素数。我已经看到this 关于编写高效筛子函数的问题,但我还没有到那个地步。现在我只是想写一个非常简单(而且很慢)的筛子。这是我想出的:

(defn sieve [potentials primes]
  (if-let [p (first potentials)]
    (recur (filter #(not= (mod % p) 0) potentials) (conj primes p))
    primes))

对于小范围,它可以正常工作,但对于大范围会导致堆栈溢出:

user=> (sieve (range 2 30) [])
[2 3 5 7 11 13 17 19 23 29]
user=> (sieve (range 2 15000) [])
java.lang.***Error (NO_SOURCE_FILE:0)

我认为通过使用recur 这将是一个不消耗堆栈的循环结构?我错过了什么?

【问题讨论】:

+1 用于在您的问题标题中出现堆栈溢出 有趣;为我工作。您在什么平台上使用什么版本的 Clojure,使用什么 JVM?你能在不溢出的情况下运行(range 2 15000) 吗? Ubuntu 9.10、Java 1.6.0_15、Clojure 1.2.0 的最新快照 是的,我在 15000 处溢出。你能跑一百万而不溢出吗? 标题应该是“非递归函数导致堆栈溢出”。 【参考方案1】:

filter 的懒惰让你受到打击。在recur 表单中将(filter ...) 更改为(doall (filter ...)),问题应该会消失。

更深入的解释:

filter 的调用返回一个惰性序列,它根据需要具体化过滤序列的实际元素。如所写,您的代码在filter 上堆叠filterfilter...,在每次迭代中增加一层filtering;在某些时候,这会爆炸。解决方案是在每次迭代时强制整个结果,以便下一次将对完全实现的 seq 进行过滤并返回完全实现的 seq,而不是添加额外的延迟 seq 处理层;这就是doall 所做的。

【讨论】:

谢谢!这解决了我的问题。很好的解释。 任何想法如何找到这个?也许像macroexpand这样的东西? 看看堆栈跟踪,我会说。一堆clojure.lang.LazySeq 方法调用将很好地表明问题与惰性有关。【参考方案2】:

从算法上讲,问题在于您在没有更多目的时继续过滤。尽早停止可以实现递归深度的二次减小(sqrt(n)n):

(defn sieve [potentials primes]    
  (if-let [p (first potentials)]
      (if (> (* p p) (last potentials))
        (concat primes potentials)
        (recur (filter (fn [n] (not= (mod n p) 0)) potentials)
               (conj primes p)))
    primes))

在 16,000 次(仅执行 30 次迭代而不是 1862 次)和 160,000 次时运行良好,on ideone。即使没有doall,运行速度也快了 5%。

【讨论】:

以上是关于递归函数导致堆栈溢出的主要内容,如果未能解决你的问题,请参考以下文章

堆栈溢出一般是由啥原因导致的?

在递归函数中处理大数组时堆栈溢出

递归代码在数组列表偏大的情况下会导致堆栈溢出。一个解决办法

Java如何在不使用递归的情况下导致栈溢出?

无限递归函数->堆栈溢出错误

为啥带有 setTimeout 的函数不会导致堆栈溢出