← All algorithms

Sieve of Eratosthenes

Math · view source on GitHub

Read this in: English · Português · Español

Category: Math

The problem

Finding every prime number up to a limit. Testing each number individually — trial division, checking candidate divisors up to its square root — is O(sqrt(k)) per number, O(n × sqrt(n)) total across the whole range. Most of that work is wasted: by the time a large composite number is reached, its smallest prime factor was almost certainly already found while checking a much smaller number earlier in the range.

The solution

Flip the question around: instead of asking "is this number prime?" one number at a time, start from every prime found so far and cross off all of its multiples in one sweep. A number only ever gets crossed off once for each of its distinct prime factors, and the sum of those sweeps across the whole range — the sum of 1/p over every prime p up to n — converges to log log n, one of the tightest, most striking bounds in classical algorithm analysis. Total cost: O(n log log n), close enough to linear that it's often treated as linear in practice.

flowchart LR
    A["start: every number 2..n is 'unmarked'"] --> B["2 is unmarked → prime. Cross off 4, 6, 8, ..."]
    B --> C["3 is unmarked → prime. Cross off 6, 9, 12, ..."]
    C --> D["4 is already crossed off → skip"]
    D --> E["5 is unmarked → prime. Cross off 10, 15, 20, ..."]
    E --> F["... every remaining unmarked number is prime"]

Classic example

classic/SieveOfEratosthenes sieves starting each prime's crossing-off sweep at candidate * candidate rather than candidate * 2 — every smaller multiple of candidate was already crossed off by a smaller prime factor, so starting there skips redundant work without changing the result. Also included: bruteForcePrimesUpTo, trial division applied to every candidate individually, specifically for the benchmark below. SieveOfEratosthenesTest checks the sieve against the well-known list of primes up to 30, the boundary cases (limits below 2 have no primes; 2 is the only even prime), and confirms brute force agrees with the sieve on every case tested.

Applied example: fraud platform dedup cache sizing

applied/HashBucketSizer finds the smallest prime at or above a requested capacity, for sizing a fraud platform's streaming deduplication cache — a hash set tracking recently-seen transaction event IDs. A prime-sized bucket array spreads hash values more evenly than a power-of-two size does, which matters here specifically because event IDs are often generated with predictable patterns (sequential counters, timestamp-prefixed IDs) that collide against power-of-two table sizes in a structured, non-random way. The search is bounded by Bertrand's postulate — a prime always exists strictly between n and 2n for n > 1 — so sieving up to 2 * minimumCapacity is always guaranteed to find one. HashBucketSizerTest covers a power-of-two capacity (1,024 → the next prime, 1,031), a capacity that's already prime, and the guard against capacities below 2.

Benchmark

./gradlew :math:sieve-of-eratosthenes:jmh

Real run on this machine (JMH 1.37, JDK 26.0.2, 2 warmup + 3 measurement iterations, 1 fork):

Cost limit=1,000 limit=10,000 limit=100,000
sieve 3.37 µs 38.92 µs 421.49 µs
brute force 24.74 µs 565.35 µs 10,220.86 µs

The sieve grew close to linearly with limit11.56x and 10.83x across the two 10x steps — exactly what a log log n factor should look like: it barely moves across these ranges. Brute force grew noticeably faster on both steps (22.85x, 18.08x) — directionally consistent with the extra sqrt(n) factor predicting roughly 31.6x per 10x step, though this particular run's error bars are wide enough (±23 to ±596 µs) that the exact multiplier shouldn't be over-read; the direction and separation are the reliable part. At limit=100,000, brute force is ~24x slower than the sieve for the identical list of primes.

When not to use it

Test coverage

100% instruction coverage, 100% branch coverage (JaCoCo). Reproduce it yourself:

./gradlew :math:sieve-of-eratosthenes:jacocoTestReport

Report at math/sieve-of-eratosthenes/build/reports/jacoco/test/html/index.html.

Further reading

Unit tests

src/test/java/com/algorithms/math/sieveoferatosthenes/classic/SieveOfEratosthenesTest.java
package com.algorithms.math.sieveoferatosthenes.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class SieveOfEratosthenesTest {

    @Test
    void findsAllPrimesUpToThirty() {
        assertThat(SieveOfEratosthenes.primesUpTo(30))
                .containsExactly(2, 3, 5, 7, 11, 13, 17, 19, 23, 29);
    }

    @Test
    void aLimitBelowTwoHasNoPrimes() {
        assertThat(SieveOfEratosthenes.primesUpTo(0)).isEmpty();
        assertThat(SieveOfEratosthenes.primesUpTo(1)).isEmpty();
    }

    @Test
    void twoIsTheOnlyEvenPrime() {
        assertThat(SieveOfEratosthenes.primesUpTo(2)).containsExactly(2);
    }

    @Test
    void bruteForceAgreesWithTheSieve() {
        assertThat(SieveOfEratosthenes.bruteForcePrimesUpTo(30)).isEqualTo(SieveOfEratosthenes.primesUpTo(30));
        assertThat(SieveOfEratosthenes.bruteForcePrimesUpTo(1)).isEqualTo(SieveOfEratosthenes.primesUpTo(1));
    }

    @Test
    void rejectsANegativeLimit() {
        assertThatThrownBy(() -> SieveOfEratosthenes.primesUpTo(-1)).isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> SieveOfEratosthenes.bruteForcePrimesUpTo(-1)).isInstanceOf(IllegalArgumentException.class);
    }
}
src/test/java/com/algorithms/math/sieveoferatosthenes/applied/HashBucketSizerTest.java
package com.algorithms.math.sieveoferatosthenes.applied;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class HashBucketSizerTest {

    private final HashBucketSizer sizer = new HashBucketSizer();

    @Test
    void findsTheNextPrimeAtOrAboveAPowerOfTwoCapacity() {
        // 1024 = 2^10 is not prime; the next prime at or above it is 1031.
        assertThat(sizer.nextPrimeBucketCount(1024)).isEqualTo(1031);
    }

    @Test
    void aCapacityThatIsAlreadyPrimeReturnsItself() {
        assertThat(sizer.nextPrimeBucketCount(97)).isEqualTo(97);
    }

    @Test
    void rejectsACapacityBelowTwo() {
        assertThatThrownBy(() -> sizer.nextPrimeBucketCount(1)).isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> sizer.nextPrimeBucketCount(0)).isInstanceOf(IllegalArgumentException.class);
    }
}

View full JaCoCo coverage report →