8

Suppose I've got a natural number n and I want a list (or whatever) of all primes up to n.

The classic prime sieve algorithm runs in O(n log n) time and O(n) space -- it's fine for more imperative languages, but requires in-place modification to lists and random access, in a fundamental way.

There's a functional version involving priority queues, which is pretty slick -- you can check it out here. This has better space complexity at about O(n / log(n)) (asymptotically better but debatable at practical scales). Unfortunately the time analysis is nasty, but it's very nearly O(n^2) (actually, I think it's about O(n log(n) Li(n)), but log(n) Li(n) is approximately n).

Asymptotically speaking it would actually be better just to check the primality of each number as you generate it, using successive trial division, as that would take only O(1) space and O(n^{3/2}) time. Is there a better way?

Edit: it turns out my calculations were simply incorrect. The algorithm in the article is O(n (log n) (log log n)), which the articles explains and proves (and see the answer below), not the complicated mess I put above. I'd still enjoy seeing a bona-fide O(n log log n) pure algorithm if there is one out there.

Will Ness
  • 62,652
  • 8
  • 86
  • 167
Richard Rast
  • 1,313
  • 13
  • 24
  • 2
    The linked article proposes a Theta(n log n log log n)-time algorithm, one log n factor off of the standard sieve of Eratosthenes, using a functional priority queue to merge the writes to the array. – David Eisenstat Feb 08 '17 at 16:35
  • 1
    While I believe this is on topic here, if you don't get any answers after a while you might also want to check into computerscience.stackexchange. – Gilles Feb 08 '17 at 16:41
  • @DavidEisenstat then probably the problem is I can't calculate complexities well. I definitely got very confused working this out. – Richard Rast Feb 08 '17 at 17:02
  • For what it's worth, [this](http://hackage.haskell.org/package/primes-0.1/src/Data/Numbers/Primes.hs) claims to be a Haskell implementation of the algorithm in the article. – Gassa Feb 08 '17 at 17:49
  • See [here](http://clj-me.blogspot.com/2009/07/everybody-loves-sieve-of-eratosthenes.html) for a very nice Clojure Sieve of Eratosthenes. – A. Webb Feb 08 '17 at 20:08
  • `n log n log log n` - I'm not certain: _n log(n log log n)_ or _n log n log(log n)? (And _why_ any interpretation - shown or not - should be discernible as intended.) – greybeard Feb 09 '17 at 14:29
  • @greybeard Edited the question to make it more clear, I hope. If you want a complete time analysis I can write one up, but it would really fit an answer, not the question. Or perhaps it could be edited into the answer below. – Richard Rast Feb 09 '17 at 14:32

3 Answers3

3

Here's a Haskell implementation of Melissa O'Neill's algorithm (from the linked article). Unlike the implementation that Gassa linked to, I've made minimal use of laziness, so that the performance analysis is clear -- O(n log n log log n), i.e., linearithmic in n log log n, the number of writes made by the imperative Sieve of Eratosthenes.

The heap implementation is just a tournament tree. The balancing logic is in push; by swapping the children every time, we ensure that, for every branch, the left subtree is the same size or one bigger compared to the right subtree, which ensures depth O(log n).

module Sieve where

type Nat = Int

data Heap = Leaf !Nat !Nat
          | Branch !Nat !Heap !Heap
          deriving Show

top :: Heap -> Nat
top (Leaf n _) = n
top (Branch n _ _) = n

leaf :: Nat -> Heap
leaf p = Leaf (3 * p) p

branch :: Heap -> Heap -> Heap
branch h1 h2 = Branch (min (top h1) (top h2)) h1 h2

pop :: Heap -> Heap
pop (Leaf n p) = Leaf (n + 2 * p) p
pop (Branch _ h1 h2)
  = case compare (top h1) (top h2) of
        LT -> branch (pop h1) h2
        EQ -> branch (pop h1) (pop h2)
        GT -> branch h1 (pop h2)

push :: Nat -> Heap -> Heap
push p h@(Leaf _ _) = branch (leaf p) h
push p (Branch _ h1 h2) = branch (push p h2) h1

primes :: [Nat]
primes
  = let helper n h
          = case compare n (top h) of
                LT -> n : helper (n + 2) (push n h)
                EQ -> helper (n + 2) (pop h)
                GT -> helper n (pop h)
      in 2 : 3 : helper 5 (leaf 3)
David Eisenstat
  • 52,844
  • 7
  • 50
  • 103
  • To be completely fair, the doesn't make the performance analysis completely trivial. One has to consider both how many pop/push operations there are, and how big the heap is when the operation occurs. If you leave this out (assume it's always maximal size ~n/ln(n)) then you get a big overestimate of the time complexity (the wrong estimate I posted above). – Richard Rast Feb 08 '17 at 21:02
  • @RichardRast Pops are one per Eratosthenes write, which is Theta(n log log n). Pushes are one per prime in the window examined. Both take time log k where k is the current size of the heap; since log √n = (log n)/2 = Theta(log n), the vast majority of the operations cost Theta(log n), as the heap grows to √n primes in the first √n log n numbers considered. – David Eisenstat Feb 08 '17 at 21:05
  • It turns out that my real question wasn't about the algorithm itself, but about computing its complexity. But this has been helpful, and I'll mark it as accepted since I think it will continue to be useful for others. – Richard Rast Feb 09 '17 at 14:19
  • @WillNess Haskell prime generators traditionally provide a lazy list of all of the primes. Minimizing the size of the heap would have added complexity. – David Eisenstat Feb 12 '17 at 01:34
2

Here it is, if (Haskell's) pure arrays count as pure (they should, IMO). The complexity is obviously O(n log (log n)), provided accumArray indeed spends O(1) time for each entry it is given, as it ought to:

import Data.Array.Unboxed 
import Data.List (tails, inits)

ps = 2 : [ n | (r:q:_, px) <- (zip . tails . (2:) . map (^2)) ps (inits ps),
               (n,True)    <- assocs (
                                accumArray (\_ _ -> False) True (r+1,q-1)
                                  [(m,()) | p <- px, let s = (r+p)`div`p*p, 
                                            m <- [s,s+p..q-1]] :: UArray Int Bool) ]

Calculates primes by segments between successive squares of primes (the map (^2) bit), generating the composites by enumerating the multiples of growing prefixes of primes (the inits bit) just as any proper sieve of Eratosthenes would, by repeated additions.

So, the primes {2,3} are used to sieve a segment from 10 to 24; {2,3,5} from 26 to 48; and so on. See also.

Also, a Python generator-based sieve might be considered functional as well. Python's dicts are extremely well-performing, empirically, though I'm not sure about the exact cost of the multiples over-producing scheme used there to avoid duplicate composites.


update: testing it indeed produces favorable results, as expected:

{-     original heap       tweaked           nested-feed         array-based
          (3*p,p)         (p*p,2*p)            JBwoVL              abPSOx
          6Uv0cL          2x speed-up     another 3x+ speed-up
                n^                n^                  n^                  n^
100K:  0.78s             0.38s               0.13s              0.065s    
200K:  2.02s   1.37      0.97s   1.35        0.29s   1.16       0.13s    1.00
400K:  5.05s   1.32      2.40s   1.31        0.70s   1.27       0.29s    1.16
800K: 12.37s   1.29                     1M:  2.10s   1.20       0.82s    1.13
 2M:                                                            1.71s    1.06
 4M:                                                            3.72s    1.12
10M:                                                            9.84s    1.06 
    overall in the tested range:
               1.33                                  1.21                1.09
-}

with empirical orders of growth calculated for producing n primes, where O(n log log n) is commonly seen as n1.05...1.10 and O(n log n log log n) as n1.20...1.25.

The "nested-feed" variant implements the postponement technique (as also seen in the above linked Python answer) that achieves quadratic reduction of the heap size which evidently has a noticeable impact on the empirical complexity, even if not quite reaching the still better results for the array-based code of this answer, which is able to produce 10 million primes in under 10 seconds on ideone.com (with overall growth rate of just n1.09 in the tested range).

("original heap" is of course the code from the other answer here).

Will Ness
  • 62,652
  • 8
  • 86
  • 167
0

I derived a prime generating function (generates all primes in order) a while back. Created a 6 pages proof for it as well. I think it is the first prime generating function in history actually (At least I couldn't find any other examples).

Here it is:
enter image description here

(-1)^((4*gamma(x)+4)/x)-1

Not sure how fast it can be computed. It returns 0 for all primes (or maybe it was 1, can't remember). Gamma function is essentially factorial so that can be fast early on. Raising negative 1 to a fractional exponent is a whole other beast though, I believe it uses integrals in base_e possible, or maybe some trigonometric functions; can't remember.

I don't know LaTeX so if someone wants to edit my post and include a LaTeX version that would be amazing!

Albert Renshaw
  • 15,644
  • 17
  • 92
  • 173
  • `It returns 0 for all composites and 1 for all primes (including 2)` that sounds a [primality test](https://en.wikipedia.org/wiki/Primality_test) rather than a [generating function](https://en.wikipedia.org/wiki/Formula_for_primes). Still… – greybeard Feb 11 '17 at 17:49
  • @greybeard well just multiply the whole function by x and then it's prime generating function! also I think I may have messed up, it might return 0 for all primes not 1. I can't remember it has been many months since I was working on this equation. – Albert Renshaw Feb 11 '17 at 18:16
  • @AlbertRenshaw no, a generating function would generate *n-th prime* for each *n*, without zeroes in between. Speaking of factorials, a simple test function for primality is based on [Wilson's theorem](https://en.wikipedia.org/wiki/Wilson's_theorem): `isPrime(n): (product([1..n-1]) % n) == (n-1)`. – Will Ness Feb 11 '17 at 19:07
  • @WillNess Oh I understand. Yeah there'd be no way to do that w/ my function unless you worked in infinite sums and had some fancy modulo tricks. That would be infinite iterations though, not fast. – Albert Renshaw Feb 11 '17 at 19:13
  • How does it work? `(4Г(x)+4)/x` is not in general an integer and `(-1)^not-an-integer` is not well-defined. – n. 'pronouns' m. May 08 '17 at 10:22
  • @n.m. for `(-1)^x` use alternative form `e^(i π x)` – Albert Renshaw May 08 '17 at 19:14