Heapsort (ordenación por montículo)
Convierte el array en un montículo de máximos, un árbol guardado en el propio array con el mayor siempre en la raíz, y saca el mayor una y otra vez al final. O(n log n) siempre y sin memoria extra.
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Heapsort
Escribe los números y mira cómo se construye un montículo de máximos y cómo se va sacando el mayor al final del array.
- dentro del montículo
Paso 1
El array visto como un árbol casi completo: los hijos de la posición i están en 2i + 1 y 2i + 2. Todavía no es un montículo: hay padres menores que sus hijos.
1static void heapSort(int[] a) {
2 int n = a.length;
3 for (int i = n / 2 - 1; i >= 0; i--)
4 hundir(a, n, i);
5 for (int fin = n - 1; fin > 0; fin--) {
6 int t = a[0]; a[0] = a[fin]; a[fin] = t;
7 hundir(a, fin, 0);
8 }
9}
10
11static void hundir(int[] a, int n, int i) {
12 while (true) {
13 int mayor = i, izq = 2 * i + 1, der = 2 * i + 2;
14 if (izq < n && a[izq] > a[mayor]) mayor = izq;
15 if (der < n && a[der] > a[mayor]) mayor = der;
16 if (mayor == i) return;
17 int t = a[i]; a[i] = a[mayor]; a[mayor] = t;
18 i = mayor;
19 }
20}Variables
- montículo
- a[0..6]
- comparaciones
- 0
- intercambios
- 0
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Heapsort es la ordenación por selección con un truco: en vez de recorrer toda la parte sin ordenar para encontrar el mayor (O(n) cada vez), la organiza como un montículo, una estructura donde el mayor está siempre arriba y, después de sacarlo, se recoloca en O(log n).
Un montículo de máximos es un árbol binario casi completo en el que cada padre es mayor o igual que sus hijos. No hace falta crear nodos: se guarda en el propio array, nivel a nivel. Los hijos de la posición i están en 2i + 1 y 2i + 2, y su padre en (i − 1) / 2.
El algoritmo tiene dos fases. Primero se construye el montículo: se recorren los padres del último al primero y se «hunde» cada uno (se intercambia con su hijo mayor mientras sea menor que él). Después, n − 1 veces: el mayor (la raíz, a[0]) se intercambia con el último del montículo, que ya queda en su sitio definitivo, el montículo se encoge una posición y se hunde la nueva raíz para recuperar la propiedad.
Así se consigue lo mejor de cada casa: O(n log n) en todos los casos como mergesort, y sin memoria extra como quicksort. A cambio, no es estable y en la práctica es algo más lento que quicksort, porque salta mucho por el array y aprovecha peor la caché.
Cuándo usarlo
- Cuando hace falta O(n log n) garantizado y sin memoria extra (sistemas embebidos, núcleos de sistemas operativos).
- Como red de seguridad de quicksort: introsort cambia a heapsort si la recursividad se hace demasiado profunda.
- Cuando solo hacen falta los k mayores: se construye el montículo (O(n)) y se sacan k (O(k log n)).
Cuándo no
- Si hace falta estabilidad.
- Para el caso típico de ordenar en memoria: quicksort (o el
sortde la biblioteca) suele ser más rápido.
Paso a paso
- El array como árbol. La posición 0 es la raíz; los hijos de
ison2i + 1y2i + 2. Las posiciones den/2en adelante son hojas. - Hundir. Para colocar
a[i], se compara con sus hijos; si alguno es mayor, se intercambia con el mayor de los dos y se sigue desde esa posición, hasta que no tenga hijos mayores. - Construir el montículo. Se hunden los padres desde
n/2 − 1hasta 0. Al acabar, el mayor está ena[0]. Esta fase cuesta O(n). - Extraer el mayor. Se intercambia
a[0]con el último del montículo, el montículo pasa a tener un elemento menos y se hunde la nueva raíz. Se repite hasta que quede uno.
El código
Heapsort mostrando el montículo
Tras construir el montículo, cada línea muestra el montículo que queda y, tras la barra, la parte ya ordenada.
1import java.util.Arrays;
2
3public class Main {
4 static void heapsort(int[] a) {
5 int n = a.length;
6 for (int i = n / 2 - 1; i >= 0; i--) hundir(a, n, i); // 1. construir el montículo de máximos
7 System.out.println("Montículo: " + Arrays.toString(a));
8 for (int fin = n - 1; fin > 0; fin--) { // 2. sacar el mayor una y otra vez
9 int t = a[0]; a[0] = a[fin]; a[fin] = t; // el mayor va a su sitio definitivo
10 hundir(a, fin, 0); // y se recompone el montículo con lo que queda
11 System.out.println("Sale el " + a[fin] + ": " + Arrays.toString(Arrays.copyOfRange(a, 0, fin)) + " | " + Arrays.toString(Arrays.copyOfRange(a, fin, n)));
12 }
13 }
14
15 /** Baja a[i] por el montículo a[0..n) hasta que sea mayor o igual que sus dos hijos. */
16 static void hundir(int[] a, int n, int i) {
17 while (true) {
18 int mayor = i, izq = 2 * i + 1, der = 2 * i + 2;
19 if (izq < n && a[izq] > a[mayor]) mayor = izq;
20 if (der < n && a[der] > a[mayor]) mayor = der;
21 if (mayor == i) return; // ya es mayor que sus hijos
22 int t = a[i]; a[i] = a[mayor]; a[mayor] = t;
23 i = mayor;
24 }
25 }
26
27 public static void main(String[] args) {
28 int[] a = {4, 10, 3, 5, 1, 8, 7};
29 heapsort(a);
30 System.out.println("Resultado: " + Arrays.toString(a));
31 }
32}def heapsort(a):
n = len(a)
for i in range(n // 2 - 1, -1, -1): # 1. construir el montículo de máximos
hundir(a, n, i)
print("Montículo:", a)
for fin in range(n - 1, 0, -1): # 2. sacar el mayor una y otra vez
a[0], a[fin] = a[fin], a[0] # el mayor va a su sitio definitivo
hundir(a, fin, 0) # y se recompone el montículo con lo que queda
print(f"Sale el {a[fin]}: {a[:fin]} | {a[fin:]}")
def hundir(a, n, i):
"""Baja a[i] por el montículo a[0:n] hasta que sea mayor o igual que sus dos hijos."""
while True:
mayor, izq, der = i, 2 * i + 1, 2 * i + 2
if izq < n and a[izq] > a[mayor]:
mayor = izq
if der < n and a[der] > a[mayor]:
mayor = der
if mayor == i: # ya es mayor que sus hijos
return
a[i], a[mayor] = a[mayor], a[i]
i = mayor
a = [4, 10, 3, 5, 1, 8, 7]
heapsort(a)
print("Resultado:", a)const texto = (a) => "[" + a.join(", ") + "]";
function heapsort(a) {
const n = a.length;
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) hundir(a, n, i); // 1. construir el montículo de máximos
console.log("Montículo: " + texto(a));
for (let fin = n - 1; fin > 0; fin--) { // 2. sacar el mayor una y otra vez
[a[0], a[fin]] = [a[fin], a[0]]; // el mayor va a su sitio definitivo
hundir(a, fin, 0); // y se recompone el montículo
console.log(`Sale el ${a[fin]}: ${texto(a.slice(0, fin))} | ${texto(a.slice(fin))}`);
}
}
/** Baja a[i] por el montículo a[0..n) hasta que sea mayor o igual que sus dos hijos. */
function hundir(a, n, i) {
while (true) {
let mayor = i;
const izq = 2 * i + 1, der = 2 * i + 2;
if (izq < n && a[izq] > a[mayor]) mayor = izq;
if (der < n && a[der] > a[mayor]) mayor = der;
if (mayor === i) return; // ya es mayor que sus hijos
[a[i], a[mayor]] = [a[mayor], a[i]];
i = mayor;
}
}
const a = [4, 10, 3, 5, 1, 8, 7];
heapsort(a);
console.log("Resultado: " + texto(a));using System;
class Program {
static string Texto(int[] a) => "[" + string.Join(", ", a) + "]";
static void Heapsort(int[] a) {
int n = a.Length;
for (int i = n / 2 - 1; i >= 0; i--) Hundir(a, n, i); // 1. construir el montículo de máximos
Console.WriteLine("Montículo: " + Texto(a));
for (int fin = n - 1; fin > 0; fin--) { // 2. sacar el mayor una y otra vez
(a[0], a[fin]) = (a[fin], a[0]); // el mayor va a su sitio definitivo
Hundir(a, fin, 0); // y se recompone el montículo
Console.WriteLine(quot;Sale el {a[fin]}: {Texto(a[..fin])} | {Texto(a[fin..])}");
}
}
// Baja a[i] por el montículo a[0..n) hasta que sea mayor o igual que sus dos hijos.
static void Hundir(int[] a, int n, int i) {
while (true) {
int mayor = i, izq = 2 * i + 1, der = 2 * i + 2;
if (izq < n && a[izq] > a[mayor]) mayor = izq;
if (der < n && a[der] > a[mayor]) mayor = der;
if (mayor == i) return; // ya es mayor que sus hijos
(a[i], a[mayor]) = (a[mayor], a[i]);
i = mayor;
}
}
static void Main() {
int[] a = { 4, 10, 3, 5, 1, 8, 7 };
Heapsort(a);
Console.WriteLine("Resultado: " + Texto(a));
}
}<?php
function texto(array $a): string { return "[" . implode(", ", $a) . "]"; }
function heapsort(array &$a): void {
$n = count($a);
for ($i = intdiv($n, 2) - 1; $i >= 0; $i--) hundir($a, $n, $i); // 1. construir el montículo de máximos
echo "Montículo: " . texto($a) . "\n";
for ($fin = $n - 1; $fin > 0; $fin--) { // 2. sacar el mayor una y otra vez
[$a[0], $a[$fin]] = [$a[$fin], $a[0]]; // el mayor va a su sitio definitivo
hundir($a, $fin, 0); // y se recompone el montículo
echo "Sale el {$a[$fin]}: " . texto(array_slice($a, 0, $fin)) . " | " . texto(array_slice($a, $fin)) . "\n";
}
}
/** Baja $a[$i] por el montículo $a[0..$n) hasta que sea mayor o igual que sus dos hijos. */
function hundir(array &$a, int $n, int $i): void {
while (true) {
$mayor = $i;
$izq = 2 * $i + 1;
$der = 2 * $i + 2;
if ($izq < $n && $a[$izq] > $a[$mayor]) $mayor = $izq;
if ($der < $n && $a[$der] > $a[$mayor]) $mayor = $der;
if ($mayor === $i) return; // ya es mayor que sus hijos
[$a[$i], $a[$mayor]] = [$a[$mayor], $a[$i]];
$i = $mayor;
}
}
$a = [4, 10, 3, 5, 1, 8, 7];
heapsort($a);
echo "Resultado: " . texto($a) . "\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
Montículo: [10, 5, 8, 4, 1, 3, 7] Sale el 10: [8, 5, 7, 4, 1, 3] | [10] Sale el 8: [7, 5, 3, 4, 1] | [8, 10] Sale el 7: [5, 4, 3, 1] | [7, 8, 10] Sale el 5: [4, 1, 3] | [5, 7, 8, 10] Sale el 4: [3, 1] | [4, 5, 7, 8, 10] Sale el 3: [1] | [3, 4, 5, 7, 8, 10] Resultado: [1, 3, 4, 5, 7, 8, 10]
El montículo de la biblioteca
Casi nunca hace falta programar el montículo a mano: las colas de prioridad de la biblioteca lo son.
1// La biblioteca ya trae un montículo: PriorityQueue (de mínimos, salvo que se le pase un Comparator)
2PriorityQueue<Integer> cola = new PriorityQueue<>(List.of(4, 10, 3, 5, 1));
3while (!cola.isEmpty()) System.out.print(cola.poll() + " "); // 1 3 4 5 10: cada poll es O(log n)
4
5PriorityQueue<Integer> maximos = new PriorityQueue<>(Comparator.reverseOrder()); // de máximosimport heapq
# heapq convierte una lista normal en un montículo de mínimos
cola = [4, 10, 3, 5, 1]
heapq.heapify(cola) # O(n)
while cola:
print(heapq.heappop(cola), end=" ") # 1 3 4 5 10: cada heappop es O(log n)
# Para un montículo de máximos se guardan los valores cambiados de signo// JavaScript no trae cola de prioridad en su biblioteca estándar: o se programa el
// montículo a mano (como en el ejemplo) o se usa una librería.
class MonticuloMin {
#a = [];
meter(x) { // sube el nuevo hasta su sitio: O(log n)
const a = this.#a;
a.push(x);
for (let i = a.length - 1, p; i > 0 && a[p = (i - 1) >> 1] > a[i]; i = p) [a[i], a[p]] = [a[p], a[i]];
}
get tamaño() { return this.#a.length; }
}// .NET 6 trae PriorityQueue<TElemento, TPrioridad>, un montículo de mínimos por prioridad
var cola = new PriorityQueue<string, int>();
cola.Enqueue("baja", 5);
cola.Enqueue("urgente", 1);
cola.Enqueue("normal", 3);
while (cola.Count > 0) Console.Write(cola.Dequeue() + " "); // urgente normal baja
// De máximos: un IComparer al revés, Comparer<int>.Create((x, y) => y.CompareTo(x))// La SPL trae montículos ya hechos: SplMinHeap, SplMaxHeap y SplPriorityQueue $cola = new SplMinHeap(); foreach ([4, 10, 3, 5, 1] as $x) $cola->insert($x); while (!$cola->isEmpty()) echo $cola->extract(), " "; // 1 3 4 5 10: cada extract es O(log n) $maximos = new SplMaxHeap(); // de máximos
Traza: heapsort de {4, 10, 3, 5, 1, 8, 7}
| Fase | Paso | Array (montículo | ordenado) |
|---|---|---|
| Inicio | — | [4, 10, 3, 5, 1, 8, 7] |
| Construir | hundir a[2] = 3 | [4, 10, 8, 5, 1, 3, 7] |
| Construir | hundir a[1] = 10 | [4, 10, 8, 5, 1, 3, 7] |
| Construir | hundir a[0] = 4 | [10, 5, 8, 4, 1, 3, 7] |
| Extraer | el 10 a la posición 6 | [8, 5, 7, 4, 1, 3] | [10] |
| Extraer | el 8 a la posición 5 | [7, 5, 3, 4, 1] | [8, 10] |
| Extraer | el 7 a la posición 4 | [5, 4, 3, 1] | [7, 8, 10] |
| Extraer | el 5 a la posición 3 | [4, 1, 3] | [5, 7, 8, 10] |
| Extraer | el 4 a la posición 2 | [3, 1] | [4, 5, 7, 8, 10] |
| Extraer | el 3 a la posición 1 | [1] | [3, 4, 5, 7, 8, 10] |
Tras la fase de construcción el 10 está en la raíz. Cada extracción lo manda al final y la parte ordenada crece por la derecha.
Complejidad
| Fase | Coste |
|---|---|
| Construir el montículo | O(n) |
| Cada extracción (hundir desde la raíz) | O(log n) |
| n − 1 extracciones | O(n log n) |
| Total (mejor, medio y peor caso) | O(n log n) |
Memoria extra: O(1). No es estable. Que construir el montículo sea O(n) y no O(n log n) se debe a que la mayoría de los nodos están cerca de las hojas y se hunden muy poco.
- Mejor caso: O(n log n)
- Caso medio: O(n log n)
- Peor caso: O(n log n)
n log n siempre y sin memoria extra, pero en la práctica es más lento que quicksort por cómo usa la caché. Las curvas grises son las demás clases, para comparar.
En la práctica
- Introsort (
std::sortde C++,List.SortyArray.Sortde .NET) usa heapsort cuando quicksort empieza a degenerar. - El núcleo de Linux usa heapsort en su función
sort()porque no necesita memoria extra ni tiene peor caso. - El montículo en sí es la cola de prioridad:
PriorityQueueen Java,heapqen Python; se usa en Dijkstra, en planificadores de tareas y para quedarse con los k mejores de un flujo de datos.
Errores típicos
- Calcular los hijos como
2iy2i + 1: es la fórmula para arrays que empiezan en 1. Con índice 0 son2i + 1y2i + 2. - Hundir comparando solo con un hijo: hay que intercambiar con el mayor de los dos, o el montículo se rompe.
- Construir el montículo hundiendo desde 0 hasta
n/2 − 1: hay que ir del último padre al primero, porque hundir un nodo da por hecho que sus subárboles ya son montículos. - No reducir el tamaño del montículo al extraer: el elemento recién colocado al final vuelve a entrar en el montículo y se desordena.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. ¿Es un montículo?
Lee una línea de enteros y di si es un montículo de máximos y si es un montículo de mínimos, viendo el array como árbol (los hijos de i son 2i + 1 y 2i + 2). Si no lo es, di el primer padre que falla: se miran los padres desde el 0 y, en cada uno, primero el hijo izquierdo y luego el derecho. Completa primeraFalla, que devuelve la posición del hijo que rompe la propiedad, o −1.
- Entrada: una línea de enteros, por ejemplo
9 5 8 1 6. - Salida, dos líneas:
Montículo de máximos: no (a[1] = 5 es menor que su hijo a[4] = 6)yMontículo de mínimos: no (a[0] = 9 es mayor que su hijo a[1] = 5), o…: sí. - Errores:
No hay númerosyNúmero no válido: «x».
Ejemplo
9 5 8 1 6
Montículo de máximos: no (a[1] = 5 es menor que su hijo a[4] = 6) Montículo de mínimos: no (a[0] = 9 es mayor que su hijo a[1] = 5)
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 /** Primera posición de un hijo que rompe el montículo (de máximos si maximos es true, de mínimos
5 si no): se recorren los padres desde el 0 y, en cada uno, primero el hijo izquierdo y luego
6 el derecho. Devuelve -1 si es un montículo. */
7 static int primeraFalla(int[] a, boolean maximos) {
8 for (int i = 0; i < a.length; i++) {
9 for (int h = 2 * i + 1; h <= 2 * i + 2 && h < a.length; h++) {
10 if (maximos ? a[h] > a[i] : a[h] < a[i]) return h;
11 }
12 }
13 return -1;
14 }
15
16 /** Lee una línea de enteros; si falla, escribe el error y devuelve null. */
17 static int[] leer(Scanner sc) {
18 String linea = sc.hasNextLine() ? sc.nextLine().trim() : "";
19 if (linea.isEmpty()) {
20 System.out.println("No hay números");
21 return null;
22 }
23 String[] partes = linea.split("\\s+");
24 int[] a = new int[partes.length];
25 for (int i = 0; i < partes.length; i++) {
26 try {
27 a[i] = Integer.parseInt(partes[i]);
28 } catch (NumberFormatException e) {
29 System.out.println("Número no válido: «" + partes[i] + "»");
30 return null;
31 }
32 }
33 return a;
34 }
35
36 static String texto(int[] a) {
37 StringJoiner sj = new StringJoiner(" ");
38 for (int x : a) sj.add(String.valueOf(x));
39 return sj.toString();
40 }
41
42 static void informe(int[] a, boolean maximos) {
43 int h = primeraFalla(a, maximos);
44 String tipo = maximos ? "máximos" : "mínimos";
45 if (h < 0) {
46 System.out.println("Montículo de " + tipo + ": sí");
47 return;
48 }
49 int p = (h - 1) / 2;
50 System.out.println("Montículo de " + tipo + ": no (a[" + p + "] = " + a[p] + " es " + (maximos ? "menor" : "mayor") + " que su hijo a[" + h + "] = " + a[h] + ")");
51 }
52
53 public static void main(String[] args) {
54 Scanner sc = new Scanner(System.in);
55 int[] a = leer(sc);
56 if (a == null) return;
57 informe(a, true);
58 informe(a, false);
59 }
60}La propiedad del montículo es local: basta comprobar cada pareja padre-hijo, y hay n − 1 parejas. Es O(n).
Un array ordenado de menor a mayor es siempre un montículo de mínimos (cada padre va antes que sus hijos), pero un montículo no tiene por qué estar ordenado: solo garantiza el orden en cada camino de la raíz a una hoja.
2. Heapsort de mayor a menor
Ordena una línea de enteros de mayor a menor con heapsort usando un montículo de MÍNIMOS: el menor está en la raíz y, al sacarlo al final del array, la parte final queda de mayor a menor. Muestra el montículo recién construido y el resultado. Completa hundirMin (si los dos hijos son iguales, se baja por el izquierdo), construir y ordenarDescendente.
- Entrada: una línea de enteros.
- Salida:
Montículo de mínimos: …yDe mayor a menor: …. - Errores:
No hay númerosyNúmero no válido: «x».
Ejemplo
4 10 3 5 1 8 7
Montículo de mínimos: 1 4 3 5 10 8 7 De mayor a menor: 10 8 7 5 4 3 1
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 /** Baja a[i] por el montículo de MÍNIMOS a[0..n): si sus dos hijos son iguales, baja por el izquierdo. */
5 static void hundirMin(int[] a, int n, int i) {
6 while (true) {
7 int menor = i, izq = 2 * i + 1, der = 2 * i + 2;
8 if (izq < n && a[izq] < a[menor]) menor = izq;
9 if (der < n && a[der] < a[menor]) menor = der;
10 if (menor == i) return;
11 int t = a[i]; a[i] = a[menor]; a[menor] = t;
12 i = menor;
13 }
14 }
15
16 /** Construye el montículo de mínimos de abajo arriba. */
17 static void construir(int[] a) {
18 for (int i = a.length / 2 - 1; i >= 0; i--) hundirMin(a, a.length, i);
19 }
20
21 /** Heapsort con el montículo de mínimos: cada mínimo va al final, así que queda de mayor a menor. */
22 static void ordenarDescendente(int[] a) {
23 construir(a);
24 for (int fin = a.length - 1; fin > 0; fin--) {
25 int t = a[0]; a[0] = a[fin]; a[fin] = t;
26 hundirMin(a, fin, 0);
27 }
28 }
29
30 /** Lee una línea de enteros; si falla, escribe el error y devuelve null. */
31 static int[] leer(Scanner sc) {
32 String linea = sc.hasNextLine() ? sc.nextLine().trim() : "";
33 if (linea.isEmpty()) {
34 System.out.println("No hay números");
35 return null;
36 }
37 String[] partes = linea.split("\\s+");
38 int[] a = new int[partes.length];
39 for (int i = 0; i < partes.length; i++) {
40 try {
41 a[i] = Integer.parseInt(partes[i]);
42 } catch (NumberFormatException e) {
43 System.out.println("Número no válido: «" + partes[i] + "»");
44 return null;
45 }
46 }
47 return a;
48 }
49
50 static String texto(int[] a) {
51 StringJoiner sj = new StringJoiner(" ");
52 for (int x : a) sj.add(String.valueOf(x));
53 return sj.toString();
54 }
55
56 public static void main(String[] args) {
57 Scanner sc = new Scanner(System.in);
58 int[] a = leer(sc);
59 if (a == null) return;
60 int[] m = a.clone();
61 construir(m);
62 System.out.println("Montículo de mínimos: " + texto(m));
63 ordenarDescendente(a);
64 System.out.println("De mayor a menor: " + texto(a));
65 }
66}Cambiar el sentido de la comparación cambia el tipo de montículo y, con él, el orden del resultado: el algoritmo es el mismo.
La regla de bajar por el izquierdo en caso de empate no cambia el resultado ordenado, pero sí la forma del montículo intermedio: por eso un algoritmo tiene que estar bien especificado para que dos implementaciones den lo mismo.
Test
Test: Heapsort (ordenación por montículo)
0/5 respondidas · 0 aciertosElige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.
1.En un montículo guardado en un array desde la posición 0, ¿dónde están los hijos de la posición 3?
2.¿Cuánto cuesta construir un montículo de n elementos de abajo arriba?
3.¿Qué ventaja tiene heapsort sobre quicksort?
4.¿Qué ventaja tiene heapsort sobre mergesort?
5.Tras construir un montículo de máximos, ¿qué se puede asegurar?