Montículos y Colas de Prioridad
Respaldado por arreglo, mínimo en tiempo logarítmico — el motor de las colas de prioridad
Min-heap · array view ↔ tree view
init
Pruébalo: Inserta y observa cómo parpadean los intercambios de pares en la subida (bubble-up). Luego extractMin — el hundimiento (sift-down) restaura el invariante. Mismo arreglo, dos formas de verlo: fila de celdas y árbol binario completo.
Por qué importa
Un montículo es el mismo árbol respaldado por arreglo de la lección de árboles binarios — pero con una regla extra que lo convierte en un motor de mínimo/máximo en tiempo constante. Cuando necesitas el elemento máximo (o mínimo) de forma rápida y repetida, los montículos son la respuesta. Impulsan las colas de prioridad, el ordenamiento por montículo (heap sort) y algoritmos como el camino más corto de Dijkstra (que viste en el modo de camino más corto de la lección de grafos).
El invariante del montículo
Dos invariantes, ambos deben cumplirse:
- Invariantes del montículo
- Forma: el árbol binario es completo — cada nivel está lleno excepto, posiblemente, el último, que se llena de izquierda a derecha. Esto nos permite almacenar el árbol de forma contigua en un arreglo, sin huecos.
- Orden (montículo de mínimos): para cada nodo
n,n.key ≤ key(child)para cada hijo. (Cambia a≥para un montículo de máximos.) Por lo tanto, la raíz contiene el mínimo (o el máximo). - Qué significa "preservar"
- Insertar viola el orden temporalmente en la hoja, y luego la subida (bubble-up) lo restaura. Extraer viola ambos temporalmente (la última hoja se mueve a la raíz), y luego el hundimiento (sift-down) restaura el orden manteniendo la forma completa.
La idea
Imagina un cuadro de torneo donde el ganador siempre asciende a la cima. Después de cada partido, el mejor equipo sube.
Un montículo es así. En un montículo de máximos, cada padre es mayor que sus hijos. La raíz es siempre el máximo.
El truco ingenioso: ¡almacenarlo en un arreglo! Para el nodo en el índice i:
- Hijo izquierdo: 2i + 1
- Hijo derecho: 2i + 2
- Padre: (i - 1) / 2
Insertar: Agregar al final, luego "subir" — intercambiar con el padre mientras sea mayor. Extraer el máximo: Quitar la raíz, poner el último elemento en la raíz, luego "hundir" — intercambiar con el hijo mayor mientras sea menor.
Ambas operaciones son O(log n) — apenas la altura del árbol.
Puntos clave
- La raíz es siempre el máximo (montículo de máximos) o el mínimo (montículo de mínimos)
- Insertar y extraer son O(log n)
- Mirar el máximo/mínimo (peek) es O(1)
- Se almacena en un arreglo usando fórmulas de índice
Profundizando
Montículo de mínimos vs montículo de máximos
Un montículo de mínimos pone el elemento más pequeño en la raíz; uno de máximos pone el más grande. El mecanismo es idéntico — solo se invierte la dirección de la comparación. Algunos lenguajes tienen un valor predeterminado en un sentido (la std::priority_queue de C++ es de máximos; heapq de Python es de mínimos) y niegas los valores para invertirlo — heapq.heappush(q, -value). Verifícalo siempre.
La prueba del heapify en O(n)
Pensarías que construir un montículo a partir de n elementos desordenados cuesta O(n log n) — una inserción O(log n) por elemento. Pero hay una forma mejor: copia el arreglo, luego hunde desde el índice del medio hacia la raíz. El costo total es O(n), no O(n log n).
La prueba es una hermosa suma geométrica. Un árbol binario completo de n nodos tiene aproximadamente n/2 hojas (profundidad 0 — sin necesidad de hundir), n/4 en la profundidad 1 (a lo sumo 1 intercambio cada una), n/8 en la profundidad 2 (a lo sumo 2 intercambios), …, 1 raíz en la profundidad log n (a lo sumo log n intercambios). El trabajo total:
trabajo ≤ Σ (n / 2^(d+1)) · d para d = 0..log n
= (n/2) · Σ d / 2^d
≤ (n/2) · 2 ya que Σ d/2^d converge a 2
= O(n)
La mayoría de los nodos están cerca del fondo y se hunden cero o un nivel. Solo una fracción ínfima cerca de la cima hace el trabajo completo de log n. Este es otro sabor del análisis amortizado — el peor caso se concentra en unos pocos nodos, y son tan pocos que no llegan a dominar.
Montículo vs ABB — cuándo elegir cuál
Ambos te dan operaciones O(log n). Dos diferencias prácticas deciden cuál usar:
| Caso de uso | Montículo | ABB |
|---|---|---|
| Extracción repetida de mín/máx | ✓ peek O(1), pop O(log n) | también O(log n) para mín/máx |
| Consultas por rango (encontrar todos > 50) | ✗ sin orden | ✓ O(log n + k) |
| Recorrido inorden | ✗ | ✓ |
| Sobrecarga de memoria | arreglo — sin punteros | 2 punteros + valor por nodo |
| Factor constante | pequeño (arreglo, amigable con la caché) | mayor (persecución de punteros) |
| Construir a partir de n elementos | heapify O(n) | inserción repetida O(n log n) |
Elige un montículo cuando solo necesitas mín/máx y nunca necesitas orden ni consultas por rango. Elige un ABB (normalmente balanceado) cuando necesitas iteración ordenada o "encontrar todas las claves en [a, b]". El modo de Dijkstra de la lección de grafos usa un montículo porque siempre necesita el nodo de la frontera más barato siguiente, no el orden de todos los nodos de la frontera.
El ordenamiento por montículo usa esto: heapify en O(n), luego extraer el máximo n veces para un O(n log n) total — pero con peor comportamiento de caché que el merge sort, así que rara vez es el ganador práctico a pesar de la asíntota coincidente.
Detalles matemáticos
- Complejidad temporal
Insert: O(log n)Extract max/min: O(log n)Peek: O(1)Heapify (build): O(n)- Fórmulas de índice del arreglo
left(i) = 2i + 1right(i) = 2i + 2parent(i) = lfloor(i-1)/2rfloor
Implementación
Operaciones del montículo
use std::collections::BinaryHeap;
let mut heap = BinaryHeap::new();
heap.push(3);
heap.push(1);
heap.push(4); // Montículo: máximo en la cima
let max = heap.pop(); // Devuelve Some(4)
let peek = heap.peek(); // Devuelve Some(&3)
Cuándo usarlo
- Planificación por prioridad
- Encontrar los k mayores/menores
- Camino más corto de Dijkstra
- Ordenamiento por montículo (heap sort)
- Mantenimiento de la mediana
Práctica
Tres problemas que muestran por qué el montículo es tu herramienta por defecto para "dame las k mejores/peores cosas":
- Los K elementos más frecuentes — medio · montículo de mínimos de tamaño k como una "lista corta rotativa"; el patrón canónico de montículo
- Fusionar K listas ordenadas — difícil · la mezcla k-vías — exactamente lo que ejecutan por dentro el ordenamiento externo, la fusión de consultas distribuidas y la compactación de árboles LSM
- Mediana de un flujo de datos — difícil · el truco de los dos montículos — dos montículos combinados te dan lo que ninguno podría por sí solo