← Todos los algoritmos

Euclidean GCD

Matemáticas · ver código fuente en GitHub

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

Categoría: Math

El problema

Encontrar el máximo común divisor (GCD) de dos enteros no negativos — el entero más grande que divide a ambos sin dejar resto. El enfoque directo — contar hacia atrás desde el número más pequeño, probando cada candidato — es O(min(a, b)). Para dos números grandes que resultan ser coprimos (su único divisor común es 1), ese recorrido tiene que llegar hasta 1 sin nada que mostrar antes de eso.

La solución

Una sola identidad sostiene todo el algoritmo: gcd(a, b) = gcd(b, a mod b). Reemplazar repetidamente el par (a, b) por (b, a mod b) reduce los números rápidamente — al menos tan rápido como crece la secuencia de Fibonacci, que es el propio peor caso clásico del algoritmo (dos números de Fibonacci consecutivos fuerzan la cantidad máxima posible de pasos para números de ese tamaño). Incluso ese peor caso es de solo O(log(min(a, b))) pasos — muy lejos del recorrido lineal que reemplaza.

flowchart LR
    A["gcd(48, 18)"] --> B["gcd(18, 48 mod 18 = 12)"]
    B --> C["gcd(12, 18 mod 12 = 6)"]
    C --> D["gcd(6, 12 mod 6 = 0)"]
    D --> E["6"]

Ejemplo clásico

classic/EuclideanGcd implementa el algoritmo iterativo basado en módulo, un lcm construido directamente sobre él (lcm(a, b) = (a / gcd(a, b)) * b), y bruteForceGcd, incluido específicamente para el benchmark a continuación. EuclideanGcdTest cubre un par ordinario, las identidades con argumento cero, un número contra sí mismo, un par coprimo, y — un guiño deliberado al propio peor caso clásico del algoritmo — un par de números de Fibonacci consecutivos, además de confirmar que la fuerza bruta coincide con Euclides en todos los casos probados.

Ejemplo aplicado: proporción de split payment en marketplace vía PIX

applied/PaymentSplitReducer reduce una regla de split payment de marketplace vía PIX a su mínima expresión — una plataforma que retiene 3,000 unidades base y un vendedor que recibe 7,000 de un total de 10,000 se reduce a la proporción canónica 3:7, que es exactamente el tipo de representación auditable y mínima que un motor de reglas de liquidación quiere almacenar, en lugar de los números originales (equivalentes, pero innecesariamente grandes). PaymentSplitReducerTest cubre un split reducible, un split que ya está en su mínima expresión, y la protección contra participaciones no positivas.

Benchmark

./gradlew :math:euclidean-gcd: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). Ambos métodos se ejecutan contra pares de enteros consecutivos (n, n-1) — siempre coprimos, por lo que su GCD verdadero es siempre 1. Ese es el peor caso del recorrido de fuerza bruta (nunca encuentra un divisor común antes de tiempo, así que siempre recorre todo hasta el final) y está cerca del mejor caso de Euclides (n mod (n-1) es siempre 1, resolviéndose en esencialmente dos pasos sin importar n):

Costo n=100 n=10,000 n=1,000,000
Euclides 0.022 µs 0.022 µs 0.022 µs
fuerza bruta 1.10 µs 110.30 µs 9,877.36 µs

Euclides se mantiene completamente estable a lo largo de cuatro órdenes de magnitud de n — realmente realiza las mismas dos operaciones de módulo sin importar cuán grandes sean los números. La fuerza bruta sigue su predicción de O(n) casi con exactitud: un aumento de 100x en n produjo un aumento de 100.1x en el costo (100 → 10,000), y un aumento de 89.6x en el siguiente paso de 100x (10,000 → 1,000,000). En n=1,000,000, la fuerza bruta es ~448,971x más lenta que Euclides para la misma respuesta.

Cuándo no usarlo

Cobertura de pruebas

100% de cobertura de instrucciones, 100% de cobertura de branches (JaCoCo). Reprodúzcalo usted mismo:

./gradlew :math:euclidean-gcd:jacocoTestReport

Informe en math/euclidean-gcd/build/reports/jacoco/test/html/index.html.

Lectura complementaria

Pruebas unitarias

src/test/java/com/algorithms/math/euclideangcd/classic/EuclideanGcdTest.java
package com.algorithms.math.euclideangcd.classic;

import org.junit.jupiter.api.Test;

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

class EuclideanGcdTest {

    @Test
    void computesTheGcdOfTwoOrdinaryNumbers() {
        assertThat(EuclideanGcd.gcd(48, 18)).isEqualTo(6);
    }

    @Test
    void gcdWithZeroReturnsTheOtherNumber() {
        assertThat(EuclideanGcd.gcd(0, 5)).isEqualTo(5);
        assertThat(EuclideanGcd.gcd(5, 0)).isEqualTo(5);
    }

    @Test
    void gcdOfANumberWithItselfIsItself() {
        assertThat(EuclideanGcd.gcd(42, 42)).isEqualTo(42);
    }

    @Test
    void coprimeNumbersHaveGcdOne() {
        assertThat(EuclideanGcd.gcd(17, 13)).isEqualTo(1);
    }

    @Test
    void handlesConsecutiveFibonacciNumbersTheAlgorithmsOwnWorstCase() {
        // Consecutive Fibonacci numbers force the maximum number of steps for a given magnitude.
        assertThat(EuclideanGcd.gcd(89, 55)).isEqualTo(1);
    }

    @Test
    void computesTheLcmOfTwoOrdinaryNumbers() {
        assertThat(EuclideanGcd.lcm(4, 6)).isEqualTo(12);
    }

    @Test
    void lcmWithZeroIsZero() {
        assertThat(EuclideanGcd.lcm(0, 5)).isZero();
        assertThat(EuclideanGcd.lcm(5, 0)).isZero();
    }

    @Test
    void bruteForceAgreesWithEuclidOnEveryCase() {
        assertThat(EuclideanGcd.bruteForceGcd(48, 18)).isEqualTo(EuclideanGcd.gcd(48, 18));
        assertThat(EuclideanGcd.bruteForceGcd(0, 5)).isEqualTo(EuclideanGcd.gcd(0, 5));
        assertThat(EuclideanGcd.bruteForceGcd(5, 0)).isEqualTo(EuclideanGcd.gcd(5, 0));
        assertThat(EuclideanGcd.bruteForceGcd(17, 13)).isEqualTo(EuclideanGcd.gcd(17, 13));
    }

    @Test
    void rejectsNegativeInputs() {
        assertThatThrownBy(() -> EuclideanGcd.gcd(-1, 5)).isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> EuclideanGcd.gcd(5, -1)).isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> EuclideanGcd.bruteForceGcd(-1, 5)).isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> EuclideanGcd.lcm(-1, 5)).isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsBothInputsBeingZero() {
        assertThatThrownBy(() -> EuclideanGcd.gcd(0, 0)).isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> EuclideanGcd.bruteForceGcd(0, 0)).isInstanceOf(IllegalArgumentException.class);
    }
}
src/test/java/com/algorithms/math/euclideangcd/applied/PaymentSplitReducerTest.java
package com.algorithms.math.euclideangcd.applied;

import com.algorithms.math.euclideangcd.applied.PaymentSplitReducer.SplitRatio;
import org.junit.jupiter.api.Test;

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

class PaymentSplitReducerTest {

    private final PaymentSplitReducer reducer = new PaymentSplitReducer();

    @Test
    void reducesAMarketplaceSplitToItsLowestTerms() {
        assertThat(reducer.reduceToLowestTerms(3_000, 7_000)).isEqualTo(new SplitRatio(3, 7));
    }

    @Test
    void anAlreadyReducedRatioIsUnchanged() {
        assertThat(reducer.reduceToLowestTerms(1, 4)).isEqualTo(new SplitRatio(1, 4));
    }

    @Test
    void rejectsNonPositiveShares() {
        assertThatThrownBy(() -> reducer.reduceToLowestTerms(0, 5)).isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> reducer.reduceToLowestTerms(5, 0)).isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> reducer.reduceToLowestTerms(-1, 5)).isInstanceOf(IllegalArgumentException.class);
    }
}

Ver informe completo de cobertura JaCoCo →