Apuntes DAM
Volver al inicio

Montículo y cola de prioridad

AlgoritmosEstructuras de datosNivel intermedioTambién: heap, montón, cola de prioridad, priority queue, PriorityQueue

Un árbol guardado en un array en el que cada padre es menor (o mayor) que sus hijos: el mínimo está siempre en la raíz y meter o sacar cuesta O(log n). Es la cola de prioridad de PriorityQueue.

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.

Montículo de mínimos

Escribe las operaciones (meter x o sacar) y mira cómo el nuevo sube hasta su sitio y cómo, al sacar el mínimo, el último baja desde la raíz.

meter x (entero) o sacar, separadas por comas
  • fuera de juego

Paso 1

Un montículo de mínimos vacío: cada padre será menor o igual que sus hijos, así que el mínimo estará siempre en la raíz, a[0].

1class MonticuloMin {
2    private int[] a = new int[16];
3    private int n = 0;
4
5    void meter(int x) {
6        a[n] = x;
7        int i = n++;
8        while (i > 0 && a[(i - 1) / 2] > a[i]) {
9            int p = (i - 1) / 2;
10            int t = a[i]; a[i] = a[p]; a[p] = t;
11            i = p;
12        }
13    }
14
15    int sacar() {
16        int min = a[0];
17        a[0] = a[--n];
18        int i = 0;
19        while (true) {
20            int menor = i, izq = 2 * i + 1, der = 2 * i + 2;
21            if (izq < n && a[izq] < a[menor]) menor = izq;
22            if (der < n && a[der] < a[menor]) menor = der;
23            if (menor == i) return min;
24            int t = a[i]; a[i] = a[menor]; a[menor] = t;
25            i = menor;
26        }
27    }
28}

Variables

n
0
mínimo
—
sacados
—

Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.

La idea

Una cola de prioridad es una cola en la que no sale el que llegó primero, sino el más importante: el paciente más grave, la tarea más urgente, el proceso con más prioridad, el camino más corto encontrado hasta ahora. Hacerla con una lista ordenada cuesta O(n) al meter; con una desordenada, O(n) al sacar. El montículo hace las dos cosas en O(log n).

Un montículo de mínimos es un árbol binario casi completo (todos los niveles llenos salvo el último, que se llena de izquierda a derecha) en el que cada padre es menor o igual que sus hijos. No tiene por qué estar ordenado: solo garantiza que, en cualquier camino de la raíz hacia abajo, los valores crecen. Por eso el mínimo está siempre en la raíz.

Al ser casi completo, se guarda en un array sin referencias: la raíz en la posición 0 y los hijos de la posición i en 2i + 1 y 2i + 2. Para meter un elemento se pone al final y se «sube» intercambiándolo con su padre mientras sea menor que él. Para sacar el mínimo se pone el último en la raíz y se «hunde» intercambiándolo con su hijo menor mientras sea mayor que él. Las dos cosas recorren como mucho la altura del árbol, log₂ n.

Un montículo de máximos es lo mismo con la comparación al revés. Con él se ordena un array en O(n log n) (heapsort) y con dos montículos se mantiene la mediana de un flujo de datos.

Cuándo usarlo

  • Sacar una y otra vez el mínimo o el máximo de una colección que cambia: tareas por prioridad, eventos por hora en una simulación.
  • Algoritmos de grafos: Dijkstra y Prim sacan en cada paso el nodo más cercano.
  • Los k mayores (o menores) de muchos datos, o de un flujo que no cabe en memoria: un montículo de tamaño k.
  • Mezclar k listas ordenadas: el montículo guarda el primero de cada una.

Cuándo no

  • Si hay que buscar un elemento cualquiera o recorrerlos en orden: el montículo solo da el mínimo; para el resto, un árbol (TreeSet).
  • Si solo se saca el mínimo una vez: basta con un recorrido O(n).

Paso a paso

  1. El array como árbol. Raíz en a[0]; hijos de i en 2i + 1 y 2i + 2; padre de i en (i − 1) / 2.
  2. Meter (subir). Se pone en a[n] y, mientras su padre sea mayor, se intercambian. O(log n).
  3. Consultar el mínimo. Es a[0], O(1).
  4. Sacar el mínimo (hundir). Se guarda a[0], se pone el último en la raíz y, mientras alguno de sus hijos sea menor, se intercambia con el menor de los dos. O(log n).

El código

Una cola de prioridad de tareas

Un montículo de mínimos por prioridad: siempre sale la tarea más urgente, aunque haya llegado la última.

Java
1import java.util.Arrays;
2
3public class Main {
4    record Tarea(int prioridad, String nombre) { }
5
6    /** Cola de prioridad de tareas: un montículo de mínimos por prioridad (1 = la más urgente). */
7    static class ColaPrioridad {
8        private Tarea[] a = new Tarea[4];
9        private int n = 0;
10
11        void meter(Tarea t) {
12            if (n == a.length) a = Arrays.copyOf(a, 2 * n);
13            a[n] = t;
14            int i = n++;
15            while (i > 0 && a[(i - 1) / 2].prioridad() > a[i].prioridad()) {   // sube mientras su padre sea mayor
16                int p = (i - 1) / 2;
17                Tarea x = a[i]; a[i] = a[p]; a[p] = x;
18                i = p;
19            }
20        }
21
22        Tarea sacar() {
23            Tarea min = a[0];
24            a[0] = a[--n];                                 // el último pasa a la raíz…
25            a[n] = null;
26            int i = 0;
27            while (true) {                                 // …y baja hasta su sitio
28                int menor = i, izq = 2 * i + 1, der = 2 * i + 2;
29                if (izq < n && a[izq].prioridad() < a[menor].prioridad()) menor = izq;
30                if (der < n && a[der].prioridad() < a[menor].prioridad()) menor = der;
31                if (menor == i) return min;
32                Tarea x = a[i]; a[i] = a[menor]; a[menor] = x;
33                i = menor;
34            }
35        }
36
37        boolean vacia() { return n == 0; }
38    }
39
40    public static void main(String[] args) {
41        ColaPrioridad cola = new ColaPrioridad();
42        cola.meter(new Tarea(3, "responder correos"));
43        cola.meter(new Tarea(1, "arreglar el servidor caído"));
44        cola.meter(new Tarea(4, "ordenar el escritorio"));
45        cola.meter(new Tarea(2, "preparar la reunión"));
46        System.out.println("Ahora: " + cola.sacar().nombre());
47        cola.meter(new Tarea(0, "llamar al cliente enfadado"));
48        while (!cola.vacia()) {
49            Tarea t = cola.sacar();
50            System.out.println("Después: " + t.nombre() + " (prioridad " + t.prioridad() + ")");
51        }
52    }
53}

Salida al ejecutarlo (la misma en los 5 lenguajes)

Ahora: arreglar el servidor caído
Después: llamar al cliente enfadado (prioridad 0)
Después: preparar la reunión (prioridad 2)
Después: responder correos (prioridad 3)
Después: ordenar el escritorio (prioridad 4)

Los k mayores de un flujo

Un montículo de mínimos de tamaño k guarda los k mayores vistos; su raíz es el listón que hay que superar para entrar.

Java
1/** Los k mayores de un flujo de datos sin guardarlos todos: un montículo de MÍNIMOS de tamaño k.
2    Su raíz es el menor de los k mayores vistos; un número nuevo solo entra si es mayor que ella. */
3static List<Integer> kMayores(Iterable<Integer> datos, int k) {
4    PriorityQueue<Integer> mejores = new PriorityQueue<>();
5    for (int x : datos) {
6        if (mejores.size() < k) mejores.offer(x);
7        else if (x > mejores.peek()) {
8            mejores.poll();                 // sale el menor de los k
9            mejores.offer(x);
10        }
11    }
12    List<Integer> r = new ArrayList<>(mejores);
13    r.sort(Comparator.reverseOrder());
14    return r;                               // O(n log k) en tiempo y O(k) en memoria
15}

Traza: un montículo de mínimos

OperaciónArrayMínimo (raíz)
meter 5[5]5
meter 3[3, 5]3
meter 8[3, 5, 8]3
meter 1[1, 3, 8, 5]1
meter 9[1, 3, 8, 5, 9]1
meter 2[1, 3, 2, 5, 9, 8]1
sacar → 1[2, 3, 8, 5, 9]2
meter 4[2, 3, 4, 5, 9, 8]2
sacar → 2[3, 5, 4, 8, 9]3

El array no está ordenado (5 y 8 aparecen después de 9 o 2), pero la raíz es siempre el mínimo.

Complejidad

OperaciónMontículoLista ordenadaLista desordenada
MeterO(log n)O(n)O(1)
Consultar el mínimoO(1)O(1)O(n)
Sacar el mínimoO(log n)O(1)O(n)
Construir con n datosO(n)O(n log n)O(n)

Memoria: O(n), en un simple array. Es la estructura equilibrada para meter y sacar mezclados.

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(1)
  • Caso medio: O(log n)
  • Peor caso: O(log n)

Consultar el mínimo es O(1); meter y sacar, O(log n): como mucho recorren la altura del árbol. Las curvas grises son las demás clases, para comparar.

En la práctica

  • PriorityQueue en Java, heapq en Python, PriorityQueue<T, P> en .NET y SplPriorityQueue en PHP.
  • El algoritmo de Dijkstra (rutas de un GPS) usa una cola de prioridad con las distancias provisionales.
  • Los planificadores de procesos por prioridad, los temporizadores de un sistema operativo y las colas de eventos de los motores de juegos y simulaciones.
  • La compresión de Huffman (ZIP, JPEG) construye su árbol sacando una y otra vez los dos símbolos menos frecuentes.

Errores típicos

  • Esperar que el array de un montículo esté ordenado, o recorrer una PriorityQueue con un for-each esperando el orden: solo poll() saca en orden.
  • Usar las fórmulas de los hijos de un array que empieza en 1 (2i y 2i + 1) con uno que empieza en 0.
  • Al hundir, intercambiar con el primer hijo menor que se encuentra en vez de con el menor de los dos: el montículo se rompe.
  • Cambiar la prioridad de un elemento que ya está dentro: el montículo no se entera. Hay que sacarlo y volver a meterlo.
  • Olvidar que PriorityQueue es de mínimos: para máximos hace falta Comparator.reverseOrder().

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. Urgencias

Simula la sala de espera de urgencias. Las órdenes llegan una por línea: llega nombre gravedad (de 1 a 5, 5 es lo más grave) y atender. Se atiende siempre al más grave y, con la misma gravedad, al que llegó antes. El main ya lee las órdenes y usa la cola: completa nuevaCola para que la PriorityQueue tenga ese orden.

  • Órdenes: llega Ana 3, atender.
  • atender escribe Atiende a Luis (gravedad 5) o No hay nadie esperando; al final, Quedan esperando: N.
  • Otra orden: Orden no válida: «…».
☕JavaUrgenciasMedio

Ejemplo

Entrada (lo que se escribe por teclado)
llega Ana 2
llega Luis 5
llega Eva 3
atender
atender
llega Pablo 4
atender
atender
Salida esperada
Atiende a Luis (gravedad 5)
Atiende a Eva (gravedad 3)
Atiende a Pablo (gravedad 4)
Atiende a Ana (gravedad 2)
Quedan esperando: 0
⏳
Test oculto #3
⏳
Test oculto #4
0/4 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    record Paciente(String nombre, int gravedad, int llegada) { }
5
6    /** Primero el más grave (5 es lo más grave); con la misma gravedad, el que llegó antes. */
7    static final Comparator<Paciente> ORDEN = Comparator.comparingInt(Paciente::gravedad).reversed()
8            .thenComparingInt(Paciente::llegada);
9
10    static PriorityQueue<Paciente> nuevaCola() {
11        return new PriorityQueue<>(ORDEN);
12    }
13
14    public static void main(String[] args) {
15        Scanner sc = new Scanner(System.in);
16        PriorityQueue<Paciente> cola = nuevaCola();
17        int llegada = 0;
18        while (sc.hasNextLine()) {
19            String linea = sc.nextLine().trim();
20            if (linea.isEmpty()) continue;
21            String[] p = linea.split("\\s+");
22            if (p.length == 3 && p[0].equals("llega") && p[2].matches("[1-5]")) {
23                cola.offer(new Paciente(p[1], Integer.parseInt(p[2]), llegada++));
24            } else if (linea.equals("atender")) {
25                Paciente x = cola.poll();
26                System.out.println(x == null ? "No hay nadie esperando" : "Atiende a " + x.nombre() + " (gravedad " + x.gravedad() + ")");
27            } else System.out.println("Orden no válida: «" + linea + "»");
28        }
29        System.out.println("Quedan esperando: " + cola.size());
30    }
31}

Un montículo no es estable: con la misma prioridad no garantiza el orden de llegada. Por eso se añade el número de llegada como segundo criterio del comparador.

Cada llegada y cada atención cuestan O(log n), tenga la sala 10 o 10.000 pacientes.

2. La mediana mientras llegan los datos

Llegan números de uno en uno y, después de cada uno, hay que dar la mediana de todos los recibidos, sin ordenarlos cada vez. El truco son dos montículos: uno de máximos con la mitad menor y otro de mínimos con la mitad mayor; la mediana está en sus raíces. Completa meter y mediana.

  • Entrada: números enteros separados por espacios o saltos de línea.
  • Salida, tras cada número: 5 → mediana 5, o con una cantidad par 8 → mediana entre 5 y 8.
  • Errores: Número no válido: «x» (se salta) y No hay números.
☕JavaLa mediana mientras llegan los datosDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
5 15 1 3 8
Salida esperada
5 → mediana 5
15 → mediana entre 5 y 15
1 → mediana 5
3 → mediana entre 3 y 5
8 → mediana 5
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
0/5 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    // la mitad menor (montículo de máximos) y la mitad mayor (montículo de mínimos)
5    static final PriorityQueue<Integer> menores = new PriorityQueue<>(Comparator.reverseOrder());
6    static final PriorityQueue<Integer> mayores = new PriorityQueue<>();
7
8    /** Mete x en su mitad y reequilibra: menores puede tener como mucho uno más que mayores. */
9    static void meter(int x) {
10        if (menores.isEmpty() || x <= menores.peek()) menores.offer(x);
11        else mayores.offer(x);
12        if (menores.size() > mayores.size() + 1) mayores.offer(menores.poll());
13        else if (mayores.size() > menores.size()) menores.offer(mayores.poll());
14    }
15
16    /** La mediana con lo que hay: el del medio o, con un número par, los dos del medio. */
17    static String mediana() {
18        if (menores.size() > mayores.size()) return String.valueOf(menores.peek());
19        return "entre " + menores.peek() + " y " + mayores.peek();
20    }
21
22    public static void main(String[] args) {
23        Scanner sc = new Scanner(System.in);
24        int n = 0;
25        while (sc.hasNext()) {
26            String t = sc.next();
27            if (!t.matches("-?\\d{1,6}")) {
28                System.out.println("Número no válido: «" + t + "»");
29                continue;
30            }
31            meter(Integer.parseInt(t));
32            n++;
33            System.out.println(t + " → mediana " + mediana());
34        }
35        if (n == 0) System.out.println("No hay números");
36    }
37}

Las dos raíces son justo los elementos del centro: el mayor de la mitad menor y el menor de la mitad mayor. No hace falta saber nada del resto.

Cada número cuesta O(log n); reordenar todo en cada paso costaría O(n log n) por número.

Test

Test: Montículo y cola de prioridad

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 de mínimos, ¿dónde está el mínimo?

  2. 2.¿Cuánto cuesta meter un elemento en un montículo de n elementos?

  3. 3.¿Qué devuelve recorrer una PriorityQueue de Java con un for-each?

  4. 4.¿Cómo se crea en Java una cola de prioridad que saque primero el mayor?

  5. 5.¿Qué algoritmo de grafos necesita una cola de prioridad?

Relacionado