← Todos los algoritmos

Bubble Sort

Ordenamiento · ver código fuente en GitHub

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

Categoría: Sorting

El problema

Ordenar un lote pequeño de datos que ya están casi ordenados no debería requerir la sobrecarga constante de un algoritmo de propósito general con O(n log n) garantizado — y una ordenación ingenua, que siempre hace la misma cantidad de trabajo sin importar cuán ordenada ya esté la entrada, desperdicia esa oportunidad. Lo que realmente varía de una llamada a otra no es solo el tamaño de la entrada, es cuán lejos está la entrada de estar ordenada.

La solución

Recorra repetidamente el array comparando pares adyacentes e intercambiando los que están fuera de orden — el mayor valor aún no colocado "burbujea" hasta su posición correcta al final de cada pasada. El único detalle que hace que esto valga la pena enseñar: registrar si una pasada hizo algún intercambio, y detenerse en el momento en que una pasada completa no hace ninguno. Esa única verificación de salida anticipada es lo que hace que el algoritmo sea adaptativo — O(n) en una entrada ya ordenada, y con el costo escalando según cuánto desorden hay realmente presente, en lugar de pagar siempre el O(n²) completo, que es lo que un bubble sort sin la salida anticipada — o un equivalente como el selection sort puro — paga incondicionalmente.

flowchart TB
    subgraph "before pass 1"
        direction LR
        A0["5"] --- A1["3"] --- A2["8"] --- A3["1"] --- A4["9"] --- A5["2"]
    end
    subgraph "after pass 1 — largest unsorted value bubbled to the end"
        direction LR
        B0["3"] --- B1["5"] --- B2["1"] --- B3["8"] --- B4["2"] --- B5["9"]
    end
Caso Costo Por qué
Mejor caso (ya ordenado) O(n) una sola pasada no hace ningún intercambio, la verificación de salida anticipada se detiene de inmediato
Peor caso (orden inverso) O(n²) cada una de las n−1 pasadas hace un intercambio, ninguna puede salir anticipadamente
Promedio / casi ordenado entre O(n) y O(n²) el costo sigue cuánto están realmente fuera de lugar los elementos, no solo n

Ejemplo clásico

classic/BubbleSort es una implementación genérica, impulsada por Comparator — sin el atajo de Arrays.sort/Collections.sort. Hacerla genérica sobre Comparator<? super T> en lugar de fijar int[] es lo que permite que el ejemplo aplicado de abajo reutilice este mismo método sort en un tipo de dominio, en lugar de necesitar una segunda implementación paralela. BubbleSortTest cubre un array desordenado, un array ya ordenado, un array en orden inverso (los dos extremos que mide el benchmark de abajo), duplicados, un array de un solo elemento y un array vacío, un comparador personalizado (descendente), y ambas protecciones contra argumento nulo.

Ejemplo aplicado: corrección del libro mayor diario en un mainframe legado

applied/DailyLedgerReorder modela un patrón real con el que los trabajos por lotes de mainframes legados todavía se topan: el archivo del libro mayor de ayer se cerró ya ordenado por hora de contabilización, y ahora una única entrada de corrección tardía necesita reinsertarse en su posición correcta antes de que el lote pueda reprocesarse. Agregar la corrección y volver a ejecutar bubble sort sobre todo el lote (todavía casi completamente ordenado) es una elección legítima precisamente porque la perturbación es pequeña y localizada — la adaptabilidad de bubble sort significa que el costo real depende de cuán fuera de lugar está esa única corrección, no del tamaño de todo el lote. DailyLedgerReorderTest cubre una corrección que pertenece al medio, una que pertenece al inicio mismo, una que pertenece al final mismo (el caso trivial en que ya está en su lugar), y ambas protecciones contra argumento nulo.

Benchmark

./gradlew :sorting:bubble-sort: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). Tres órdenes de entrada en cada tamaño: ya ordenado, casi ordenado (una pequeña cantidad de intercambios de pares adyacentes dispersos por el array — desorden genuinamente localizado, no solo un puñado de intercambios aleatorios de largo alcance, que pueden reproducir accidentalmente una perturbación similar al peor caso incluso con una cantidad "pequeña" de intercambios), y completamente aleatorio:

Costo de ordenamiento size=100 size=1,000 size=10,000
ya ordenado 0.099 µs 0.750 µs 10.363 µs
casi ordenado 0.232 µs 1.997 µs 40.054 µs
aleatorio 17.971 µs 2,198.934 µs 337,009.203 µs

En size=10,000, el caso aleatorio es ~32.527x más lento que el caso ya ordenado, y ~8.414x más lento que el caso casi ordenado — en exactamente el mismo código, la única variable es cuán ordenada ya estaba la entrada. Esa es la afirmación de adaptabilidad de "La solución" de arriba, convertida en un número medido en lugar de una aseveración: esto es precisamente lo que el CLRS de Cormen, Leiserson, Rivest & Stein establece de forma abstracta en el Problema 2-2 ("Correctness of bubblesort") — O(n) en el mejor caso, O(n²) en el peor caso — hecho concreto en esta máquina. El intervalo de confianza del caso aleatorio en size=10,000 es amplio (el ruido de JVM/GC a escala de milisegundos de un solo dígito domina una carga de trabajo O(n²) de ese tamaño en una máquina de desarrollo compartida) — la brecha de más de ~1,000x entre los órdenes es la señal confiable aquí, no el último dígito de ningún número individual.

Cuándo no usarlo

Cobertura de pruebas

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

./gradlew :sorting:bubble-sort:jacocoTestReport

Informe en sorting/bubble-sort/build/reports/jacoco/test/html/index.html.

Lectura complementaria

Pruebas unitarias

src/test/java/com/algorithms/sorting/bubblesort/classic/BubbleSortTest.java
package com.algorithms.sorting.bubblesort.classic;

import org.junit.jupiter.api.Test;

import java.util.Comparator;

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

class BubbleSortTest {

    @Test
    void sortsAnUnorderedArrayIntoAscendingOrder() {
        Integer[] array = {5, 3, 8, 1, 9, 2};

        BubbleSort.sort(array, Comparator.naturalOrder());

        assertThat(array).containsExactly(1, 2, 3, 5, 8, 9);
    }

    @Test
    void alreadySortedArrayStaysSorted() {
        Integer[] array = {1, 2, 3, 4, 5};

        BubbleSort.sort(array, Comparator.naturalOrder());

        assertThat(array).containsExactly(1, 2, 3, 4, 5);
    }

    @Test
    void reverseSortedArrayEndsUpAscending() {
        Integer[] array = {5, 4, 3, 2, 1};

        BubbleSort.sort(array, Comparator.naturalOrder());

        assertThat(array).containsExactly(1, 2, 3, 4, 5);
    }

    @Test
    void duplicatesAreHandledCorrectly() {
        Integer[] array = {3, 1, 3, 2, 1};

        BubbleSort.sort(array, Comparator.naturalOrder());

        assertThat(array).containsExactly(1, 1, 2, 3, 3);
    }

    @Test
    void singleElementArrayStaysUnchanged() {
        Integer[] array = {42};

        BubbleSort.sort(array, Comparator.naturalOrder());

        assertThat(array).containsExactly(42);
    }

    @Test
    void emptyArrayStaysEmpty() {
        Integer[] array = {};

        BubbleSort.sort(array, Comparator.naturalOrder());

        assertThat(array).isEmpty();
    }

    @Test
    void sortsUsingACustomComparatorForDescendingOrder() {
        Integer[] array = {1, 5, 3, 2, 4};

        BubbleSort.sort(array, Comparator.reverseOrder());

        assertThat(array).containsExactly(5, 4, 3, 2, 1);
    }

    @Test
    void rejectsANullArray() {
        assertThatThrownBy(() -> BubbleSort.sort(null, Comparator.naturalOrder()))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsANullComparator() {
        assertThatThrownBy(() -> BubbleSort.sort(new Integer[] {1, 2}, null))
                .isInstanceOf(IllegalArgumentException.class);
    }
}
src/test/java/com/algorithms/sorting/bubblesort/applied/DailyLedgerReorderTest.java
package com.algorithms.sorting.bubblesort.applied;

import org.junit.jupiter.api.Test;

import java.time.Instant;
import java.time.temporal.ChronoUnit;

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

class DailyLedgerReorderTest {

    private static final Instant BASE = Instant.parse("2026-08-17T09:00:00Z");

    @Test
    void spliceALateCorrectionIntoTheMiddleOfTheBatch() {
        DailyLedgerReorder reorder = new DailyLedgerReorder();
        LedgerEntry[] batch = {
                entry("a", 0),
                entry("b", 1),
                entry("c", 3),
                entry("d", 4),
        };
        LedgerEntry correction = entry("late", 2);

        LedgerEntry[] result = reorder.reorderWithCorrection(batch, correction);

        assertThat(result).extracting(LedgerEntry::id).containsExactly("a", "b", "late", "c", "d");
    }

    @Test
    void correctionThatBelongsAtTheStartMovesToTheFront() {
        DailyLedgerReorder reorder = new DailyLedgerReorder();
        LedgerEntry[] batch = {entry("a", 1), entry("b", 2), entry("c", 3)};
        LedgerEntry correction = entry("earliest", 0);

        LedgerEntry[] result = reorder.reorderWithCorrection(batch, correction);

        assertThat(result).extracting(LedgerEntry::id).containsExactly("earliest", "a", "b", "c");
    }

    @Test
    void correctionThatBelongsAtTheEndStaysAppended() {
        DailyLedgerReorder reorder = new DailyLedgerReorder();
        LedgerEntry[] batch = {entry("a", 0), entry("b", 1)};
        LedgerEntry correction = entry("latest", 2);

        LedgerEntry[] result = reorder.reorderWithCorrection(batch, correction);

        assertThat(result).extracting(LedgerEntry::id).containsExactly("a", "b", "latest");
    }

    @Test
    void rejectsANullBatch() {
        DailyLedgerReorder reorder = new DailyLedgerReorder();

        assertThatThrownBy(() -> reorder.reorderWithCorrection(null, entry("x", 0)))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsANullCorrection() {
        DailyLedgerReorder reorder = new DailyLedgerReorder();

        assertThatThrownBy(() -> reorder.reorderWithCorrection(new LedgerEntry[0], null))
                .isInstanceOf(IllegalArgumentException.class);
    }

    private static LedgerEntry entry(String id, long offsetMinutes) {
        return new LedgerEntry(id, BASE.plus(offsetMinutes, ChronoUnit.MINUTES));
    }
}

Ver informe completo de cobertura JaCoCo →