▷ Timsort en Python — el que usa Python de verdad cuando llamas a sort 2026

Timsort en Python — el que usa Python de verdad cuando llamas a sort

Cuando llamas a sorted([3, 1, 4, 1, 5]) en Python, el algoritmo que se ejecuta detrás se llama Timsort. No es quicksort. No es merge sort. No es heapsort. Es un híbrido inteligente que Tim Peters inventó en 2002 específicamente para Python, y que después se ha copiado en JavaScript (V8, desde 2018) y en Java (para objetos, desde 1.7).

La idea es preciosa por su pragmatismo: las listas del mundo real casi nunca son aleatorias. Tienen tramos ya ordenados, datos parcialmente clasificados, ráfagas de actualización. Los algoritmos clásicos (quick, merge, heap) ignoran eso y procesan toda la lista como si fuera caótica. Timsort, en cambio, detecta los tramos ya ordenados (los llama “runs”), los respeta, y solo trabaja para fusionarlos.

Resultado: en el mejor caso es Ω(n) — lineal. En el peor, Θ(n log n) como cualquier algoritmo serio. Estable (preserva orden de iguales). Y aprovecha hardware moderno (uso de caché, predicción de ramas) mejor que casi cualquier otro.

Esta entrada cubre el algoritmo en serio: su idea base, una versión simplificada en Python (la real es muy compleja), comparativa con sus rivales, y por qué Tim Peters es uno de los nombres más respetados de Python.

Si quieres el panorama de los 14 algoritmos del clúster: Algoritmos de ordenación en Python — los 14 explicados.

Timsort en una frase

Detecta tramos ya ordenados (“runs”) en la lista. Ordena cada run con insertion sort (rapidísimo en tramos cortos). Fusiona los runs con merge sort (estable y O(n log n)). Resultado: lo mejor de ambos, aprovechando el orden parcial natural de las listas reales.

Animación sobre 12 valores:

Antes de seguir: qué quieren decir O, Θ y Ω

En las tablas verás los tres símbolos. Idea rápida:

  • O(n log n) — “O grande”, cota superior (peor caso).
  • Ω(n) — “Omega”, cota inferior (mejor caso): lista ya ordenada → Timsort la detecta como un único run y termina en una pasada lineal.
  • Θ(n log n) — caso típico.

Timsort tiene un mejor caso Ω(n) que ningún otro O(n log n) tiene. Por eso es ideal para listas reales (que están parcialmente ordenadas). Más detalle en el pilar del clúster.

La idea fundamental: runs

Un run es un tramo de la lista que ya está ordenado (ascendente o descendente). Por ejemplo, en [1, 3, 5, 9, 2, 4, 6, 8] hay dos runs ascendentes:
[1, 3, 5, 9] (run 1)
[2, 4, 6, 8] (run 2)

En [5, 3, 1, 9, 8, 2, 7, 6] hay tres runs:
[5, 3, 1] (descendente; Timsort lo invierte)
[9, 8, 2] (descendente)
[7, 6] (descendente)

La observación clave de Tim Peters: las listas del mundo real casi siempre tienen runs naturales. Datos parcialmente ordenados, listas con un par de inserciones, datos por fechas con desorden esporádico. Si detectas y respetas esos runs, ahorras MUCHÍSIMO trabajo.

📥 Llévate el cheatsheet de Optimización Python (gratis)

PDF de 8 páginas con los patrones que más rendimiento te dan en Python: medir antes de optimizar, complejidad real, estructuras adecuadas, evitar trabajo repetido, profiling y cuándo paralelizar. Para tener al lado cuando algo va lento.

Sin spam. Te apuntas a la lista, descargas el cheatsheet y recibes contenido de rendimiento en Python cada semana.

Cómo funciona — versión conceptual

Timsort real es muy complejo (galloping mode, decisiones de fusión por tamaño…). La versión simplificada que vamos a usar aquí captura la idea:

  1. Recorre la lista identificando runs naturales (tramos ya ordenados). Si un run es ascendente, déjalo. Si es descendente, inviértelo (sigue siendo “casi gratis”, O(n) de ese tramo).
  2. Si un run es corto (típicamente < 32 elementos), extiéndelo aplicando insertion sort sobre los siguientes elementos. Esto convierte tramos cortos en runs más largos.
  3. Apila los runs en una pila. Cuando aparecen tres runs en cumbre, se aplican reglas de equilibrado (estilo merge balanceado).
  4. Fusiona pares de runs con merge sort. La fusión es estable (preserva orden de iguales).
  5. Termina cuando solo queda un run: la lista está ordenada.

Implementación didáctica en Python

La versión real está implementada en C en CPython (Objects/listobject.c) y ocupa miles de líneas. Esta versión didáctica captura solo la esencia:

MIN_RUN = 32


def ordenar(lista: list[int]) -> list[int]:
    n = len(lista)
    if n < 2:
        return lista

    # Fase 1: detectar runs naturales (asc/desc, tamaño variable).
    runs = []
    i = 0
    while i < n:
        run_inicio = i
        if i + 1 == n:
            runs.append((run_inicio, n))
            break
        ascendente = lista[i] <= lista[i + 1]
        j = i + 1
        while j + 1 < n and (
            (ascendente and lista[j] <= lista[j + 1]) or
            (not ascendente and lista[j] > lista[j + 1])
        ):
            j += 1
        run_fin = j + 1
        if not ascendente:
            _invertir(lista, run_inicio, run_fin - 1)
        # Si el run natural es corto, extender con insertion sort.
        deseado = min(run_inicio + MIN_RUN, n)
        if run_fin < deseado:
            _insertion(lista, run_inicio, run_fin, deseado)
            run_fin = deseado
        runs.append((run_inicio, run_fin))
        i = run_fin

    # Fase 2: fusionar pares de runs hasta tener uno solo.
    while len(runs) > 1:
        nuevos = []
        for k in range(0, len(runs), 2):
            if k + 1 < len(runs):
                lo, mid = runs[k]
                _, hi = runs[k + 1]
                _fusionar(lista, lo, mid, hi)
                nuevos.append((lo, hi))
            else:
                nuevos.append(runs[k])
        runs = nuevos
    return lista


def _invertir(lista, lo, hi):
    while lo < hi:
        lista[lo], lista[hi] = lista[hi], lista[lo]
        lo += 1; hi -= 1


def _insertion(lista, inicio, ya_ordenado_hasta, fin):
    for i in range(ya_ordenado_hasta, fin):
        actual = lista[i]
        j = i - 1
        while j >= inicio and lista[j] > actual:
            lista[j + 1] = lista[j]
            j -= 1
        lista[j + 1] = actual


def _fusionar(lista, lo, mid, hi):
    # mid = fin exclusivo del izquierdo; hi = fin exclusivo del derecho.
    izq = lista[lo:mid]
    der = lista[mid:hi]
    i = j = 0
    for k in range(lo, hi):
        if j >= len(der) or (i < len(izq) and izq[i] <= der[j]):
            lista[k] = izq[i]; i += 1
        else:
            lista[k] = der[j]; j += 1


if __name__ == "__main__":
    print(ordenar([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]))

La versión real añade encima:
Galloping mode: cuando una fusión detecta que un lado “gana” mucho, salta exponencialmente en vez de comparar uno a uno.
Política de equilibrado de la pila de runs (Tim/Powersort) para minimizar fusiones grandes.
Min-run dinámico: el tamaño mínimo de run se calcula a partir de n para maximizar el balance.

Si te pica la curiosidad, el código real está en Objects/listobject.c de CPython. Busca “TimSort” en el archivo. Hay un documento de diseño (listsort.txt) que explica todas las decisiones — es lectura obligatoria si quieres entender cómo se hace ingeniería de algoritmos en producción.

Implementación en JavaScript (versión simplificada)

const MIN_RUN = 32;

function ordenar(lista) {
  const n = lista.length;
  if (n < 2) return lista;

  // Fase 1: detectar runs naturales.
  const runs = [];
  let i = 0;
  while (i < n) {
    const inicio = i;
    if (i + 1 === n) { runs.push([inicio, n]); break; }
    const asc = lista[i] <= lista[i + 1];
    let j = i + 1;
    while (j + 1 < n && (
      (asc && lista[j] <= lista[j + 1]) ||
      (!asc && lista[j] > lista[j + 1])
    )) j++;
    let fin = j + 1;
    if (!asc) invertir(lista, inicio, fin - 1);
    const deseado = Math.min(inicio + MIN_RUN, n);
    if (fin < deseado) { insertionSort(lista, inicio, fin, deseado); fin = deseado; }
    runs.push([inicio, fin]);
    i = fin;
  }

  // Fase 2: fusionar pares de runs hasta uno solo.
  let pendientes = runs;
  while (pendientes.length > 1) {
    const nuevos = [];
    for (let k = 0; k < pendientes.length; k += 2) {
      if (k + 1 < pendientes.length) {
        const [lo, mid] = pendientes[k];
        const [, hi] = pendientes[k + 1];
        fusionar(lista, lo, mid, hi);
        nuevos.push([lo, hi]);
      } else nuevos.push(pendientes[k]);
    }
    pendientes = nuevos;
  }
  return lista;
}

function invertir(lista, lo, hi) {
  while (lo < hi) { [lista[lo], lista[hi]] = [lista[hi], lista[lo]]; lo++; hi--; }
}

function insertionSort(lista, inicio, desde, fin) {
  for (let i = desde; i < fin; i++) {
    const actual = lista[i];
    let j = i - 1;
    while (j >= inicio && lista[j] > actual) { lista[j + 1] = lista[j]; j--; }
    lista[j + 1] = actual;
  }
}

function fusionar(lista, lo, mid, hi) {
  const izq = lista.slice(lo, mid);
  const der = lista.slice(mid, hi);
  let i = 0, j = 0;
  for (let k = lo; k < hi; k++) {
    if (j >= der.length || (i < izq.length && izq[i] <= der[j])) lista[k] = izq[i++];
    else lista[k] = der[j++];
  }
}

Implementación en Java

private static final int MIN_RUN = 32;

public static void ordenar(int[] lista) {
    int n = lista.length;
    if (n < 2) return;

    // Fase 1: detectar runs naturales.
    List<int[]> runs = new ArrayList<>();
    int i = 0;
    while (i < n) {
        int inicio = i;
        if (i + 1 == n) { runs.add(new int[]{inicio, n}); break; }
        boolean asc = lista[i] <= lista[i + 1];
        int j = i + 1;
        while (j + 1 < n && (
            (asc && lista[j] <= lista[j + 1]) ||
            (!asc && lista[j] > lista[j + 1])
        )) j++;
        int fin = j + 1;
        if (!asc) invertir(lista, inicio, fin - 1);
        int deseado = Math.min(inicio + MIN_RUN, n);
        if (fin < deseado) { insertion(lista, inicio, fin, deseado); fin = deseado; }
        runs.add(new int[]{inicio, fin});
        i = fin;
    }

    // Fase 2: fusionar pares de runs.
    while (runs.size() > 1) {
        List<int[]> nuevos = new ArrayList<>();
        for (int k = 0; k < runs.size(); k += 2) {
            if (k + 1 < runs.size()) {
                int lo  = runs.get(k)[0];
                int mid = runs.get(k)[1];
                int hi  = runs.get(k + 1)[1];
                fusionar(lista, lo, mid, hi);
                nuevos.add(new int[]{lo, hi});
            } else nuevos.add(runs.get(k));
        }
        runs = nuevos;
    }
}

private static void invertir(int[] lista, int lo, int hi) {
    while (lo < hi) {
        int tmp = lista[lo]; lista[lo] = lista[hi]; lista[hi] = tmp;
        lo++; hi--;
    }
}

private static void insertion(int[] lista, int inicio, int desde, int fin) {
    for (int i = desde; i < fin; i++) {
        int actual = lista[i];
        int j = i - 1;
        while (j >= inicio && lista[j] > actual) {
            lista[j + 1] = lista[j];
            j--;
        }
        lista[j + 1] = actual;
    }
}

private static void fusionar(int[] lista, int lo, int mid, int hi) {
    int[] izq = Arrays.copyOfRange(lista, lo, mid);
    int[] der = Arrays.copyOfRange(lista, mid, hi);
    int i = 0, j = 0;
    for (int k = lo; k < hi; k++) {
        if (j >= der.length || (i < izq.length && izq[i] <= der[j])) {
            lista[k] = izq[i++];
        } else {
            lista[k] = der[j++];
        }
    }
}

Complejidad — lo mejor de ambos mundos

MétricaMejor casoCaso medioPeor caso
TiempoΩ(n)Θ(n log n)O(n log n)
MemoriaO(n)O(n)O(n)
Estable

El detalle clave: ningún otro O(n log n) tiene Ω(n) en el mejor caso. Quicksort, merge, heap están todos en Ω(n log n) — la lista ya ordenada no les ahorra trabajo. Timsort detecta “ya está ordenada, es un único run, devuelvo”.

Por qué Θ(n log n) en el caso medio: la fase de fusión tiene la misma estructura de merge sort. log n niveles de fusión, cada uno O(n) trabajo.

Por qué es estable: insertion y merge ambos son estables. La fusión preserva el orden de iguales (la comparación es <=, no <).

Ver en acción — Timsort sobre 10 listas fijas

Todo el clúster usa las mismas 10 listas fijas (n=15):

#NombreQué es
1ordenada1, 2, 3, ..., 15
2inversa15, 14, ..., 1
3casi_ordenada1..15 con un par de swaps
4muy_desordenadaPatrón min-max alternado
5duplicadosEnteros del rango [1..5] repetidos
6-10aleatoria_s1 .. aleatoria_s55 permutaciones aleatorias deterministas

Aplicamos Timsort (versión simplificada de sortlab):

from sortlab import bench_estandar, formato_tabla
print(formato_tabla(bench_estandar("tim", n=15)))

Salida real (n=15):

Lista            Comp.  Interc.  Escr.  Tiempo (ms)   Caso  Θ observada
---------------  -----  -------  -----  -----------  -----  -----------
ordenada         14           0      0        0.002  Mejor         Θ(n)
inversa          14           7     14        0.003  Mejor         Θ(n)
casi_ordenada    32           0     30        0.005  Medio   Θ(n log n)
muy_desordenada  64           0     62        0.008   Peor   Θ(n log n)
duplicados       54           0     51        0.007   Peor   Θ(n log n)
aleatoria_s1     74           1     73        0.010   Peor   Θ(n log n)
aleatoria_s2     65           1     68        0.009   Peor   Θ(n log n)
aleatoria_s3     73           1     73        0.009   Peor   Θ(n log n)
aleatoria_s4     82           0     82        0.010   Peor        Θ(n²)
aleatoria_s5     63           0     62        0.008   Peor   Θ(n log n)

Lectura de la tabla:

  • ordenada → 14 comparaciones, Mejor + Θ(n). Tim detecta la lista como UN único run ascendente y termina con una sola pasada. n-1 = 14 comparaciones exactas. Es el sello de Tim: SOLO Tim hace esto.
  • inversa → 14 comparaciones, Mejor + Θ(n). La misma magia, en el otro extremo: Tim detecta la lista como un único run descendente, lo invierte en una pasada (7 swaps) y termina. También lineal. Ningún otro O(n log n) consigue esto.
  • casi_ordenada → 32 comparaciones, Medio + Θ(n log n). La lista tiene 2 runs cortos que se extienden con insertion y luego se fusionan. Menos de la mitad del trabajo de un merge puro sobre la misma lista.
  • Aleatorias → entre 63 y 82 comparaciones. El caso medio. Tim no encuentra runs naturales largos en datos aleatorios y degenera a un merge sort con insertion previo.

Confirmación con n=100

Para ver el comportamiento de runs + fusiones sobre una lista más grande:

print(formato_tabla(bench_estandar("tim", n=100)))
Lista            Comp.  Tiempo (ms)   Caso  Θ observada
---------------  -----  -----------  -----  -----------
ordenada         99           0.008  Mejor         Θ(n)
inversa          99           0.014  Mejor         Θ(n)
casi_ordenada    410          0.051  Medio   Θ(n log n)
muy_desordenada  958          0.111   Peor   Θ(n log n)
duplicados       979          0.108   Peor   Θ(n log n)
aleatoria_s1     994          0.110   Peor   Θ(n log n)
aleatoria_s2     907          0.104   Peor   Θ(n log n)
aleatoria_s3     887          0.103   Peor   Θ(n log n)
aleatoria_s4     1.085        0.115   Peor   Θ(n log n)
aleatoria_s5     993          0.113   Peor   Θ(n log n)

Ahora se ve nítido:
ordenada e inversa → 99 comparaciones cada una. Idénticas en trabajo: detección de un único run natural, invertido si hace falta, y listo. n-1 comparaciones exactas. Esto es el Ω(n) en acción a escala.
casi_ordenada → 410 (la magia: detecta los runs largos ya ordenados y solo trabaja en fusionarlos con los desordenados).
Aleatorias → 880-1100 (caso medio bueno, todas en Θ(n log n)).

Comparemos contra merge sort puro sobre la misma aleatoria_s1:

Algoritmon=100n=1000n=10000
Merge5558.702120.560
Tim (simplificado)99412.534161.012

Tim simplificado parece un poco peor que merge puro en aleatoria pura — porque no hay runs naturales que aprovechar, y aún así pagamos el coste de buscarlos. La versión real de CPython (escrita en C, con galloping y reglas de equilibrado de la pila de runs) bate a merge puro en todos los escenarios, especialmente cuando hay orden parcial (que es lo habitual en datos reales).

Reproducirlo sin instalar nada

sortlab vive privada en el Python profesional: medir, optimizar y escalar. Versión instrumentada inline:

MIN_RUN = 32


def tim_medido(lista_original: list[int]) -> dict:
    arr = list(lista_original)
    comparaciones = 0
    escrituras = 0
    n = len(arr)

    def invertir(lo, hi):
        nonlocal escrituras
        while lo < hi:
            arr[lo], arr[hi] = arr[hi], arr[lo]
            escrituras += 2
            lo += 1; hi -= 1

    def insertion(inicio, desde, fin):
        nonlocal comparaciones, escrituras
        for i in range(desde, fin):
            actual = arr[i]
            j = i - 1
            while j >= inicio:
                comparaciones += 1
                if arr[j] <= actual:
                    break
                arr[j + 1] = arr[j]
                escrituras += 1
                j -= 1
            arr[j + 1] = actual
            escrituras += 1

    # Fase 1: detectar runs naturales.
    runs = []
    i = 0
    while i < n:
        inicio = i
        if i + 1 == n:
            runs.append((inicio, n)); break
        comparaciones += 1
        asc = arr[i] <= arr[i + 1]
        j = i + 1
        while j + 1 < n:
            comparaciones += 1
            if asc and arr[j] <= arr[j + 1]:    j += 1
            elif not asc and arr[j] > arr[j + 1]: j += 1
            else: break
        fin = j + 1
        if not asc:
            invertir(inicio, fin - 1)
        deseado = min(inicio + MIN_RUN, n)
        if fin < deseado:
            insertion(inicio, fin, deseado); fin = deseado
        runs.append((inicio, fin))
        i = fin

    # Fase 2: fusionar pares de runs.
    aux = arr.copy()
    while len(runs) > 1:
        nuevos = []
        for k in range(0, len(runs), 2):
            if k + 1 < len(runs):
                lo, mid = runs[k]
                _, hi = runs[k + 1]
                for kk in range(lo, hi):
                    aux[kk] = arr[kk]
                ii, jj = lo, mid
                for kk in range(lo, hi):
                    if ii >= mid:
                        arr[kk] = aux[jj]; jj += 1
                    elif jj >= hi:
                        arr[kk] = aux[ii]; ii += 1
                    else:
                        comparaciones += 1
                        if aux[ii] <= aux[jj]:
                            arr[kk] = aux[ii]; ii += 1
                        else:
                            arr[kk] = aux[jj]; jj += 1
                    escrituras += 1
                nuevos.append((lo, hi))
            else:
                nuevos.append(runs[k])
        runs = nuevos

    return {"ordenada": arr, "comparaciones": comparaciones, "escrituras": escrituras}


# Probar sobre las 5 listas con forma:
listas = {
    "ordenada":         list(range(1, 16)),
    "inversa":          list(range(15, 0, -1)),
    "casi_ordenada":    [1, 11, 3, 4, 5, 6, 7, 8, 9, 10, 2, 12, 13, 14, 15],
    "muy_desordenada":  [1, 15, 2, 14, 3, 13, 4, 12, 5, 11, 6, 10, 7, 9, 8],
    "duplicados":       [1, 1, 3, 2, 2, 2, 1, 5, 1, 5, 4, 1, 1, 1, 2],
}
for nombre, lista in listas.items():
    r = tim_medido(lista)
    print(f"{nombre:<16}  comp={r['comparaciones']:>3}  esc={r['escrituras']:>3}")

Cuándo usar Timsort

Siempre que ordenes en Python o JavaScript moderno. Ya lo estás usando: sorted(), list.sort(), Array.prototype.sort() ejecutan Timsort por defecto. No tienes que implementarlo tú.

Solo se implementa Timsort a mano en estos escenarios:
Educativos: para entender cómo combinar dos algoritmos.
Sistemas embebidos sin stdlib Python: si tienes que ordenar en un microcontrolador.
Lenguajes que NO lo tienen aún: Go usa pdqsort por defecto pero hay implementaciones de Timsort en bibliotecas.

Cuándo NO usar Timsort:
Si NO tienes la memoria O(n) extra disponible. Timsort necesita un buffer auxiliar para las fusiones. Si la memoria es escasa, usa heapsort (O(1)).
Para enteros con rango pequeño: radix o counting baten a Timsort cuando se puede aplicar.

Comparativa rápida

AlgoritmoMejorPeorMemoriaEstableVentaja única
QuicksortΩ(n log n)O(n²)O(log n)NoRápido en aleatoria
MergeΩ(n log n)O(n log n)O(n)Garantía pura
HeapΩ(n log n)O(n log n)O(1)NoSin memoria extra
TimsortΩ(n)O(n log n)O(n)Aprovecha runs naturales
InsertionΩ(n)O(n²)O(1)Rey en tramos cortos
MergeΩ(n log n)O(n log n)O(n)Mitad de Timsort

Por qué los lenguajes adoptaron Timsort:
– Python (Tim Peters, 2002): el lenguaje original.
– Java (2009): para Collections.sort y List.sort (objetos). Mantienen Dual-Pivot Quicksort para primitivos por velocidad pura sobre int[].
– JavaScript V8 (2018): el motor de Chrome y Node.js. La razón: Timsort es estable; el QuickSort que usaban antes no lo era, y se notaba en operaciones de la web.

Tres de los lenguajes más usados del mundo corren tu sort() con Timsort. Es la prueba de que el algoritmo es realmente la mejor opción de uso general hoy.

Mitos que circulan sobre Timsort

  • “Es solo insertion + merge.” Es la idea base, pero la implementación real tiene MUCHOS detalles: galloping mode, política de equilibrado de runs, mínimo dinámico, descarga de fusiones. El documento de diseño (listsort.txt) tiene 700 líneas.
  • “Es el más rápido siempre.” Es el más rápido en uso general sobre datos reales. Para escenarios específicos (enteros pequeños), counting/radix lo baten. Para listas completamente aleatorias y grandes, quicksort puede empatar.
  • “Python lo inventó.” Tim Peters lo inventó para Python. Pero la idea de detectar runs naturales tiene antecedentes en algoritmos clásicos de los 70 — Peters los combinó con merge sort moderno y galloping de una manera nueva.

¿Y en la stdlib de Python?

SÍ. Cuando llamas a sorted() o list.sort(), estás ejecutando Timsort. No hay otra implementación expuesta. El código vive en Objects/listobject.c de CPython, escrito en C, mantenido desde 2002.

Para listas pequeñas (n ≤ 32), Python usa insertion sort directo dentro de Timsort. Es decir: si ordenas una lista de 10 elementos, NO estás corriendo merge sort — estás corriendo insertion sort. Eso es la sutileza de Timsort.

Trivia algorítmica

Tim Peters es uno de los nombres más respetados de la comunidad Python. Es también el autor del Zen of Python (import this en una terminal Python para verlo). Inventó Timsort en 2002 mientras trabajaba como ingeniero en una empresa privada — fue contribución abierta a Python sin esperar nada a cambio.

El nombre “Timsort” no lo eligió él; fue la comunidad la que empezó a llamarlo así en homenaje. Tim Peters está orgulloso pero modesto al respecto: en el archivo listsort.txt de CPython, NO menciona que el algoritmo lleva su nombre — solo describe técnicamente cómo funciona.

Lectura imprescindible si te interesa la ingeniería de algoritmos en producción: listsort.txt. Es un documento de diseño escrito en 2002 y mantenido desde entonces. Explica TODAS las decisiones técnicas — incluida la elección del mínimo de run, el galloping mode, la política de equilibrado. Es un master class de cómo se hace ingeniería seria.

FAQ rápida

¿Es Timsort lo mismo que merge sort?
No. Timsort detecta runs naturales (lo que merge no hace) y usa insertion para tramos cortos (lo que merge no hace). En el peor caso degenera a algo parecido a merge, pero el caso medio es muy distinto.

¿Es estable?
Sí. Es uno de sus argumentos clave: ordenas objetos por una clave y se preserva el orden de iguales.

¿Es in-place?
No. Necesita un buffer auxiliar de tamaño hasta n/2 para las fusiones. Si necesitas O(1) memoria, usa heapsort.

¿Por qué Java solo lo usa para objetos, no para primitivos?
Porque para int[], el coste de instanciar el comparator y el overhead de objeto hacen que Dual-Pivot Quicksort sea más rápido en la práctica. Para Object[] (donde la comparación es por método y el orden estable importa), Timsort gana.

¿Cómo puedo desactivar Timsort y usar otra cosa?
En Python, no puedes (es la implementación nativa). Si necesitas otra cosa, importas heapq (heap) o implementas el algoritmo a mano.

¿Te ha valido esto?

Si te ha resultado útil, llévate el cheatsheet de Optimización Python en PDF — 8 páginas con cómo medir, cuándo importa la complejidad, qué estructura usar y profiling práctico. Para tener al lado cuando algo va lento. Gratis.

Sin spam. Email + cheatsheet de rendimiento + un correo por semana sobre medir y optimizar Python.

En resumen

Timsort es el algoritmo de ordenación más usado del mundo: cada vez que escribes sorted() en Python, Array.prototype.sort() en JavaScript o Collections.sort() en Java (para objetos), Tim Peters está ejecutándose por debajo. Su genialidad es aprovechar que las listas del mundo real no son aleatorias: tienen runs naturales que el algoritmo respeta. Resultado: lo mejor de insertion (rápido en tramos cortos) + lo mejor de merge (estable y O(n log n) garantizado) + un Ω(n) en el mejor caso que ningún otro O(n log n) ofrece.

Es la prueba de que la mejor algoritmia no es la más elegante, sino la más pragmática: la que se adapta a los datos reales en vez de pelearse con ellos.

Si quieres aprender a diseñar algoritmos pragmáticos que se adapten a tus datos reales —no a recitar los clásicos como si todos los problemas fueran libros de texto— ese es el camino del curso de El Pythonista.

Y con esto cerramos el clúster: 14 algoritmos explicados, comparados, animados. Cada uno con su nicho. El pilar te resume cuál usar cuándo. Y si llegaste hasta aquí leyendo los 14, ya sabes más sobre ordenación que el 99% de developers en activo. Enhorabuena.

¿Quieres aprender Python en orden, no a saltos?

Esto que has leído es solo una pieza. En El Pythonista lo verás todo encadenado: 11 módulos, 37+ horas de vídeo, 734 actividades y un proyecto real (MovieTracker) que crece contigo desde la primera variable hasta el deploy a producción.

Ver el curso completo →

37+ horas · 734 actividades · Proyecto real · Acceso de por vida · 14 días de garantía

Compartir

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Publicar un comentario