← Todos los algoritmos

Insertion Sort

Ordenamiento · ver código fuente en GitHub

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

Categoría: Sorting

El problema

Para un lote pequeño — de un puñado a unas pocas docenas de elementos — recurrir a un algoritmo de propósito general con O(n log n) garantizado tiene un costo de configuración real (recursión, particionamiento, arrays auxiliares) que eclipsa el trabajo real una vez que n es lo bastante pequeño. Lo que se necesita en esa escala es el algoritmo con el menor factor constante por comparación, no el mejor techo asintótico — y los sorts de producción reales ya hacen exactamente esa concesión.

La solución

Haz crecer un prefijo ordenado un elemento a la vez: toma el siguiente elemento y desplázalo hacia la izquierda a través del prefijo ya ordenado hasta que caiga en su posición correcta. El costo de ese desplazamiento es proporcional a cuántos elementos del prefijo están realmente fuera de lugar respecto a él — lo que significa que el costo total sigue el número total de inversiones del array, no solo su tamaño. Un array ya ordenado tiene cero inversiones (cada elemento se desplaza cero posiciones, O(n) en total); un array en orden inverso tiene el número máximo posible (cada elemento se desplaza hasta el principio, O(n²) en total). Por eso precisamente Arrays.sort/TimSort del propio JDK cambia a insertion sort por debajo de un umbral de tamaño pequeño, en lugar de pagar el costo de configuración de merge/quicksort en una ejecución diminuta.

flowchart TB
    subgraph "sorted prefix [1,3,5], next = 2"
        direction LR
        P0["1"] --- P1["3"] --- P2["5"] --- N["2 →"]
    end
    subgraph "2 shifted left past 5 and 3, inserted after 1"
        direction LR
        Q0["1"] --- Q1["2"] --- Q2["3"] --- Q3["5"]
    end
Caso Costo Por qué
Mejor caso (ya ordenado, cero inversiones) O(n) la distancia de desplazamiento de cada elemento es cero
Peor caso (orden inverso, inversiones máximas) O(n²) cada elemento se desplaza hasta el principio
Promedio / casi ordenado proporcional al número real de inversiones el costo sigue directamente el desorden, no solo n

Ejemplo clásico

classic/InsertionSort es genérico sobre Comparator<? super T> — sin atajo de Arrays.sort/Collections.sort — el bucle de desplazamiento mueve los elementos una posición a la vez usando asignación simple, sin swaps. InsertionSortTest cubre un array desordenado, un array ya ordenado, un array en orden inverso, duplicados, un array de un solo elemento y un array vacío, un comparator personalizado (descendente), y ambas protecciones contra argumento nulo.

Ejemplo aplicado: ordenamiento por lotes de registros de detalle de llamadas de telecom

applied/CallDetailRecordSort ordena un pequeño lote de registros de detalle de llamadas por hora de inicio antes de entregarlos a un motor de rating/facturación en tiempo real. Las llamadas de un único suscriptor dentro de una ventana corta de facturación son exactamente la forma en la que este algoritmo destaca: una n pequeña y — dado que la ingesta de eventos del lado del operador es, en sí misma, aproximadamente cronológica — por lo general ya está casi ordenada cuando llega a esta etapa. La clase documenta RECOMMENDED_MAX_BATCH_SIZE (64) como el mismo tipo de umbral de tamaño que usan los sorts de producción reales antes de abandonar insertion sort. CallDetailRecordSortTest cubre registros ordenados en la secuencia correcta, que el array de entrada queda intacto (el método devuelve un nuevo array ordenado), y la protección contra argumento nulo.

Benchmark

./gradlew :sorting:insertion-sort:jmh

Ejecución real en esta máquina (JMH 1.37, JDK 26.0.2, 2 iteraciones de warmup + 3 de medición, 1 fork). Mismos tres ordenamientos y tamaños que el benchmark de Bubble Sort de este repositorio, así que ambos son directamente comparables:

Costo del sort size=100 size=1,000 size=10,000
ya ordenado 0.269 µs 2.213 µs 30.636 µs
casi ordenado 0.715 µs 7.408 µs 72.491 µs
aleatorio 9.275 µs 691.996 µs 73,063.752 µs

En size=10,000, el caso aleatorio es ~2.384x más lento que el ya ordenado — la afirmación sobre el conteo de inversiones de "La solución" más arriba, hecha medible. Vale la pena compararlo directamente con el benchmark de Bubble Sort de este repositorio: mismos ordenamientos, mismos tamaños, misma máquina — el costo del caso aleatorio de insertion sort en size=10,000 (73,063.752 µs) queda bien por debajo de la mitad del de bubble sort (337,009.203 µs), lo cual coincide con el resultado conocido de que insertion sort hace aproximadamente la mitad de los movimientos de elementos que hace bubble sort para el mismo desorden, aunque ambos sean O(n²) en el peor caso. Misma clase asintótica, constante medible distinta.

Cuándo no usarlo

Cobertura de pruebas

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

./gradlew :sorting:insertion-sort:jacocoTestReport

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

Lectura complementaria

Pruebas unitarias

src/test/java/com/algorithms/sorting/insertionsort/classic/InsertionSortTest.java
package com.algorithms.sorting.insertionsort.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 InsertionSortTest {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        assertThat(array).containsExactly(42);
    }

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

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

        assertThat(array).isEmpty();
    }

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

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

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

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

    @Test
    void rejectsANullComparator() {
        assertThatThrownBy(() -> InsertionSort.sort(new Integer[] {1, 2}, null))
                .isInstanceOf(IllegalArgumentException.class);
    }
}
src/test/java/com/algorithms/sorting/insertionsort/applied/CallDetailRecordSortTest.java
package com.algorithms.sorting.insertionsort.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 CallDetailRecordSortTest {

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

    @Test
    void sortByStartTimeOrdersRecordsAscendingByStartTime() {
        CallDetailRecordSort sorter = new CallDetailRecordSort();
        CallDetailRecord[] records = {
                record("c", 5),
                record("a", 1),
                record("b", 3),
        };

        CallDetailRecord[] sorted = sorter.sortByStartTime(records);

        assertThat(sorted).extracting(CallDetailRecord::callId).containsExactly("a", "b", "c");
    }

    @Test
    void sortByStartTimeDoesNotMutateTheInputArray() {
        CallDetailRecordSort sorter = new CallDetailRecordSort();
        CallDetailRecord[] records = {record("b", 2), record("a", 1)};

        sorter.sortByStartTime(records);

        assertThat(records).extracting(CallDetailRecord::callId).containsExactly("b", "a");
    }

    @Test
    void rejectsNullRecords() {
        CallDetailRecordSort sorter = new CallDetailRecordSort();

        assertThatThrownBy(() -> sorter.sortByStartTime(null)).isInstanceOf(IllegalArgumentException.class);
    }

    private static CallDetailRecord record(String callId, long offsetMinutes) {
        return new CallDetailRecord(callId, BASE.plus(offsetMinutes, ChronoUnit.MINUTES));
    }
}

Ver informe completo de cobertura JaCoCo →