递归函数导致堆栈溢出

时间:2010-06-01 01:05:47

标签: recursion clojure primes lazy-evaluation lazy-sequences

我正在尝试编写一个简单的筛子函数来计算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.StackOverflowError (NO_SOURCE_FILE:0)

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

2 个答案:

答案 0 :(得分:54)

你被filter的懒惰所打击。在(filter ...)表单中将(doall (filter ...))更改为recur,问题就会消失。

更深入的解释:

filter的调用返回一个惰性seq,它根据需要实现了已过滤seq的实际元素。如上所述,您的代码在filterfilterfilter堆叠{...},在每次迭代时再添加一个filter级别;在某些时候,这会爆发。解决方案是在每次迭代时强制执行整个结果,以便下一个将在完全实现的seq上进行过滤并返回完全实现的seq,而不是添加额外的lazy seq处理层;这就是doall的作用。

答案 1 :(得分:0)

从算法上讲,问题是当没有更多目的时你会继续过滤。尽早停止可以实现递归深度的二次减少(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%。