Homeβ€Ί DSAβ€Ί Sieve of Eratosthenes β€” Prime Number Generation

Sieve of Eratosthenes β€” Prime Number Generation

Where developers are forged. Β· Structured learning Β· Free forever.
πŸ“ Part of: Number Theory β†’ Topic 1 of 5
Learn the Sieve of Eratosthenes to generate all primes up to n in O(n log log n).
πŸ§‘β€πŸ’» Beginner-friendly β€” no prior DSA experience needed
In this tutorial, you'll learn:
  • Start inner loop at pp, not 2p β€” numbers below pΒ² are already marked. This is not a micro-optimisation; it's the difference between correct complexity analysis and a slower algorithm.
  • Use bytearray or numpy bool arrays, not Python list of booleans β€” 28x less memory, dramatically better cache performance above n=10^5.
  • O(n log log n) is effectively linear β€” for n=10^7 the constant is ~3.2. A correct sieve is fast enough that you should never be using trial division to generate all primes below a bound.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
⚑ Quick Answer
The Sieve of Eratosthenes works like crossing names off a list. Write all numbers from 2 to n. Circle 2, then cross out every multiple of 2. Circle 3, cross out every multiple of 3. Skip 4 (already crossed). Circle 5, cross out multiples of 5. What remains circled are all the primes. It's 2300 years old and still one of the most elegant algorithms ever devised.

The Sieve of Eratosthenes is 2300 years old and still the fastest way to generate all primes up to n in most practical scenarios. That longevity is not nostalgia β€” it is a testament to how completely it solves the problem. In competitive programming, you hit this algorithm in the first 10 minutes of almost any number theory problem. In production, you hit it when building RSA key generation utilities, cryptographic prime tables, or anything involving primality at scale.

What makes it elegant is not the algorithm itself β€” it's obvious once explained β€” but the complexity. O(n log log n) time is almost linear. For n=10^7, a correctly implemented sieve runs in under 100ms in Python and under 5ms in C. Compare that to trial division on each number: O(n√n) = O(10^10.5) for n=10^7 β€” three orders of magnitude slower. The choice is not subtle.

The Basic Sieve

The implementation is four lines of meaningful code. The subtlety is in where the inner loop starts.

Starting the inner loop at pp instead of 2p is the critical optimisation. Every composite number smaller than pΒ² already has a prime factor smaller than p and has therefore already been marked. When we reach prime p=5, multiples 10, 15, 20 were already marked by 2 and 3. The first unmarked multiple of 5 is 5Γ—5=25. This halves the total work.

The second optimisation is memory layout. A Python list of booleans uses 28 bytes per element (Python object overhead). Use a bytearray or numpy bool array instead β€” 28x less memory and dramatically better cache performance. For n=10^7, a Python list uses 268MB; a bytearray uses 10MB. Cache misses dominate the runtime at large n, so this is not premature optimisation.

sieve.py Β· PYTHON
12345678910111213141516
def sieve(n: int) -> list[int]:
    """Return all primes <= n using Sieve of Eratosthenes."""
    is_prime = [True] * (n + 1)
    is_prime[0] = is_prime[1] = False
    p = 2
    while p * p <= n:
        if is_prime[p]:
            # Mark multiples of p starting from p*p
            for multiple in range(p * p, n + 1, p):
                is_prime[multiple] = False
        p += 1
    return [i for i, prime in enumerate(is_prime) if prime]

print(sieve(50))
# [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47]
print(f'Primes up to 100: {len(sieve(100))}')  # 25
β–Ά Output
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
Primes up to 100: 25

Why Start Marking at pΒ²?

The harmonic series of primes 1/2 + 1/3 + 1/5 + 1/7 + ... grows as log(log(n)) β€” this is Mertens' theorem from 1874. Each prime p contributes n/p marking operations. Summing over all primes up to √n gives the total work: n Γ— Ξ£(1/p) β‰ˆ n Γ— log(log(n)).

The practical implication: for n=10^9, log(log(n)) β‰ˆ 3.04. The sieve does roughly 3Γ— the work of a simple O(n) pass. This is why the sieve feels "almost free" in practice β€” the constant is tiny.

Memory limits in production: At n=10^9, a bit-packed sieve uses 125MB. A bytearray uses 1GB. Bit packing (using Python's bitarray library or numpy with uint8) is the practical choice above n=10^8. The segmented sieve reduces this further β€” work on √n-sized chunks, keeping only the current segment in cache at any time.

πŸ”₯
ComplexityO(n log log n) time β€” the harmonic series of prime reciprocals. Essentially O(n) in practice. O(n) space for the boolean array. For n=10^7, runs in under 1 second.

Segmented Sieve β€” Large Ranges

The segmented sieve is what you reach for when n doesn't fit in RAM or when you need primes in a specific range [L, R] where R is large but R-L is manageable.

Real scenario: Finding all primes in [10^12, 10^12 + 10^6]. You obviously cannot sieve up to 10^12. But √(10^12) = 10^6 β€” you can sieve all primes up to 10^6 (trivial: ~78,498 primes), then use them to sieve the segment. The segment has only 10^6 numbers β€” fits easily in L1/L2 cache.

This is the pattern used in distributed prime-finding projects (like primegrid.com) and in production RSA implementations that need to test primality of large numbers using a quick sieve pre-filter before Miller-Rabin.

segmented_sieve.py Β· PYTHON
12345678910111213141516171819202122
import math

def segmented_sieve(n: int) -> list[int]:
    """Memory-efficient sieve for large n using O(sqrt(n)) space."""
    limit = int(math.sqrt(n)) + 1
    base_primes = sieve(limit)
    primes = list(base_primes)
    low, high = limit + 1, min(2 * limit, n)
    while low <= n:
        segment = [True] * (high - low + 1)
        for p in base_primes:
            start = max(p * p, ((low + p - 1) // p) * p)
            for j in range(start, high + 1, p):
                segment[j - low] = False
        for i, is_p in enumerate(segment):
            if is_p:
                primes.append(low + i)
        low = high + 1
        high = min(high + limit, n)
    return primes

print(len(segmented_sieve(1_000_000)))  # 78498
β–Ά Output
78498

Smallest Prime Factor Sieve

A variant that stores the smallest prime factor (SPF) for each number instead of just prime/composite. Enables O(log n) factorisation of any number after O(n log log n) preprocessing.

spf_sieve.py Β· PYTHON
12345678910111213141516171819202122
def spf_sieve(n: int) -> list[int]:
    """Smallest prime factor sieve."""
    spf = list(range(n + 1))  # spf[i] = i initially
    p = 2
    while p * p <= n:
        if spf[p] == p:  # p is prime
            for multiple in range(p * p, n + 1, p):
                if spf[multiple] == multiple:
                    spf[multiple] = p
        p += 1
    return spf

def factorise(n: int, spf: list) -> list[int]:
    factors = []
    while n > 1:
        factors.append(spf[n])
        n //= spf[n]
    return factors

spf = spf_sieve(100)
print(factorise(84, spf))   # [2, 2, 3, 7]
print(factorise(360, spf))  # [2, 2, 2, 3, 3, 5]
β–Ά Output
[2, 2, 3, 7]
[2, 2, 2, 3, 3, 5]

🎯 Key Takeaways

  • Start inner loop at pp, not 2p β€” numbers below pΒ² are already marked. This is not a micro-optimisation; it's the difference between correct complexity analysis and a slower algorithm.
  • Use bytearray or numpy bool arrays, not Python list of booleans β€” 28x less memory, dramatically better cache performance above n=10^5.
  • O(n log log n) is effectively linear β€” for n=10^7 the constant is ~3.2. A correct sieve is fast enough that you should never be using trial division to generate all primes below a bound.
  • SPF (Smallest Prime Factor) sieve is the production version for competitive programming β€” O(log n) factorization of any number after O(n log log n) preprocessing.
  • Segmented sieve for large ranges: sieve primes to √n, then sieve each segment. Handles ranges [L, R] where R is up to 10^12 or larger.

⚠ Common Mistakes to Avoid

  • βœ•Starting the inner loop at 2p instead of pp β€” works but wastes ~half the operations.
  • βœ•Using a list of integers instead of booleans β€” uses 28x more memory unnecessarily.
  • βœ•Not handling edge cases n < 2 β€” should return empty list.

Interview Questions on This Topic

  • QWhy does the outer loop only need to go up to √n?
  • QWhat is the time complexity of the Sieve of Eratosthenes and why?
  • QHow would you find all primes in the range [L, R] where R can be up to 10^12?

Frequently Asked Questions

What is the prime counting function Ο€(n)?

Ο€(n) counts the number of primes ≀ n. Ο€(100)=25, Ο€(10^6)=78498, Ο€(10^9)β‰ˆ50.8 million. The prime number theorem says Ο€(n) β‰ˆ n/ln(n).

Can the sieve be parallelised?

Yes β€” the segmented sieve naturally parallelises since each segment is independent once base primes are computed. Used in distributed prime-finding projects.

πŸ”₯
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

Next β†’GCD and LCM β€” Euclidean Algorithm
Forged with πŸ”₯ at TheCodeForge.io β€” Where Developers Are Forged