cv.cm

Programación · Algoritmos

Heapsort (ordenamiento por montículo): cómo funciona, código y complejidad

6 min de lectura

Respuesta corta

Heapsort es un ordenamiento por comparación que primero organiza el arreglo en un montículo máximo (max-heap), un árbol binario almacenado en el arreglo en el que cada padre es al menos tan grande como sus hijos. Después intercambia repetidamente el mayor elemento (la raíz) con el final del arreglo, reduce el montículo en uno y restablece la propiedad del montículo hundiendo la nueva raíz (sift-down). Se ejecuta en tiempo O(n log n) en todos los casos y ordena in situ con O(1) de espacio extra, pero no es estable y en la práctica suele ser más lento que quicksort.

Abrir Ordenar líneas

La idea en palabras sencillas

Heapsort es selection sort con una forma más rápida de encontrar el máximo. Selection sort recorre toda la parte sin ordenar, O(n) cada vez. Heapsort mantiene la parte sin ordenar como un montículo, de modo que el máximo siempre está en el índice 0 y restablecer el orden tras extraerlo cuesta solo O(log n).

Un montículo binario se almacena en un arreglo simple, sin punteros. Para el elemento del índice i:

  • el hijo izquierdo está en 2*i + 1 y el hijo derecho en 2*i + 2;
  • el padre está en (i - 1) // 2;
  • en un montículo máximo, cada padre es mayor o igual que sus hijos, así que el mayor elemento está en el índice 0.
  1. Construye un montículo máximo a partir del arreglo hundiendo cada nodo que no sea hoja, empezando por el último y avanzando hasta el índice 0.
  2. Intercambia la raíz (el máximo) con el último elemento del montículo. Ese elemento queda ya en su posición final.
  3. Reduce el montículo en uno y hunde la nueva raíz hasta que vuelva a cumplirse la propiedad del montículo.
  4. Repite los pasos 2 y 3 hasta que el montículo tenga un solo elemento.

Ejemplo resuelto, paso a paso

Ordena [4, 10, 3, 5, 1]. Los elementos posteriores a la barra están ordenados y son definitivos.

PasoAcciónArreglo
Inicio[4, 10, 3, 5, 1]
Construcción 1hundir el índice 1 (10): es mayor que sus hijos 5 y 1, sin cambios[4, 10, 3, 5, 1]
Construcción 2hundir el índice 0 (4): se intercambia con 10 y luego con 5[10, 5, 3, 4, 1] montículo máximo
Extracción 110 pasa al final, se hunde 1: se intercambia con 5 y luego con 4[5, 4, 3, 1 | 10]
Extracción 25 pasa al final, se hunde 1: se intercambia con 4[4, 1, 3 | 5, 10]
Extracción 34 pasa al final, se hunde 3: ningún hijo es mayor[3, 1 | 4, 5, 10]
Extracción 43 pasa al final[1 | 3, 4, 5, 10] ordenado

Implementación en Python

def sift_down(a, root, end):
    """Restore the max-heap property for a[root..end-1]."""
    while True:
        child = 2 * root + 1
        if child >= end:
            return
        if child + 1 < end and a[child + 1] > a[child]:
            child += 1                      # pick the larger child
        if a[root] >= a[child]:
            return
        a[root], a[child] = a[child], a[root]
        root = child

def heapsort(a):
    """Sort list a in place, ascending."""
    n = len(a)
    for i in range(n // 2 - 1, -1, -1):     # build the max-heap
        sift_down(a, i, n)
    for end in range(n - 1, 0, -1):
        a[0], a[end] = a[end], a[0]         # move the max to the end
        sift_down(a, 0, end)
    return a

print(heapsort([4, 10, 3, 5, 1]))   # [1, 3, 4, 5, 10]
print(heapsort([2, 2, 1]))          # [1, 2, 2]

El módulo heapq de Python ofrece un montículo mínimo sobre una lista simple. Extraer todos los elementos da una lista ordenada, que es un heapsort corto (no in situ):

import heapq

def heapsort_heapq(items):
    h = list(items)
    heapq.heapify(h)                       # O(n)
    return [heapq.heappop(h) for _ in range(len(h))]

print(heapsort_heapq([4, 10, 3, 5, 1]))     # [1, 3, 4, 5, 10]
print(heapq.nlargest(2, [4, 10, 3, 5, 1]))  # [10, 5]

Complejidad temporal y espacial

CasoTiempoMotivo
Construir el montículoO(n)la mayoría de los nodos están cerca del fondo y solo se hunden una distancia corta
MejorO(n log n)n − 1 extracciones, cada hundimiento de hasta log₂ n niveles (claves distintas)
PromedioO(n log n)el mismo trabajo sea cual sea el orden
PeorO(n log n)la altura del árbol es siempre de unos log₂ n
Espacio extraO(1)el montículo vive dentro del arreglo de entrada y el hundimiento es un bucle

Construir el montículo de abajo arriba es O(n), no O(n log n), porque la suma de las alturas de los nodos de un árbol binario completo es O(n). La fase de extracción domina el total.

Cómo se compara heapsort con otros ordenamientos

AlgoritmoMejorPromedioPeorEspacio extraEstable
HeapsortO(n log n)O(n log n)O(n log n)O(1)No
QuicksortO(n log n)O(n log n)O(n²)O(log n)No
Merge sortO(n log n)O(n log n)O(n log n)O(n)Sí
Selection sortO(n²)O(n²)O(n²)O(1)No
Insertion sortO(n)O(n²)O(n²)O(1)Sí
Timsort (sorted de Python)O(n)O(n log n)O(n log n)O(n)Sí

Heapsort no es estable: intercambiar la raíz con el final puede llevar un elemento más allá de otros con la misma clave. Sus principales fortalezas son un peor caso O(n log n) garantizado junto con O(1) de espacio extra, una combinación que no ofrecen ni quicksort ni merge sort. Su debilidad es el acceso a memoria: saltar entre el índice i y 2*i + 1 aprovecha mal las cachés de la CPU, por lo que suele ser más lento que un quicksort bien implementado. Introsort usa heapsort como respaldo cuando quicksort recursa demasiado profundo.

Variantes y errores comunes

  • Ordenamiento parcial: para obtener solo los k mayores, construye el montículo y extrae k veces, O(n + k log n). En Python, heapq.nlargest y heapq.nsmallest lo hacen.
  • Top-k de un flujo de datos: mantén un montículo mínimo de tamaño k e inserta o reemplaza cada elemento nuevo, O(n log k) de tiempo y O(k) de memoria.
  • Heapsort ascendente (bottom-up) hunde primero el hueco hasta una hoja y luego lo sube, lo que ahorra comparaciones.
  • Smoothsort, una variante basada en montículos de Dijkstra, se ejecuta en O(n) con entradas ya ordenadas, pero se usa poco por su complejidad.

Cuándo usar heapsort

Usa heapsort cuando necesites un peor caso O(n log n) garantizado sin memoria adicional, por ejemplo en sistemas embebidos o de tiempo real, o como respaldo de seguridad dentro de otro ordenamiento. En el código Python de uso diario, usa sorted(), que es estable y más rápido con datos típicos. El propio montículo es más útil que el ordenamiento: colas de prioridad, caminos mínimos de Dijkstra, planificadores, mezcla de k listas ordenadas (heapq.merge) y búsqueda de los k mayores valores se basan en él. Las entrevistas suelen preguntar por las fórmulas de índices del montículo, por qué construirlo es O(n) y por el problema del top-k.

Preguntas frecuentes

¿Es estable heapsort?

No. Mover la raíz al final y hundir elementos puede cambiar el orden relativo de claves iguales.

¿Cuál es la complejidad temporal de heapsort?

O(n log n) en el mejor caso, en el promedio y en el peor. Construir el montículo es O(n) y cada una de las n − 1 extracciones cuesta O(log n).

¿Es heapsort in situ?

Sí. El montículo se almacena dentro del arreglo que se ordena y la parte ordenada crece desde el final, por lo que solo se necesita O(1) de espacio extra.

¿Por qué heapsort es más lento que quicksort en la práctica?

Ambos son O(n log n) en promedio, pero heapsort salta entre posiciones distantes del arreglo, lo que causa más fallos de caché, y hace más comparaciones por elemento que quicksort.

¿Cómo se ordena con heapq en Python?

Llama a heapq.heapify(list) y luego a heapq.heappop repetidamente, o usa heapq.nsmallest(k, items) para obtener los k menores. Para un ordenamiento completo, sorted() es más simple y más rápido.

Todos los tutoriales de programación