← Todos los algoritmos

Sieve of Eratosthenes

Matemáticas · ver código fuente en GitHub

Leer en: English · Português · Español

Categoría: Math

El problema

Encontrar todos los números primos hasta un límite. Probar cada número individualmente — división por tentativa, verificando los divisores candidatos hasta su raíz cuadrada — es O(sqrt(k)) por número, O(n × sqrt(n)) en total a lo largo de todo el rango. La mayor parte de ese trabajo se desperdicia: para cuando se llega a un número compuesto grande, su menor factor primo casi con certeza ya fue encontrado al verificar un número mucho más pequeño anteriormente en el rango.

La solución

Invertir la pregunta: en lugar de preguntar "¿este número es primo?" un número a la vez, empezar desde cada primo ya encontrado y tachar todos sus múltiplos en un solo barrido. Un número solo se tacha una vez por cada uno de sus factores primos distintos, y la suma de esos barridos a lo largo de todo el rango — la suma de 1/p sobre cada primo p hasta n — converge a log log n, uno de los límites más ajustados y sorprendentes del análisis clásico de algoritmos. Costo total: O(n log log n), lo bastante cercano a lineal como para tratarse como lineal en la práctica.

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

Ejemplo clásico

classic/SieveOfEratosthenes hace el barrido comenzando el tachado de cada primo en candidate * candidate en lugar de candidate * 2 — todo múltiplo menor de candidate ya fue tachado por un factor primo menor, así que empezar ahí evita trabajo redundante sin cambiar el resultado. También incluido: bruteForcePrimesUpTo, división por tentativa aplicada a cada candidato individualmente, específicamente para el benchmark de abajo. SieveOfEratosthenesTest verifica la sieve contra la conocida lista de primos hasta 30, los casos límite (los límites por debajo de 2 no tienen primos; 2 es el único primo par), y confirma que la fuerza bruta coincide con la sieve en todos los casos probados.

Ejemplo aplicado: dimensionamiento de caché de deduplicación de plataforma antifraude

applied/HashBucketSizer encuentra el menor primo mayor o igual a una capacidad solicitada, para dimensionar la caché de deduplicación en streaming de una plataforma antifraude — un hash set que rastrea IDs de eventos de transacciones vistos recientemente. Un array de buckets de tamaño primo distribuye los valores de hash de forma más uniforme que un tamaño potencia de dos, lo cual importa aquí específicamente porque los IDs de eventos suelen generarse con patrones predecibles (contadores secuenciales, IDs con prefijo de timestamp) que colisionan contra tamaños de tabla potencia de dos de forma estructurada y no aleatoria. La búsqueda está acotada por el postulado de Bertrand — siempre existe un primo estrictamente entre n y 2n para n > 1 — así que hacer la sieve hasta 2 * minimumCapacity siempre garantiza encontrar uno. HashBucketSizerTest cubre una capacidad potencia de dos (1,024 → el siguiente primo, 1,031), una capacidad que ya es prima, y la protección contra capacidades por debajo de 2.

Benchmark

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

Ejecución real en esta máquina (JMH 1.37, JDK 26.0.2, 2 iteraciones de calentamiento + 3 de medición, 1 fork):

Costo limit=1,000 limit=10,000 limit=100,000
sieve 3.37 µs 38.92 µs 421.49 µs
fuerza bruta 24.74 µs 565.35 µs 10,220.86 µs

La sieve creció de forma casi lineal con limit11.56x y 10.83x a lo largo de los dos pasos de 10x — exactamente lo que debería verse con un factor log log n: apenas se mueve en estos rangos. La fuerza bruta creció notablemente más rápido en ambos pasos (22.85x, 18.08x) — consistente en dirección con el factor extra sqrt(n), que predice aproximadamente 31.6x por paso de 10x, aunque los márgenes de error de esta ejecución en particular son lo bastante amplios (±23 a ±596 µs) como para no sobreinterpretar el multiplicador exacto; la dirección y la separación son la parte confiable. En limit=100,000, la fuerza bruta es ~24x más lenta que la sieve para la misma lista de primos.

Cuándo no usarlo

Cobertura de pruebas

100% de cobertura de instrucciones, 100% de cobertura de ramas (JaCoCo). Reprodúcelo tú mismo:

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

Reporte en math/sieve-of-eratosthenes/build/reports/jacoco/test/html/index.html.

Lectura complementaria

Pruebas unitarias

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 informe completo de cobertura JaCoCo →