Está en la página 1de 11

Estructuras de Datos y Algoritmos II curso 2019-2020

Conferencia # 02: “TDA Cola de Prioridad”


Sumario:
El TDA Cola de Prioridad: Modelo
Operaciones
Implementación.
o Con estructuras de datos conocidas: Listas, ABB y AVL.
o Con Montículo Binario.

Bibliografía del Tema TDA Cola de Prioridad:


- Mark Allen Weis, Estructuras de Datos en Java, Volumen I, epígrafe 6.8, pág 155; Volumen III,
capítulo 20, epígrafes 20.1 al 20.3, página 523.
- THOMAS H. CORMEN, CHARLES E. LEISERSON, RONALD L. RIVEST and CLIFFORD
STEIN, Introduction to Algorithms, epigraph 6.5, page 162.

INTRODUCCIÓN
Recordar el TDA Cola, cuyo modelo es:
quitarPrimero
COLA
insertar
primero

Sólo es accesible el primer elemento que entró

DESARROLLO
En muchos casos el TDA Cola no resuelve algunos problemas de espera, pues aquí siempre la técnica
de acceso es FIFO; pero, en ocasiones, interesa que se atiendan no en el orden en que llegan sino en
base a cierta prioridad que tengan.

Modelo
Una cola de prioridad es una estructura de datos que mantiene un conjunto de elementos, cada uno de
los cuales es asociado con un valor, llamado llave o prioridad.
Así, una cola de prioridad puede verse como una cola a cuyos elementos se le ha asignado una
prioridad, de forma que el orden en que los elementos son procesados sigue la siguiente regla: el
elemento con mayor prioridad (o de menor prioridad) es procesado primero.

En estos casos, para el procesamiento lo que nos interesa es la prioridad que estamos considerando; por
lo tanto, al almacenar los datos éste será el valor que tendremos en cuenta.
Consideraremos la prioridad como un número entero; de tal manera que, mientras menor sea el número
la prioridad será más alta.
eliminarMin,
insertar buscarMin

Cola de
Prioridad
Sólo es accesible el elemento mínimo
2
Este tipo de cola de prioridad se conoce como cola de prioridad mínima (min-priority queue), pero
también existe la cola de prioridad máxima (max-priority queue).

Estas colas de prioridad pueden ser utilizadas en los hospitales para atender pacientes, los que deben
ser atendidos según el grado de gravedad que tengan. En computación se utilizan mucho en sistemas
operativos multiprocesamiento, pues se le da una prioridad a cada proceso que se ejecuta y en
dependencia de ésta se asigna el procesador.

Operaciones
Las operaciones básicas sobre una Cola de Prioridad Mínima se relacionan a continuación, según las
características de una interface para el lenguaje Java:

A continuación, se presenta la Interfaz de las colas de prioridad con sus operaciones básicas:

//Interfaz ColaPrioridad
public interface ColaPrioridad <E>
{
void insertar( E x );
void introducir(E x);
E buscarMin( ) throws ColaVacia;
E eliminarMin( ) throws ColaVacia;
void vaciar( );
boolean esVacia( );
}

Implementaciones
Hay varias formas de implementar una cola de prioridad:
 Lista enlazada. Pueden seguirse dos estrategias:
- Se hacen las inserciones al inicio (O(1)) y se recorre la lista para eliminar el mínimo (O(N)).
- Se inserta de tal manera que la lista quede ordenada (O(N)) y se elimina siempre el primero
(O(1)).

 Árbol binario de búsqueda (ABB, AVL).


- Las operaciones de inserción y eliminación requieren como promedio O(log N).
- Como siempre se elimina el menor, puede pasar que en el árbol ocurra un desbalance de la rama
izquierda, por lo que debe utilizarse un árbol balanceado.

Si se usa uno de estos dos casos, se debe implementar de manera de no permitir operaciones que no
sean necesarias en una cola de prioridad.

 Montículos binarios, o simplemente Montículos.


Definición. Un montículo es un árbol binario que tiene dos propiedades:

2
3
1. Propiedad estructural. Un montículo es un árbol binario completo; es decir, está totalmente
lleno con la excepción del nivel inferior, que debe llenarse de izquierda a derecha.
Ejemplo 1:
A

B C

D E F G

H I J

Si el nodo J fuera hijo derecho de E, el árbol ya no estaría completo, pues faltaría un nodo.
Por tratarse de un árbol binario completo, debemos tener en cuenta que:
a) Su altura es a lo sumo log2 N. Esto significa que podemos esperar un comportamiento
logarítmico en el caso peor, si restringimos los cambios en la estructura a los caminos de la
raíz a una hoja.
b) No se necesitan referencias a los hijos izquierdo y derecho, ya que se puede representar sin
ambigüedad almacenando su recorrido por niveles en un vector. Colocaremos la raíz en la
posición 1 para reservar la posición 0 para cierta información que veremos después.
En el ejemplo 1, el montículo puede almacenarse en un vector, de la forma siguiente:

0 1 2 3 4 5 6 7 8 9 10 11 12
A B C D E F G H I J
Observe que, si el elemento en la posición i del arreglo tiene hijo izquierdo, éste está en la
posición 2i y el derecho en la 2i+1 y el padre en  i/2  . Por ejemplo, el elemento C ocupa la
posición 3, su hijo izquierdo F ocupa la 6, su hijo derecho G ocupa la 7 y su padre A ocupa la
posición 1.
De esta forma, si la raíz tuviera padre estaría en la posición 0. Precisamente, se reserva esta
posición para colocar un elemento falso que sirva como padre de la raíz, lo que simplificará
algunas operaciones. A este elemento se le llama centinela.

2. Propiedad de ordenación. En un montículo, para cada nodo X con padre P, la llave en P es


menor o igual que la llave en X.
De esta forma, la menor llave de cada subárbol queda en la raíz. A estos montículos se les llama
montículos mínimos. Podemos colocar un valor muy pequeño en la posición 0 para eliminar este
caso especial cuando implementamos los montículos.
Ejemplo 2:

3
4
13 13

21 16 21 16

24 31 19 68 6 31 19 68

65 26 32 65 26 32

Montículo No es montículo

Podemos también establecer la propiedad del orden de manera que cada elemento del montículo sea
mayor o igual que sus dos hijos; en este caso estaríamos en presencia de un montículo máximo y la
operación de eliminar el elemento que está en la raíz del montículo sería eliminarMax. En esta clase
nos referiremos siempre a un montículo mínimo.

Inserción:
Para insertar un elemento x creamos un “hueco” en la siguiente posición disponible, ya que de otra
forma el árbol no será completo. Si lo podemos insertar en el hueco sin violar la propiedad de orden,
todo está resuelto. En otro caso, se desliza el elemento que está en el lugar del nodo padre del hueco,
subiendo así el hueco hacia la raíz. Seguimos este proceso hasta que se pueda colocar x en el hueco.

A continuación, se muestra la inserción del 14, en el montículo del ejemplo 2.

0 1 2 3 4 5 6 7 8 9 10 11
13 21 16 24 31 19 68 65 26 32
13

21 16 Se inserta un hueco en la última


posición.
El 14 no puede ir allí pues viola la
24 31 19 68 propiedad de orden: 31 > 14

65 26 32

0 1 2 3 4 5 6 7 8 9 10 11
13 21 16 24 31 19 68 65 26 32
x=14; hueco  11

4
5
13

Se corre el 31 (el padre del hueco).


21 16
Tampoco aquí puede ser insertado el
14, pues 21 > 14.
24 19 68

65 26 32 31

0 1 2 3 4 5 6 7 8 9 10 11
13 21 16 24 19 68 65 26 32 31
vector[11] = 31; hueco  11/2 = 5

13

14 16 Se corre el 21 (el padre del hueco).


Ahora sí se puede insertar el 14 en el
hueco que deja el 21, ya que 13 < 14.
24 21 19 68

65 26 32 31

0 1 2 3 4 5 6 7 8 9 10 11
13 16 24 21 19 68 65 26 32 31
vector[5] = 21; hueco  5/2 = 2

0 1 2 3 4 5 6 7 8 9 10 11
13 14 16 24 21 19 68 65 26 32 31

Esta estrategia se conoce como filtrado ascendente o reflotamiento, porque el nuevo elemento sube
hasta encontrar la posición adecuada.
En caso necesario, se puede duplicar el tamaño del vector.

Ejemplo 3: Insertar en un montículo vacío las llaves: 150, 80, 40, 30, 10, 70, 110 y 100.
10
0 1 2 3 4 5 6 7 8 9
150
80 150 30 70
40 150 80
30 40 80 150
10 30 80 150 40 100 40 80 110
10 30 70 150 40 80 110
10 30 70 100 40 80 110 150
150

5
6
Observe que, si efectuamos N operaciones seguidas de insertar, obtenemos una complejidad O(N logN).

Analicemos si es posible ser más eficiente, introduciendo los N nodos en el árbol, sin ocuparnos de
mantener el ordenamiento del montículo, y solo en el momento que sea necesario extraer el nodo con
mayor prioridad, efectuar el ordenamiento.
Veamos que sucede al introducir en un árbol vacío las llaves del ejemplo 3, teniendo en cuenta solo la
propiedad de la estructura, pero no así la de ordenación.
En este caso, obtenemos el árbol siguiente, en el que los elementos 150 y 80 violan la propiedad de
ordenación:
150
0 1 2 3 4 5 6 7 8 9
150 80 40 30 10 70 110 100
80 40

30 10 70 110

100

El costo de introducir N nodos en un árbol es O(N), si logramos ahora construir un montículo a partir
del árbol binario completo, en un tiempo lineal, habremos obtenido una mayor eficiencia.

Reconstruir el montículo.
La propiedad de ordenación es inherente a los nodos que tienen hijos; por tanto, se comienza
ordenando por el último nodo que tiene hijos; es decir por el que ocupa la posición tamaño/2 y se va
retrocediendo hasta llegar a la raíz. Si un nodo es menor o igual que sus hijos, entonces está bien
posicionado. Por el contrario, si es mayor que alguno de sus hijos, se debe hundir hasta que se cumpla
la propiedad de ordenamiento. Su lugar lo ocupa el menor de sus hijos.
Del árbol anterior se obtiene el montículo siguiente:
10
0 1 2 3 4 5 6 7 8 9
150 80 40 30 10 70 110 100 ok
150 80 40 30 10 70 110 100 ok 30 40
150 80 40 30 10 70 110 100 hundir
150 10 40 30 80 70 110 100 hundir
10 150 40 30 80 70 110 100 hundir 100 80 70 110
10 30 40 150 80 70 110 100 hundir
10 30 40 100 80 70 110 150 ok
150

Observe que no coincide exactamente con el que se obtuvo ordenando a medida que se insertaba, pero
es también válido.
Cuando restablecemos el montículo, después de introducir N nodos en el árbol vacío, debemos analizar
si se encuentran en la posición correcta los N/2 nodos que tienen hijos. Si alguno no lo está, debemos
hundirlo hasta encontrar su posición.
En el texto, pág. 564-568 se demuestra que ambos procesos (recorrer los N/2 nodos y hundir los que
sean necesario) tiene, en total, un costo lineal.

6
7

Eliminación
Encontrar el mínimo es sencillo: siempre está en la raíz del árbol, la parte más difícil es eliminarlo.
Cuando se retira el mínimo se crea un hueco en la raíz. Como el tamaño del montículo se reduce en
uno, la propiedad estructural obliga a que el último nodo (X) debe moverse a alguna otra parte. Si se
puede colocar X en el hueco habremos terminado -esto solo es posible si el tamaño del árbol es 2 ó 3,
¿por qué?- de lo contrario, se sube para el hueco el menor de sus dos hijos y el hueco se empuja hacia
abajo. Este paso se realiza hasta que X se pueda colocar en el hueco.
Ejemplo 2. Si en el montículo final del ejemplo 2 se hace una eliminación queda:

0 1 2 3 4 5 6 7 8 9 10 11
14 16 24 21 19 68 65 26 32 31
hueco  1; 31 > 14 y 31 > 16

Se elimina la raíz y entonces


14 16 hay que reubicar el 31.
De los dos hijos del hueco
sube el menor: 14
24 21 19 68

65 26 32 31

0 1 2 3 4 5 6 7 8 9 10 11
14 16 24 21 19 68 65 26 32 31
hueco  2; 31 > 24 y 31 > 21

14

16
El 31 no se puede ubicar en
el hueco. Se sube el menor
de los hijos: 21
24 21 19 68

65 26 32 31

0 1 2 3 4 5 6 7 8 9 10 11
14 21 16 24 19 68 65 26 32 31
hueco  5; 31 < 32

7
8
14

Aquí si se puede ubicar el


21 16 31, pues es menor que 32
que es el hijo que le queda.
24 19 68

65 26 32 31

0 1 2 3 4 5 6 7 8 9 10 11
14 21 16 24 31 19 68 65 26 32

14

21 16

24 31 19 68

65 26 32

Esta técnica se conoce como filtrado descendente o hundimiento. Es fácil ver que esta es una
operación logarítmica en el caso peor.

Resumamos las características de cada una de las operaciones vistas:


- Las operaciones insertar y eliminarMin en el montículo pueden traer consigo que este pierda
sus propiedades; por tanto, de alguna manera hay que reestablecerlas.
- Podemos tener una operación introducir que no se ocupa de restablecer el montículo. Esto es
útil si queremos colocar N elementos en el montículo antes de ejecutar la primera operación
eliminarMin; ya que es más eficiente ejecutar N operaciones introducir (O(n)) y una
operación arreglarMonticulo (O (n)), que N operaciones insertar (O (n*logn)).
- Una vez que se procesa una operación introducir, el usuario no puede realizar una operación
buscarMin, ni eliminarMin sin que se realice antes una operación arreglarMonticulo. En
lugar de confiar en el usuario, se mantiene un atributo ordenado, que toma el valor false cuando
una aplicación de introducir produce una violación del orden en el montículo.
- Por su parte, buscarMin comprueba si se satisface la propiedad de ordenación y si es necesario,
llama a arreglarMonticulo para restablecerla.

Analicemos el esqueleto de la clase MonticuloBinario y algunos de sus métodos:

public class MonticuloBinario<E extends Comparable> implements


ColaPrioridad<E>{

private int tamActual; //cantidad de elementos del montículo


private boolean ordenado; //true si cumple la propiedad de ordenación

8
9
private Object [] vector; //el vector del montículo
private static final int CAPACIDAD = 11;//tamaño del vector con centinela

public MonticuloBinario( Object infNeg ) //constructor con centinela


public void insertar( E x ) //añade un nuevo elemento al montículo
manteniendo la ordenación
public void introducir ( E x ) //añade un nuevo elemento, pero sin ordenar
public E buscarMin()throws ColaVacia //devuelve la llave almacenada en la raíz
public E eliminarMin()throws ColaVacia //devuelve y elimina la llave de la raíz
public boolean esVacia()
{ return tamActual ==0;}
public void vaciar()
{ tamActual = 0;}

private void obtenerVector( int nuevoTamMax )//reserva memoria para el vector


private void comprobarTam() //duplica el tamaño del vector si es necesario
private void hundir(int hueco) //para hundir en el montículo
private void arreglarMonticulo() //reestablece el orden en el montículo
}

Constructor
public MonticuloBinario(Object infNeg) //infNeg un valor menor o igual q
todos los demás.
{
tamActual = 0;
ordenado = true;
obtenerVector(CAPACIDAD);
vector[ 0] = infNeg;
}

Reserva de memoria
private void obtenerVector(int nuevoTamMax)//reserva memoria para el vector
{
vector=new Object[nuevoTamMax+1];// una posición extra para el centinela
}

Buscar el elemento de mayor prioridad


public E buscarMin() throws ColaVacia {
if (esVacia()) {
throw new ColaVacia("No hay elementos en la Cola");
}
if (!ordenado)//solo hay garantía que el mínimo está raíz si ordenado
{
arreglarMonticulo();
}
return (E) vector[1];
}

Introducir elementos en el árbol, sin ordenar


public void introducir(E x) {
comprobarTam();
vector[ ++ tamActual] = x;
if( x.compareTo((E) vector[ tamActual/2]) < 0)// lo compara con el padre

9
10
{
ordenado = false; // se ha introducido un elemento sin ordenar
}
}

Insertar elementos en el montículo manteniendo el ordenamiento, si existe


public void insertar(E x) {
if (!ordenado) //no hay por qué preocuparse por el ordenamiento
{
introducir(x);
return;
}
comprobarTam();
int hueco = ++tamActual;
for (; (x.compareTo((E) vector[hueco / 2]) < 0); hueco /= 2) {
vector[hueco] = vector[hueco / 2];
}
vector[hueco] = x;
}

Duplicar el tamaño del vector, si fuese necesario


private void comprobarTam() {
if (tamActual == vector.length - 1) {
Object[] vectorAnt = vector;
obtenerVector(tamActual * 2);
for (int i = 0; i < vectorAnt.length; i++) {
vector[i] = vectorAnt[i];
}
}
}

Retornar el elemento de mayor prioridad, manteniendo el montículo ordenado


public E eliminarMin() throws ColaVacia {
E elemMin = buscarMin(); //Queda libre la raíz.
vector[ 1] = vector[tamActual--];//Se trae el nodo de la Última posición.
hundir(1); //Se hunde el elemento de la raíz
//hasta encontrar la posición correcta.
return elemMin;
}

Construcción del montículo:


La idea del método es llamar a hundir sobre cada nodo en sentido inverso al recorrido por niveles; de
esta forma, cuando realicemos la llamada con i se habrán procesado todos los descendientes del nodo
i. Observe que, no hace falta ejecutar hundir sobre las hojas, por lo que se comienza por el nodo de
mayor índice que no sea una hoja, es decir, por el último padre.

private void arreglarMonticulo() {


for (int i = tamActual / 2; i > 0; i--) {
hundir(i);
}
ordenado = true;
}

10
11
private void hundir(int hueco) {
//coloca el elemento que está en la posición hueco en el lugar
//correcto dentro del montículo, empujándolo hacia abajo
int hijo;
Object tmp = vector[hueco];
for (; hueco * 2 <= tamActual; hueco = hijo) {
hijo = hueco * 2;
E x = (E) vector[hijo + 1];
if (hijo != tamActual && x.compareTo((E) vector[hijo]) < 0) {
hijo++;
}
E y = (E) vector[hijo];
if (y.compareTo((E) tmp) < 0) {
vector[hueco] = vector[hijo];
} else {
break;
}
}
vector[hueco] = tmp;
}

CONCLUSIONES
Hemos estudiado los Montículos, un tipo de árbol con propiedades de estructura y ordenación que
permiten implementar el TDA Cola de Prioridad.
- Se puede implementar usando un simple vector.
- Soporta insertar y eliminarMin en tiempo logarítmico respecto al número de elementos
existentes
- Se puede lograr un tiempo lineal al introducir un conjunto de elementos antes de realizar
alguna operación de acceso.

11

También podría gustarte