Capı́tulo 5 Colas Las colas al igual que las pilas son un tipo especial de listas en las cuales los elementos se insertan por un lado y se eliminan por el otro. Es decir se sacan en el mismo orden en que entraron. En ocasiones suele referirse a ellas como estructuras FIFO (first in, first out). En la vida diaria tenemos las filas en las taquillas, en el banco, en el super etc. En computación se usan mucho por ejemplo, en sistemas operativos para listar los archivos que se desean imprimir (sin matar a nadie) denominadas colas de impresión, los buffers de lectura, etc. en la teorı́a de gráficas, etc. 5.1 Implementación La interfaz podrı́a ser: interface Encolable { public boolean estáVacı́a(); public void limpiar() ; public void agregar(Object dato) ; public void eliminar() ; public Object tomar(); public java.util.Iterator elementos(); } Donde los métodos • agregar. Inserta Un nuevo elemento colocandola al final de la cola. La posición de los otros elementos no se altera. 1 5.1. IMPLEMENTACIÓN 2 • eliminar. Elimina el elemtno que está al inicio de la cola. Si la cola está vacı́a se genera un error. (NoSuchElementException). • tomar. Devuelve el valor del primer elemento de la cola, sin alterar el estado de ésta. Este método no altera el estado de la cola. • estáVacı́a. Permite saber si la pila está vacı́a. • limpiar. Vacia el contenido de una pila. Una implementación usando estructuras ligadas se presenta a continuación: /** * Cola usando nodos * Programo: Amparo López Gaona * Marzo 2006 */ public class Cola implements Encolable{ protected Nodo inicio; protected Nodo fin; private final Comparator prueba; /** Construye la cola */ public Cola() { inicio = null; fin = null; prueba = new DefaultComparator(); } /** Construye una cola con el comparador proporcionado. */ public Cola(Comparator c) { inicio = null; fin = null; prueba = c; } /** Prueba que la cola esté vacı́a. * @return true si está vacı́a y false en otro caso. 5.1. IMPLEMENTACIÓN 3 */ public boolean estáVacı́a() { return inicio == null; } /** Crea una cola vacı́a. public void limpiar() { inicio = fin = null; } */ /** Inserta en la cola, siempre por el final * @param dato - elemento que será insertado */ public void agregar(Object dato) { if (inicio == null) inicio = fin = new Nodo(dato); else { Nodo temp = new Nodo(dato); fin.sgte = temp; fin = temp; } } /** Devuelve el primer elemento de la cola * @return Object - elemento del inicio de la cola */ public Object tomar() { if (inicio == null) return null; //La cola está vacı́a return inicio.elemento; } /** Elimina el primer elemento de la cola public void eliminar() { if (inicio != null) inicio = inicio.sgte; } public java.util.Iterator elementos() { return new MiIterador(); */ 5.2. EJEMPLO 4 } // Falta el iterador y el comparador por omisión 5.2 Ejemplo En esta sección se presenta el clásico problema del consumidor productor, sólo que muy reducido. El productor produce una cantidad de productos y los deposita en una banda (cola). El consumidor consume, de la banda, alguna otra cantidad de productos, esperando hasta que en la banda haya la cantidad suficiente. Si los productores y los consumidores son independientes ¿cómo se comunican/sincronizan entre sı́?. R. vı́a colas. Cada vez que se produce un artı́culo éste se coloca en una cola sin preocuparse del momento en que será consumido ni por quién. El consumidor consume de la cola y no se preocupa ni cuándo ni por quién fue producido. El ejemplo concreto consiste en simular el trabajo en un panaderı́a que expende cajas con tres tipos de panes: conchas, donas y cuernos. Para ello se tienen tres tipos de panaderos cada uno especializado en producir un tipo de pan. Cada pan es colocado en una cola común y del otro lado un empacador los toma para colocarlos en una caja. La cantidad de elementos que se guardan en la caja está determinada por el peso de los mismos y la capacidad máxima de las cajas. Para resolver este problema se requieren varias clases: una para los panes, otra para las cajas, otra para los panaderos, otra para los empacadores y otra más para la panaderı́a. Un pan se simula con su peso y nombre. Los únicos métodos que tiene son un constructor y el toString que devuelve el nombre del pan. class Pan { private final double peso; privata final String nombre; public Pan (double pp, String n) { peso = pp; nombre = n; } public double peso() {return peso;} public String toString () { return nombre; } } La clase de las cajas tiene el peso de la caja en cada momento y una 5.2. EJEMPLO 5 lista en la que se guardan los panes. (Aquı́ podrı́a ir cualquier estructura incluyendo arreglo dinámico). Los métodos que tiene además del constructor son uno para ir guardando los panes en la caja, otro que proporciona el peso actual de la caja y otro método para mostrar lo que se tiene en ella, para ello se utiliza el iterador de la clase Lista. class Caja { private double pesoC; private Lista panes; private int pesoMaximo; // peso actual de la caja // panes // Peso maximo de la caja public Caja () { pesoC = 0; panes = new Lista(); pesoMaximo = 120; } public Caja (int n) { pesoC = 0; panes = new Lista(); pesoMaximo = n; } public void guardaPan (Pan p) { panes.insertar(p); pesoC += p.peso; } public double peso () { return pesoC; } public double pesoMax() { return pesoMaximo;} public String toString () { java.util.Iterator e = panes.elementos(); String resultado = "Caja con "; while (e.hasNext()) resultado = resultado + e.next()+" "; 5.2. EJEMPLO 6 return resultado; } } La clase Panadero simula el comportamiento de un panadero, para ello se requiere especificar el tipo de pan que fabrica, el tiempo que le toma hacerlo y la cola en donde dejara el pan. Aquı́ se introduce un nuevo concepto: el de hilos. Hasta ahora toda la programación ha sido en forma secuencial desde la primera instrucción del main hasta la última. Ahora se va a escribir una clase que puede trabajar de manera concurrente, es decir, un poco independiente del flujo del programa principal. En java esto se conoce como un hilo. 1 Este nuevo programa o proceso requiere para su ejecución un método run que es el equivalente al main, en el sentido de inicialización, para ejecución no-concurrente. class Panadero extends Thread { private Cola banda; private Pan pas; private int tiempo; public Panadero (Cola c, Pan p, int t) { banda = c; pas = p; tiempo = t; } public void run () { while (true) { banda.agregar(pas); try { sleep(tiempo);} catch(Exception e) { } } } //Agrega un pan a la banda //Tiempo que tarda en hacer un pan } La clase Empacador también va a trabajar de manera concurrente, aquı́ sólo necesita una cola como estructura del objeto. En el método run se simula el 1 Un hilo es un programa que corre dentro de otro. 5.2. EJEMPLO 7 llenado de la caja. Aquı́ se tiene la instrucción synchronized que especifica que en la cola denominada banda se hará el trabajo concurrente, para no intentar sacar de ella un pan mientras algún panadero está colocando alguno. class Empacador extends Thread { private Cola banda; private final int peso; public Empacador (Cola c, int p) { banda = c; peso = p; } public void run () { int contadorCajas = 0; Caja miCaja = new Caja(peso); while (contadorCajas <= 0) { synchronized (banda) { if (! banda.estáVacı́a()) { Pan p = (Pan) banda.tomar(); double pesoF = miCaja.peso() + p.peso(); if (pesoF > miCaja.pesoMax()) { System.out.println(miCaja+"\n\n"); miCaja = new Caja(peso); } miCaja.agregaPan(p); } } } } La clase Panaderı́a se encarga de poner a trabajar a los panaderos y al empacador. Para ello se crea cada objeto y luego se llama al método start que a su vez se encarga de llamar implı́citamente al método run. class Panaderia { public static void main (String [ ] args) Cola colaPastas = new Cola(); Panadero p1 = new Panadero (colaPastas, Panadero p2 = new Panadero (colaPastas, Panadero p3 = new Panadero (colaPastas, Panadero p4 = new Panadero (colaPastas, { new new new new Pan(10.0, Pan(25.0, Pan(20.0, Pan(30.0, "galleta"), 10); "dona"), 70); "cuerno"), 50); "concha"), 60); 5.2. EJEMPLO 8 Empacador c = new Empacador(colaPastas,250); p2.start(); p3.start(); p4.start(); p1.start(); c.start(); } } Un ejemplo de ejecución de este programa es: Caja con dona cuerno concha galleta galleta galleta galleta galleta cuerno galleta concha galleta dona galleta galleta galleta Caja con cuerno galleta concha galleta galleta dona galleta cuerno galleta galleta galleta concha galleta galleta cuerno Caja con dona galleta galleta galleta concha galleta cuerno galleta galleta galleta dona galleta galleta concha cuerno galleta Caja con galleta galleta galleta dona galleta cuerno concha galleta galleta galleta galleta cuerno galleta galleta dona concha Caja con galleta galleta galleta cuerno galleta galleta concha galleta dona galleta cuerno galleta galleta galleta galleta concha galleta ... 5.2.1 Backtracking El backtrack es una técnica que consiste en permitir elegir entre varias posibilidades un camino de solución y en caso de no ser correcto regresar al punto donde se tomó la decisión. Ejemplo: Encontrar la salida en un laberinto, como el siguiente: +--+--+--+--+--+ |F | __ | | |__ __| | |__ __ |__ | | | | | | |___________|_S| 5.2. EJEMPLO 9 La representación del laberinto puede realizarse mediante el siguiente código: 0 0000 1 2 3 4 5 0001 0010 0011 0100 0101 6 0110 7 0111 8 9 10 11 12 13 14 15 0100 0101 1010 1011 1100 1101 1110 1111 Figura 5.1: Con ello, el programa puede hacerse general leyendo de un archivo la configuración del laberinto. El laberinto del ejemplo usando la configuración descrita queda como sigue: +--+--+--+--+--+ |14|12 5_ 4 6 | |10|9_ 4 3_|10| |9_ 5_ 2 |13 2 | |14|14|10|12 2 | |9__1__1__3_|11| Para el programa se utiliza un arreglo visitado que marca el orden en que se visitan las celdas. Este arreglo tiene dos propósitos: dar una forma de indicar si una celda ya se ha visitado y proporcionar una representación del orden en el que se realiza la búsqueda. /** * Programa que muestra la salida de un laberinto (si la hay) * @author ALG * @version Octubre 2006 */ public class Laberinto { private int largo; private int ancho ; 5.2. EJEMPLO private int [][] paredes = { {14, 12, 5, 4, 6}, {10, 9, 4, 3, 10}, {9, 5, 2, 13, 2}, {14, 14, 10, 12, 2}, {9, 1, 1, 3, 11}, }; private int [][] visitado; public Laberinto () { largo = 5; ancho = 5; visitado = new int[5][5]; for (int i = 0; i < ancho; i++) for (int j = 0; j < largo; j++) visitado[i][j] = 0; } public Laberinto (String file) throws IOException { DataInputStream in = new DataInputStream( new FileInputStream(file)); ancho = in.readInt(); largo = in.readInt(); paredes = new int[ancho][largo]; visitado = new int[ancho][largo]; for (int i = 0; i < ancho; i++) for (int j = 0; j < largo; j++) { paredes[i][j] = in.readInt(); visitado[i][j] = 0; } } private void resolverLaberinto () { Deque que = new Deque(); que.agregarÚltimo(new Punto(largo-1, ancho-1)); int contVisitas = 0; while (! que.estáVacı́a()) { Punto p = (Punto) que.tomarÚltimo(); 10 5.2. EJEMPLO que.eliminarÚltimo(); if (visitado[p.obtenerX()][p.obtenerY()] == 0) { visitado[p.obtenerX()][p.obtenerY()] = ++contVisitas; mostrar(); if ((p.obtenerX() == 0) && (p.obtenerY() == 0)) return; // Se llegó a la meta ponVecinos(p.obtenerX(), p.obtenerY(), que); try {Thread.sleep(200);} catch (Exception e) { } } } System.err.println("No hay solución"); } private void ponVecinos (int x, int y, Deque que) { if ((paredes[x][y] & 1) == 0) que.agregarÚltimo(new Punto(x+1, y)); if ((paredes[x][y] & 2) == 0) que.agregarÚltimo(new Punto(x, y+1)); if ((paredes[x][y] & 4) == 0) que.agregarÚltimo(new Punto(x-1, y)); if ((paredes[x][y] & 8) == 0) que.agregarÚltimo(new Punto(x, y-1)); } public void mostrar() { System.out.println("El laberinto original tiene:"); for(int i = 0; i < largo; i++){ for (int j = 0; j < ancho; j++) System.out.print(paredes[i][j]+ " "); System.out.println(); } System.out.println("\nSolución:\n"); for(int i = 0; i < largo; i++){ for (int j = 0; j < ancho; j++) System.out.print(visitado[i][j]+ " "); System.out.println(); } 11 5.2. EJEMPLO 12 } public static void main (String [ ] args) { Laberinto mundo = new Laberinto(); mundo.mostrar(); mundo.resolverLaberinto(); } } Se utiliza una deque para mantener las celdas que están en espera de ser investigadas. Se inicializa con la celda inicial (la esquina inferior derecha) del laberinto. Se van removiendo elementos de la deque, si es un punto que no se ha visto, se marca como vistado. Luego todos los vecions de la celada actual se coloca en la deque. El método ponerVecinos busca las celdas válidas verificando si los lados de la variable paredes están abiertos. Seguimiento del programa: Si se tuviera en la deque el elemento (3,4): +-------+ I-> | (3,4) | +-------+ <-F Se saca el elemento y se introducen sus vecinos: +-------+-------+-------+ I-> | (3,3) | (2,4) | (4,4) | <-F +-------+-------+-------+ Al introducir los vecinos no se verifica que hayan sido visitados previamente, esto se hace al sacarlos. Siempre se saca un sólo nodo y se introducen sus vecinos. Dos pasos más adelante se tiene el siguiente contenido: +-------+-------+-------+-------+-------+-------+ I-> | (4,1) | (3,2) | (4,3) | (3,3) | (2,4) | (4,4) | <-F +-------+-------+-------+-------+-------+-------+