← Todos os algoritmos

Sieve of Eratosthenes

Matemática · ver código-fonte no GitHub

Leia em: English · Português · Español

Categoria: Math

O problema

Encontrar todo número primo até um limite. Testar cada número individualmente — divisão por tentativa, verificando os divisores candidatos até sua raiz quadrada — é O(sqrt(k)) por número, O(n × sqrt(n)) no total ao longo de todo o intervalo. Boa parte desse trabalho é desperdiçada: no momento em que um grande número composto é alcançado, seu menor fator primo quase certamente já foi encontrado ao verificar um número muito menor anteriormente no intervalo.

A solução

Inverter a pergunta: em vez de perguntar "esse número é primo?" um número de cada vez, começar a partir de cada primo já encontrado e riscar todos os seus múltiplos em uma única varredura. Um número só é riscado uma vez para cada um de seus fatores primos distintos, e a soma dessas varreduras ao longo de todo o intervalo — a soma de 1/p sobre cada primo p até n — converge para log log n, um dos limites mais precisos e marcantes da análise clássica de algoritmos. Custo total: O(n log log n), próximo o suficiente de linear para ser tratado como linear na prática.

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"]

Exemplo clássico

classic/SieveOfEratosthenes faz a varredura começando o riscamento de múltiplos de cada primo em candidate * candidate em vez de candidate * 2 — todo múltiplo menor de candidate já foi riscado por um fator primo menor, então começar ali evita trabalho redundante sem alterar o resultado. Também incluído: bruteForcePrimesUpTo, divisão por tentativa aplicada a cada candidato individualmente, especificamente para o benchmark abaixo. SieveOfEratosthenesTest verifica a sieve contra a conhecida lista de primos até 30, os casos de borda (limites abaixo de 2 não têm primos; 2 é o único primo par), e confirma que a força bruta concorda com a sieve em todos os casos testados.

Exemplo aplicado: dimensionamento de cache de deduplicação de plataforma antifraude

applied/HashBucketSizer encontra o menor primo maior ou igual a uma capacidade solicitada, para dimensionar o cache de deduplicação em streaming de uma plataforma antifraude — um hash set rastreando IDs de eventos de transação vistos recentemente. Um array de buckets de tamanho primo distribui os valores de hash de forma mais uniforme do que um tamanho potência de dois, o que importa aqui especificamente porque os IDs de eventos costumam ser gerados com padrões previsíveis (contadores sequenciais, IDs prefixados com timestamp) que colidem contra tamanhos de tabela potência de dois de forma estruturada e não aleatória. A busca é limitada pelo postulado de Bertrand — um primo sempre existe estritamente entre n e 2n para n > 1 — então fazer a sieve até 2 * minimumCapacity sempre garante encontrar um. HashBucketSizerTest cobre uma capacidade potência de dois (1,024 → o próximo primo, 1,031), uma capacidade que já é prima, e a proteção contra capacidades abaixo de 2.

Benchmark

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

Execução real nesta máquina (JMH 1.37, JDK 26.0.2, 2 iterações de aquecimento + 3 de medição, 1 fork):

Custo limit=1,000 limit=10,000 limit=100,000
sieve 3.37 µs 38.92 µs 421.49 µs
força bruta 24.74 µs 565.35 µs 10,220.86 µs

A sieve cresceu de forma próxima à linear com limit11.56x e 10.83x ao longo dos dois passos de 10x — exatamente o que um fator log log n deveria parecer: mal se move nesses intervalos. A força bruta cresceu visivelmente mais rápido em ambos os passos (22.85x, 18.08x) — consistente em direção com o fator extra sqrt(n), que prevê aproximadamente 31.6x por passo de 10x, embora as margens de erro desta execução específica sejam largas o suficiente (±23 a ±596 µs) para que o multiplicador exato não deva ser superinterpretado; a direção e a separação são a parte confiável. Em limit=100,000, a força bruta é ~24x mais lenta que a sieve para a mesma lista de primos.

Quando não usar

Cobertura de testes

100% de cobertura de instruções, 100% de cobertura de branches (JaCoCo). Reproduza você mesmo:

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

Relatório em math/sieve-of-eratosthenes/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

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);
    }
}

Ver relatório completo de cobertura JaCoCo →