Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Number Theory for Algorithms  ›  Lesson

Eratosthenes and the Linear Sieve

Sieves 26 minStrike multiples instead of testing numbers, and give every composite exactly one owner
You're building a piece ofQuarry — the hash kernel
This piece — primes_up_to(): Every prime up to n, fast enough to pick a bucket count or a modulus at startup.
Scenario Quarry's hash kernel needs a table of small primes to pick bucket counts from, and it needs to build that table fast enough to run at startup, not just once offline.
Your task
Build primes_up_to(n): every prime <= n, in order, using the Sieve of Eratosthenes (strike multiples of each prime p starting at p*p, not 2p). Example: primes_up_to(10) -> [2, 3, 5, 7], primes_up_to(30) -> [2, 3, 5, 7, 11, 13, 17, 19, 23, 29], primes_up_to(1) -> [].

Eratosthenes and the Linear Sieve

In short: to find every prime up to n, don't test each number on its own — strike out multiples instead. Starting from each prime p, cross off p·p, p·p+p, p·p+2p, … up to n; anything never crossed off is prime. That's the Sieve of Eratosthenes, and it costs O(n log log n). A sharper version, the linear sieve, strikes every composite exactly once — each composite is crossed off only by its smallest prime factor — and costs O(n). Quarry's hash kernel needs this: prime bucket counts, prime moduli and quick factorisation all start with "which numbers up to n are prime".

A hand-drawn 12-wide grid of numbers with three colours of crossed-out multiples fanning out from 2, 3 and 5, and the surviving primes circled

Picking a bucket count that resists clustering

Quarry's hash table spreads keys across capacity buckets with hash(key) % capacity. Pick capacity = 100 and every key that's a multiple of 10 — user ids 10, 20, 30, … — lands in buckets 0, 20, 40, …: a fifth of the buckets, used by everything. A prime capacity breaks that pattern, because a prime shares no common factor with any key except 1.

python

97 keys hashed into a 97-bucket table land in 97 different buckets, no matter what the key pattern is — because 97 has no factor in common with 10. Pick table sizes from a prime list, not round numbers.

How much faster is striking than testing?

Testing every number up to n for primality, one at a time, costs about n · √n in the worst case. The sieve strikes multiples instead, and the total number of strikes is n · Σ(1/p) over primes p ≤ n, which is n · log log n — a function so slow-growing it's barely above a constant for any n you'll meet in practice.

What does this print?
n = 100
is_c = [False] * (n + 1)
strikes = 0
p = 2
while p * p <= n:
    if not is_c[p]:
        m = p * p
        while m <= n:
            is_c[m] = True
            strikes += 1
            m += p
    p += 1
print(strikes)

A live pass over 2..120

Watch three colours of strikes fan out: 2's multiples, then 3's, then 5's and 7's, each starting at the prime's own square because anything smaller was already struck by a smaller prime.

your code, drawn · grid

Each prime only strikes from its own square, because smaller primes already cleared everything below it. The circled survivors at the end are every prime up to 120.

The linear sieve: one strike per composite

The classic sieve strikes some composites more than once — 12 gets struck by both 2 and 3. The linear sieve avoids that by giving every composite exactly one owner: its smallest prime factor. Walk i from 2 upward. If i has no owner yet, it's prime. Then, for every prime p found so far (smallest first), mark p·i as owned by p — but stop as soon as p exceeds i's own smallest prime factor, because any composite past that point will get a smaller owner later anyway.

python

spf[i] falls out for free — it's a byproduct worth keeping. With it, factorising any number up to n takes O(log n): repeatedly divide by spf[current].

What does this print?
def spf_ops(n, guard):
    spf = [0] * (n + 1)
    primes = []
    ops = 0
    for i in range(2, n + 1):
        if spf[i] == 0:
            spf[i] = i
            primes.append(i)
        for p in primes:
            if (guard and p > spf[i]) or p * i > n:
                break
            spf[p * i] = p
            ops += 1
    return ops

print(spf_ops(2000, True) < spf_ops(2000, False))

What it costs

approach time why
test every number for primality O(n · √n) trial division per number
Sieve of Eratosthenes (strike from p²) O(n log log n) Σ(n/p) over primes ≤ n
linear sieve O(n) every composite struck exactly once, by its smallest factor
segmented sieve over [lo, hi] O((hi−lo) log log hi + √hi) sieve the small base primes once, strike only inside the window

Quick practice

What does this print?
def is_prime(n):
    if n < 2:
        return False
    i = 2
    while i * i <= n:
        if n % i == 0:
            return False
        i += 1
    return True

print(is_prime(1), is_prime(2), is_prime(91))
broken — fix it

This prints `[]` — every number, prime or not, gets struck before it's checked. Reorder so the check-and-append happens first, and strike from `p * p` onward.

🎯 Your turn

Build primes_up_to(n): every prime ≤ n, in order, using the Sieve of Eratosthenes (strike from p·p, not 2p).

  • primes_up_to(10) → [2, 3, 5, 7]
  • primes_up_to(30) → [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
  • primes_up_to(1) → []

Hint: a boolean array is_composite[0..n]. For each p from 2 up while p*p <= n, if p isn't marked composite, strike p*p, p*p+p, …. Collect whatever's left unmarked.

Press ▶ Run, then ✓ Check.

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upTest one number

Write is_prime(n) returning whether n is prime, by trial division up to sqrt(n).

divisibility
DrillCount primes up to n

Write count_primes(n) returning how many primes are <= n, using a sieve (not is_prime in a loop).

sieve-of-eratosthenes
BuildBreak a number into its prime factors

Write prime_factorize(n) returning the prime factors of n with repetition, smallest first, by trial division. prime_factorize(360) -> [2, 2, 2, 3, 3, 5].

divisibilitytrial-division
BossPrimes in a window, without sieving from zero

Write segmented_primes(lo, hi): every prime in [lo, hi], without allocating an array of size hi. Sieve the small primes up to sqrt(hi) once, then strike only inside the window. 0 <= lo <= hi <= 10**7. segmented_primes(100, 120) -> [101, 103, 107, 109, 113].

segmented-sievesieve-of-eratosthenes
CapstoneOne strike per composite

Write linear_sieve(n): every prime <= n, using the O(n) linear sieve where every composite is struck exactly once, by its smallest prime factor.

linear-sievesmallest-prime-factor
primes_up_to.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
primes_up_to(n) → listEvery prime <= n, in increasing order.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.