我正在尝试编写一个简单的筛子函数来计算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
这将是一个非堆栈消耗的循环结构?我错过了什么?
答案 0 :(得分:54)
你被filter
的懒惰所打击。在(filter ...)
表单中将(doall (filter ...))
更改为recur
,问题就会消失。
更深入的解释:
对filter
的调用返回一个惰性seq,它根据需要实现了已过滤seq的实际元素。如上所述,您的代码在filter
上filter
后filter
堆叠{...},在每次迭代时再添加一个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%。