Apuntes DAM
Volver al inicio

Heapsort (ordenación por montículo)

AlgoritmosOrdenaciónNivel avanzadoTambién: heap sort, ordenación por montículo, ordenación por montón

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.

De 2 a 12 enteros separados por espacios
  • 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 sort de la biblioteca) suele ser más rápido.

Paso a paso

  1. El array como árbol. La posición 0 es la raíz; los hijos de i son 2i + 1 y 2i + 2. Las posiciones de n/2 en adelante son hojas.
  2. 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.
  3. Construir el montículo. Se hunden los padres desde n/2 − 1 hasta 0. Al acabar, el mayor está en a[0]. Esta fase cuesta O(n).
  4. 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.

Java
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}

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.

Java
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áximos

Traza: heapsort de {4, 10, 3, 5, 1, 8, 7}

FasePasoArray (montículo | ordenado)
Inicio—[4, 10, 3, 5, 1, 8, 7]
Construirhundir a[2] = 3[4, 10, 8, 5, 1, 3, 7]
Construirhundir a[1] = 10[4, 10, 8, 5, 1, 3, 7]
Construirhundir a[0] = 4[10, 5, 8, 4, 1, 3, 7]
Extraerel 10 a la posición 6[8, 5, 7, 4, 1, 3] | [10]
Extraerel 8 a la posición 5[7, 5, 3, 4, 1] | [8, 10]
Extraerel 7 a la posición 4[5, 4, 3, 1] | [7, 8, 10]
Extraerel 5 a la posición 3[4, 1, 3] | [5, 7, 8, 10]
Extraerel 4 a la posición 2[3, 1] | [4, 5, 7, 8, 10]
Extraerel 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

FaseCoste
Construir el montículoO(n)
Cada extracción (hundir desde la raíz)O(log n)
n − 1 extraccionesO(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.

0102030405015101520tamaño de la entrada (n)operacionesO(n!)O(2ⁿ)O(n²)O(n log n)O(1)O(log n)O(n)
  • 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::sort de C++, List.Sort y Array.Sort de .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: PriorityQueue en Java, heapq en 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 2i y 2i + 1: es la fórmula para arrays que empiezan en 1. Con índice 0 son 2i + 1 y 2i + 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) y Montículo de mínimos: no (a[0] = 9 es mayor que su hijo a[1] = 5), o …: sí.
  • Errores: No hay números y Número no válido: «x».
☕Java¿Es un montículo?Medio

Ejemplo

Entrada (lo que se escribe por teclado)
9 5 8 1 6
Salida esperada
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)
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
⏳
Test oculto #6
0/6 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
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: … y De mayor a menor: ….
  • Errores: No hay números y Número no válido: «x».
☕JavaHeapsort de mayor a menorDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
4 10 3 5 1 8 7
Salida esperada
Montículo de mínimos: 1 4 3 5 10 8 7
De mayor a menor: 10 8 7 5 4 3 1
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
⏳
Test oculto #6
0/6 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
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 aciertos

Elige 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. 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. 2.¿Cuánto cuesta construir un montículo de n elementos de abajo arriba?

  3. 3.¿Qué ventaja tiene heapsort sobre quicksort?

  4. 4.¿Qué ventaja tiene heapsort sobre mergesort?

  5. 5.Tras construir un montículo de máximos, ¿qué se puede asegurar?

Relacionado