【问题标题】: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.StackOverflowError (NO_SOURCE_FILE:0)

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

【问题讨论】:

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

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


【解决方案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%。

【讨论】:

    猜你喜欢
    • 2013-04-05
    • 2020-03-06
    • 2018-06-19
    • 1970-01-01
    • 1970-01-01
    • 2018-10-29
    相关资源
    最近更新 更多