Fast Prime Number Generation in Clojure

后端 未结 16 1793
萌比男神i
萌比男神i 2020-12-02 12:30

I\'ve been working on solving Project Euler problems in Clojure to get better, and I\'ve already run into prime number generation a couple of times. My problem is that it is

相关标签:
16条回答
  • 2020-12-02 13:00

    Here is a Clojure solution. i is the current number being considered and p is a list of all prime numbers found so far. If division by some prime numbers has a remainder of zero, the number i is not a prime number and recursion occurs with the next number. Otherwise the prime number is added to p in the next recursion (as well as continuing with the next number).

    (defn primes [i p]
      (if (some #(zero? (mod i %)) p)
        (recur (inc i) p)
        (cons i (lazy-seq (primes (inc i) (conj p i))))))
    (time (do (doall (take 5001 (primes 2 []))) nil))
    ; Elapsed time: 2004.75587 msecs
    (time (do (doall (take 10001 (primes 2 []))) nil))
    ; Elapsed time: 7700.675118 msecs
    

    Update: Here is a much slicker solution based on this answer above. Basically the list of integers starting with two is filtered lazily. Filtering is performed by only accepting a number i if there is no prime number dividing the number with remainder of zero. All prime numbers are tried where the square of the prime number is less or equal to i. Note that primes is used recursively but Clojure manages to prevent endless recursion. Also note that the lazy sequence primes caches results (that's why the performance results are a bit counter intuitive at first sight).

    (def primes
      (lazy-seq
        (filter (fn [i] (not-any? #(zero? (rem i %))
                                  (take-while #(<= (* % %) i) primes)))
                (drop 2 (range)))))
    (time (first (drop 10000 primes)))
    ; Elapsed time: 542.204211 msecs
    (time (first (drop 20000 primes)))
    ; Elapsed time: 786.667644 msecs
    (time (first (drop 40000 primes)))
    ; Elapsed time: 1780.15807 msecs
    (time (first (drop 40000 primes)))
    ; Elapsed time: 8.415643 msecs
    
    0 讨论(0)
  • 2020-12-02 13:01

    Here's another approach that celebrates Clojure's Java interop. This takes 374ms on a 2.4 Ghz Core 2 Duo (running single-threaded). I let the efficient Miller-Rabin implementation in Java's BigInteger#isProbablePrime deal with the primality check.

    (def certainty 5)
    
    (defn prime? [n]
          (.isProbablePrime (BigInteger/valueOf n) certainty))
    
    (concat [2] (take 10001 
       (filter prime? 
          (take-nth 2 
             (range 1 Integer/MAX_VALUE)))))
    

    The Miller-Rabin certainty of 5 is probably not very good for numbers much larger than this. That certainty is equal to 96.875% certain it's prime (1 - .5^certainty)

    0 讨论(0)
  • 2020-12-02 13:03

    Very late to the party, but I'll throw in an example, using Java BitSets:

    (defn sieve [n]
      "Returns a BitSet with bits set for each prime up to n"
      (let [bs (new java.util.BitSet n)]
        (.flip bs 2 n)
        (doseq [i (range 4 n 2)] (.clear bs i))
        (doseq [p (range 3 (Math/sqrt n))]
          (if (.get bs p)
            (doseq [q (range (* p p) n (* 2 p))] (.clear bs q))))
        bs))
    

    Running this on a 2014 Macbook Pro (2.3GHz Core i7), I get:

    user=> (time (do (sieve 1e6) nil))
    "Elapsed time: 64.936 msecs"
    
    0 讨论(0)
  • 2020-12-02 13:03

    So I've just started with Clojure, and yeah, this comes up a lot on Project Euler doesn't it? I wrote a pretty fast trial division prime algorithm, but it doesn't really scale too far before each run of divisions becomes prohibitively slow.

    So I started again, this time using the sieve method:

    (defn clense
      "Walks through the sieve and nils out multiples of step"
      [primes step i]
      (if (<= i (count primes))
        (recur 
          (assoc! primes i nil)
          step
          (+ i step))
        primes))
    
    (defn sieve-step
      "Only works if i is >= 3"
      [primes i]
      (if (< i (count primes))
        (recur
          (if (nil? (primes i)) primes (clense primes (* 2 i) (* i i)))
          (+ 2 i))
        primes))
    
    (defn prime-sieve
      "Returns a lazy list of all primes smaller than x"
      [x]
      (drop 2 
        (filter (complement nil?)
        (persistent! (sieve-step 
          (clense (transient (vec (range x))) 2 4) 3)))))
    

    Usage and speed:

    user=> (time (do (prime-sieve 1E6) nil))
    "Elapsed time: 930.881 msecs
    

    I'm pretty happy with the speed: it's running out of a REPL running on a 2009 MBP. It's mostly fast because I completely eschew idiomatic Clojure and instead loop around like a monkey. It's also 4X faster because I'm using a transient vector to work on the sieve instead of staying completely immutable.

    Edit: After a couple of suggestions / bug fixes from Will Ness it now runs a whole lot faster.

    0 讨论(0)
提交回复
热议问题