notas estructuras

159
Estructuras de Datos con JAVA ector Tejeda V Abril, 2010

Upload: felipe-cespedes

Post on 05-Jul-2015

3.207 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Notas Estructuras

Estructuras de Datos con JAVA

Hector Tejeda V

Abril, 2010

Page 2: Notas Estructuras

2

Page 3: Notas Estructuras

Indice general

1. Introduccion 71.1. Que son las estructuras de datos . . . . . . . . . . . . . . . . . 71.2. Generalidades de las Estructuras de Datos . . . . . . . . . . . 7

2. Arreglos, listas enlazadas y recurrencia 112.1. Usando arreglos . . . . . . . . . . . . . . . . . . . . . . . . . . 11

2.1.1. Guardando entradas de juego en un arreglo . . . . . . . 112.1.2. Ordenando un arreglo . . . . . . . . . . . . . . . . . . 152.1.3. Metodos para arreglos y numeros aleatorios . . . . . . 172.1.4. Arreglos bidimensionales y juegos de posicion . . . . . 22

2.2. Listas simples enlazadas . . . . . . . . . . . . . . . . . . . . . 262.2.1. Insercion en una lista simple enlazada . . . . . . . . . . 282.2.2. Insercion de un elemento en la cola . . . . . . . . . . . 282.2.3. Quitar un elemento de una lista simple enlazada . . . . 29

2.3. Listas doblemente enlazadas . . . . . . . . . . . . . . . . . . . 292.3.1. Insercion entre los extremos de una lista doble enlazada 312.3.2. Remover en el centro de una lista doblemente enlazada 322.3.3. Una implementacion de una lista doblemente enlazada 33

2.4. Listas circulares y ordenamiento . . . . . . . . . . . . . . . . . 362.4.1. Listas circularmente enlazadas . . . . . . . . . . . . . . 362.4.2. Ordenando una lista enlazada . . . . . . . . . . . . . . 41

2.5. Recurrencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422.5.1. Recurrencia binaria . . . . . . . . . . . . . . . . . . . . 502.5.2. Recurrencia multiple . . . . . . . . . . . . . . . . . . . 52

3. Pilas y colas 553.1. Genericos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 553.2. Pilas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58

Page 4: Notas Estructuras

4 INDICE GENERAL

3.2.1. El tipo de dato abstracto pila . . . . . . . . . . . . . . 583.2.2. Una implementacion simple de la pila usando un arreglo 613.2.3. Implementando una pila con una lista generica enlazada 683.2.4. Invirtiendo un arreglo usando una pila . . . . . . . . . 713.2.5. Apareando parentesis y etiquetas HTML . . . . . . . . 72

3.3. Colas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 763.3.1. El tipo de dato abstracto cola . . . . . . . . . . . . . . 763.3.2. Una interfaz cola en Java . . . . . . . . . . . . . . . . . 783.3.3. Una implementacion simple de la cola con un arreglo . 793.3.4. Implementando una cola con una lista generica ligada . 823.3.5. Planificador Round Robin . . . . . . . . . . . . . . . . 83

3.4. Colas con doble terminacion . . . . . . . . . . . . . . . . . . . 843.4.1. El tipo de dato abstracto deque . . . . . . . . . . . . . 853.4.2. Implementando una deque . . . . . . . . . . . . . . . . 86

4. Listas e Iteradores 914.1. Listas Arreglo . . . . . . . . . . . . . . . . . . . . . . . . . . . 91

4.1.1. El tipo de dato abstracto lista arreglo . . . . . . . . . . 924.1.2. El patron adaptador . . . . . . . . . . . . . . . . . . . 924.1.3. Una implementacion simple usando un arreglo . . . . . 934.1.4. Una interfaz simple y la clase java.util.ArrayList . . . . 954.1.5. Implementando una lista arreglos usando Arreglos Ex-

tendibles . . . . . . . . . . . . . . . . . . . . . . . . . . 964.2. Listas nodo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98

4.2.1. Operaciones basadas en nodos . . . . . . . . . . . . . . 984.2.2. Posiciones . . . . . . . . . . . . . . . . . . . . . . . . . 994.2.3. El tipo de dato abstracto . . . . . . . . . . . . . . . . . 994.2.4. Implementacion de una lista doblemente enlazada . . . 103

4.3. Iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1104.3.1. Los tipos de dato abstracto Iterador e Iterable . . . . . 1104.3.2. El ciclo Java For-Each . . . . . . . . . . . . . . . . . . 1124.3.3. Implementando Iteradores . . . . . . . . . . . . . . . . 113

5. Arboles 1175.1. Arboles generales . . . . . . . . . . . . . . . . . . . . . . . . . 117

5.1.1. Definiciones de arboles y propiedades . . . . . . . . . . 1175.1.2. Definicion formal de arbol . . . . . . . . . . . . . . . . 1185.1.3. El tipo de dato abstracto arbol . . . . . . . . . . . . . 119

Page 5: Notas Estructuras

INDICE GENERAL 5

5.1.4. Implementando un arbol . . . . . . . . . . . . . . . . . 1215.2. Algoritmos de recorrido para arbol . . . . . . . . . . . . . . . 122

5.2.1. Profundidad y altura . . . . . . . . . . . . . . . . . . . 1225.2.2. Recorrido en preorden . . . . . . . . . . . . . . . . . . 1265.2.3. Recorrido en postorden . . . . . . . . . . . . . . . . . . 128

5.3. Arboles Binarios . . . . . . . . . . . . . . . . . . . . . . . . . 1305.3.1. El ADT arbol binario . . . . . . . . . . . . . . . . . . . 1315.3.2. Una interfaz de arbol binario en Java . . . . . . . . . . 1325.3.3. Propiedades de arboles binarios . . . . . . . . . . . . . 1325.3.4. Una estructura enlazada para arboles binarios . . . . . 1345.3.5. Una representacion lista arreglo de un arbol binario . . 1435.3.6. Recorridos de Arboles Binarios . . . . . . . . . . . . . 1445.3.7. El metodo patron plantilla . . . . . . . . . . . . . . . . 151

Page 6: Notas Estructuras

6 INDICE GENERAL

Page 7: Notas Estructuras

Capıtulo 1

Introduccion

1.1. Que son las estructuras de datos

Una estructura de datos es un arreglo de datos en la memoria de unacomputadora, y en algunas ocasioneas en un disco. Las estructuras de datosincluyen arreglos, listas enlazadas, pilas, arboles binarios, y tablas de disper-sion entre otros. Los algoritmos manipulan los datos en estas estructuras devarias formas, para buscar un dato particular u ordenar los datos.

1.2. Generalidades de las Estructuras de Da-

tos

Una forma de ver las estructuras de daotos es enfocandose en sus forta-lezas y debilidades. En el cuadro 1.2 se muestran las ventajas y desventajasde las estructuras de datos que se revisaran, los terminos que se usan seranabordados en otros capıtulos.

Las estructuras mostradas en el cuadro 1.2, excepto los arreglos, pue-den ser pensados como tipos de datos abstractos o Abstract Data Type(ADT). Un ADT, de forma general, es una forma de ver una estructura dedatos: enfocandose en lo que esta hace e ignorando como hace su trabajo, esdecir, el ADT debera cumplir con ciertas propiedades, pero la manera comoestara implementado puede variar, aun empleando el mismo lenguaje. Porejemplo, el ATD pila puede ser implementado con un arreglo o bien con unalista enlazada.

Page 8: Notas Estructuras

8 Introduccion

Estructura Desventajas VentajasArreglo Insercion rapida, acceso muy

rapido si se conoce el ındice.Busqueda y borrado lento,tamano fijo.

Arreglo orde-nado

Busqueda mas rapida que elarreglo desordenado.

Insercion y borrado lento,tamano fijo.

Pila Proporciona acceso ultimoen entrar, primero en salir.

Acceso lento a otros elemen-tos.

Cola Proporciona primero en en-trar, primero en salir.

Acceso lento a otros elemen-tos.

Lista enlaza-da

Insercion y borrado rapido. Busqueda lenta.

Arbol binarioBusqueda, insercion y borra-do rapido si el arbol perma-nece balanceado.

Complejo.

Arbol rojine-gro

Busqueda, insercion y borra-do rapido. Arbol siempre ba-lanceado.

Complejo.

Arbol 2-3-4Busqueda, insercion y borra-do rapido. Arbol siempre ba-lanceado.

Complejo.

Tabla de dis-persion

Acceso muy rapido si se co-noce la llave. Insercion rapi-da.

Borrado lento, acceso lentosi no se conoce la llave, usoineficiente de la memoria.

Montıculo Insercion, borrado y accesoal elemento mas grande rapi-do

Acceso lento a otros elemen-tos.

Grafo Modela situaciones del mun-do real

Algunos algoritmos son len-tos y complejos.

Cuadro 1.1: Caracterısticas de las Estructuras de Datos

Page 9: Notas Estructuras

1.2 Generalidades de las Estructuras de Datos 9

Varios de los algoritmos que seran discutidos en este materioal se apli-caran directamente a estructuras de datos especıficas. Para la mayorıa de lasestructuras de datos, se requiere que hagan las siguientes tareas:

1. Insertar un nuevo dato.

2. Buscar un elemento indicado.

3. Borrar un elemento indicado.

Tambien se necesitara saber como iterar a traves de todos los elementos enla estructura de datos, visitando cada uno en su turno ya sea para mostrarloo para realizar alguna accion en este.

Otro algoritmo importante es el ordenamiento. Hay varias formas paraordenar los datos, desde ideas simples hasta otras que permiten realizarlo demanera rapida.

El concepto de recurrencia es importante en el diseno de ciertos algorit-mos. La recurrencia involucra que un metodo se llame a si mismo.

Page 10: Notas Estructuras

10 Introduccion

Page 11: Notas Estructuras

Capıtulo 2

Arreglos, listas enlazadas yrecurrencia

2.1. Usando arreglos

En esta seccion, se exploran unas cuantas aplicaciones con arreglos.

2.1.1. Guardando entradas de juego en un arreglo

La primera aplicacion que sera revisada es para guardar las entradas en unarreglo—en particular, las mayores registros para un videojuego. Los arreglostambien se podrıan usar para guardar registros de pacientes en un hospitalo los nombres de estudiantes.

En primer lugar se debe determinar lo que se quiere incluir en el registro deuna puntuacion alta. Por supuesto, un componente que se debera incluir es unentero representando los puntos obtenidos, al cual llamaremos puntuacion.Otra caracterıstica que debera incluirse es el nombre de la persona que obtuvola puntuacion, la cual denominaremos nombre.�public class EntradaJuego {

protected String nombre; // nombre de la persona que gano esta puntuacion

protected int puntuacion; // el valor de la puntuacion

/** Constructor para crear una entrada del juego */

public EntradaJuego(String n, int p) {

nombre = n;

puntuacion = p;

}

Page 12: Notas Estructuras

12 Arreglos, listas enlazadas y recurrencia

/** Recupera el campo nombre */

public String getNombre () { return nombre; }

/** Recupera el campo puntuacion */

public int getPuntuacion () { return puntuacion; }

/** Regresa una cadena representando esta entrada */

public String toString () {

return "(" + nombre + ", " + puntuacion + ")";

}

}�Listado 2.1: Clase EntradaJuego.java

Clase para las mayores puntuaciones

En el listado 2.2 se implementa lo que se detalla en los siguientes miem-bros de la clase Puntuaciones. La clase es usada para guardar un determi-nado numero de puntuaciones altas. Adicionalmente se consideran metodosque permiten modificar una instanciacion de esta clase, ya sea para agregarnuevos records, o bien, para quitar algun record no valido.�/** Clase para guardar los records en un arreglo en orden descendente. */

public class Puntuaciones {

public static final int maxEntradas = 10; // num. de records que se guardan

protected int numEntradas; // num. actual de entradas

protected EntradaJuego [] entradas; // arreglo de entradas (nomb. y puntuac .)

/** Constructor por defecto */

public Puntuaciones () {

entradas = new EntradaJuego[maxEntradas ];

numEntradas = 0;

}

/** Regresa una cadena de las mayores puntuaciones */

public String toString () {

String s = "[";

for (int i = 0; i < numEntradas; i++) {

if (i > 0) s += ", "; // separar entradas con comas

s += entradas[i];

}

return s + "]";

}

/** Intentar agregar un nuevo record a la coleccion si es lo suficientemente

alto */

public void add(EntradaJuego e) {

int nuevaPuntuacion = e.getPuntuacion ();

// ¿es la nueva entrada realmente un record?

if (numEntradas == maxEntradas) { // el arreglo esta lleno

if ( nuevaPuntuacion <= entradas[numEntradas -1]. getPuntuacion () )

return; // la nueva entrada , e, no es un record

}

Page 13: Notas Estructuras

2.1 Usando arreglos 13

else // el arreglo no esta lleno

numEntradas ++;

// Encontrar el lugar en donde la nueva entrada (())e estara

int i = numEntradas -1;

for ( ; (i >= 1) && (nuevaPuntuacion > entradas[i-1]. getPuntuacion ()); i--)

entradas[i] = entradas[i - 1]; // mover entrada i un lugar a la derecha

entradas[i] = e; // agregar el nuevo record a entradas

}

/** Quitar el record del ındice i y devolverlo. */

public EntradaJuego remove(int i) throws IndexOutOfBoundsException {

if ((i < 0) || (i >= numEntradas ))

throw new IndexOutOfBoundsException("Indice no valido: " + i);

// guardar temporlmente el objecto a ser quitado

EntradaJuego temp = entradas[i];

for (int j = i; j < numEntradas - 1; j++) // contar hacia adelante desde i

entradas[j] = entradas[j+1]; // mover una celda a la izquierda

entradas[ numEntradas - 1 ] = null; // anular el menor registro

numEntradas --;

return temp; // regresar objeto eliminado

}

}�Listado 2.2: Clase Puntuciones.java

Se quiere almacenar los mayores registros en un arreglo llamado entradas.La cantidad de registros puede variar, por lo que se usara el nombre simboli-co, maxEntradas, para representar el numero de registros que se quierenguardar. Se debe poner esta variable con un valor especıfico, y usando es-ta variable en el codigo, se pueden hacer facilmente cambios mas tarde sise requiere. Se define despues el arreglo, entradas, para que sea un arreglode tamano maxEntradas. Inicialmente, el arreglo guarda solamente entradasnulas, pero conforme el usuario juega, se llenara en el arreglo con nuevos ob-jetos EntradaJuego. Por lo que se necesitara definir metodos para actualizarlas referencias EntradaJuego en las entradas del arreglo.

La forma como se mantiene organizado el arreglo entradas es simple—seguarda el conjunto de objetos EntradaJuego ordenado por sus valores ente-ros puntuacion, de mayor a menor. Si el numero de objetos EntradaJuego

es menor que maxEntradas, entonces se permite que al final del arreglo seguarden referencias null. Con este diseno se previene tener celdas vacıas, u“hoyos”, y los registros estan desde el ındice cero hasta la cantidad de juegosjugados.

El metodo toString() produce una cadena representando las mayo-res puntuaciones del arreglo entradas. Puede tambien ser empleado parapropositos de depuracion. La cadena sera una lista separada con comas de

Page 14: Notas Estructuras

14 Arreglos, listas enlazadas y recurrencia

objetos EntradaJuego del arreglo entradas. La lista se genera con un ciclofor, el cual agrega una coma justo antes de cada entrada que venga despuesde la primera.

Insercion

Una de las actualizaciones mas comunes que se quiere hacer al arregloentradas es agregar una nueva entrada. Suponiendo que se quiera insertarun nuevo objeto e del tipo EntradaJuego se considera como se harıa lasiguiente operacion en una instancia de la clase Puntuaciones:

add(e): inserta la entrada de juego e en la coleccion de las ma-yores puntuaciones. Si la coleccion esta llena, entoncese es agregado solamente si su puntuacion es mayor quela menor puntuacion en el conjunto, y en este caso, ereemplaza la entrada con la menor puntuacion.

El principal reto en la implementacion de esta operacion es saber dondee debera ir en el arreglo entradas y hacerle espacio a e.

Se supondra que las puntuaciones estan en el arreglo de izquierda a dere-cha desde la mayor hasta la menor. Entonces se quiere que dada una nuevaentrada, e, se requiere saber donde estara. Se inicia buscando al final delarreglo entradas. Si la ultima referencia en el arreglo no es null y su pun-tuacion es mayor que la puntuacion de e, entonces se detiene la busqueda,ya que en este caso, e no es una puntuacion alta. De otra forma, se sabeque e debe estar en el arreglo, y tambien se sabe que el ultimo registro enel arreglo ya no debe estar. Enseguida, se va al penultima referencia, si estareferencia es null o apunta a un objeto EntradaJuego con una puntuacionque es menor que e, esta referencia debera ser movida una celda a la derechaen el arreglo entrada. Se continua comparando y desplazando referencias deentradas del juego hasta que se alcanza el inicio del arreglo o se compara lapuntuacion de e con una entrada con una puntuacion mayor. En cualquiercaso, se habra identificado el lugar al que e pertenece.

Remocion de un objeto

Suponiendo que algun jugador hace trampa y obtiene una puntuacionalta, se desearıa tener un metodo que permita quitar la entrada del juego de

Page 15: Notas Estructuras

2.1 Usando arreglos 15

la lista de las mayores puntuaciones. Por lo tanto se considera como deberıaimplementarse la siguiente operacion:

remove(i): quita y regresa la entrada de juego e con el ındice i enel arreglo entradas. Si el ındice i esta fuera de los lımi-tes del arreglo entradas, entonces el metodo debera lan-zar una excepcion; de otra forma, el arreglo entradas

sera actualizado para quitar el objeto en la posicion i ytodos los objetos guardados con ındices mayores que iseran movidos encima para llenar el espacio del dejadopor el objeto quitado.

La implementacion para remove sera parecida al algoritmo para agregarun objeto, pero en forma inversa. Para remover la referencia al objeto conındice i, se inicia en el ındice i y se mueven todas las referencias con ındicesmayores que i una celda a la izquierda.

Los detalles para hacer la operacion de remocion consideran que prime-ramente se debe guardar la entrada del juego, llamada e, en el ındice i delarreglo, en una variable temporal. Se usara esta variable para regresar ecuando se haya terminado de borrar. Para el movimiento de las referenciasque son mayores que i una celda a la izquierda, no se hace hasta el finaldel arreglo—se para en la penultima referencia, ya que la ultima referenciano tiene una referencia a su derecha. Para la ultima referencia en el arregloentradas, es suficiente con anularla. Se termina regresando una referenciade la entrada removida, la cual no tiene una referencia que le apunte desdeel arreglo entradas.

2.1.2. Ordenando un arreglo

En la seccion previa, se mostro como se pueden agregar objetos o quitarestos en un cierto ındice i en un arreglo mientras se mantiene el ordenamientoprevio de los objetos intacto. En esta seccion, se revisa una forma de iniciarcon un arreglo con objetos que no estan ordenadados y ponerlos en orden.Esto se conoce como el problema de ordenamiento.

Un algoritmo simple de insercion ordenada

Se revisaran varios algoritmos de ordenamiento en este material. Como in-troduccion, se describe en esta seccion, un algoritmo simple de ordenamiento,

Page 16: Notas Estructuras

16 Arreglos, listas enlazadas y recurrencia

llamado insercion ordenada. En este caso, se describe una version especıficadel algoritmo donde la entrada es un arreglo de elementos comparables. Seconsideran tipos de algoritmos de ordenamiento mas general mas adelante.

El algoritmo de insercion ordenada trabaja de la siguiente forma. Se iniciacon el primer caracter en el arreglo y este caracter por si mismo ya esta or-denado. Entonces se considera el siguiente caracter en el arreglo. Si estees menor que el primero, se intercambian. Enseguida se considera el tercercaracter en el arreglo. Se intercambia hacia la izquierda hasta que este ensu ordenamiento correcto con los primeros dos caracteres. De esta manerase considera el resto de los caracteres, hasta que el arreglo completo este or-denado. Mezclando la descripcion informal anterior con construcciones deprogramacion, se puede expresar el algoritmo de insercion ordenada como semuestra a continuacion.

Algoritmo InsercionOrdenada(A):Entrada: Un arreglo A de n elementos comparablesSalida: El arreglo A con elementos reacomodados en orden crecientePara i← 1 Hasta n− 1 Hacer

Insertar A[i] en su sitio correcto en A[0], A[1], . . . , A[i− 1].

Esta es una descripcion de alto nivel del insercion ordenada y muestraporque este algoritmo recibe tal nombre—porque cada iteracion inserta elsiguiente elemento en la parte ordenada del arreglo que esta antes de este.Antes de codificar la descripcion se requiere trabajar mas en los detalles decomo hacer la tarea de insercion.

Se reescribira la descripcion de tal forma que se tenga un ciclo anidadodentro de otro. El ciclo exterior considerara cada elemento del arreglo enturno y el ciclo interno movera ese elemento a su posicion correcta con elsubarreglo ordenado de caracteres que estan a su izquierda.

Refinando los detalles para insercion ordenada

Se describe a continuacion una descripcion en nivel intermedio del al-goritmo de insercion ordenada. Esta descripcion esta mas cercana al codigoactual, ya que hace una mejor descripcion para insertar el elemento A[i] en elsubarreglo que esta antes de este. Todavıa emplea una descripcion informalpara el movimiento de los elementos si no estan todavıa ordenados.

Page 17: Notas Estructuras

2.1 Usando arreglos 17

Algoritmo InsercionOrdenada(A):Entrada: Un arreglo A de n elementos comparablesSalida: El arreglo A con elementos reacomodados en orden crecientePara i← 1 Hasta n− 1 Hacer{Insertar A[i] en su sitio correcto en A[0], A[1], . . . , A[i− 1].}actual← A[i]j ← i− 1Mientras j ≥ 0 Y A[j] > actual HacerA[j + 1]← A[j]j ← j − 1

A[j + 1]← actual {actual esta ahora en el lugar correcto}

Descripcion de insercion ordenada con Java

Se proporciona a continuacion un codigo de Java para esta version simpledel algoritmo de insercion ordenada. El codigo proporcionado es para el casoespecial cuando A es un arreglo de caracteres llamado referenciado por a.�

/** Insercion ordenada de un arreglo de caracteres en orden creciente */

public static void insercionOrdenada( char[] a ) {

int n = a.length;

for (int i=1; i<n; i++) { // ındice desde el segundo caracter en a

char actual = a[i]; // el caracter actual a ser insertado

int j = i-1; // iniciar comparando con la celda izq. de i

while ((j>=0) && (a[j]>actual )) // mientras a[j] no esta ordenado con actual

a[ j+1 ] = a[ j-- ]; // mover a[j] a la derecha y decrementar j

a[ j+1 ] = actual; // este es el lugar correcto para actual

}

}�Listado 2.3: Codigo Java para realizar la insercion ordenada de un arreglo decaracteres

2.1.3. Metodos para arreglos y numeros aleatorios

Como los arreglos son muy importantes, Java proporciona un determinadonumero de metodos incorporados para realizar tareas comunes en arreglos.Estos metodos son estaticos en la clase java.util.Arrays. Esto es, ellosestan asociados con la propia clase, y no con alguna instancia particular dela clase.

Page 18: Notas Estructuras

18 Arreglos, listas enlazadas y recurrencia

Algunos metodos simples

Se listan a continuacion algunos metodos simples de la clase java.util.Arraysque no requieren explicacion adicional:

equals(A,B): regresa verdadero si y solo si el arreglo A y el arregloB son iguales. Se considera que dos arreglos son igua-les si estos tienen el mismo numero de elementos y cadapar correspondiente de elementos en los dos arreglos soniguales. Esto es, A y B tienen los mismos elementos enel mismo orden.

fill(A, x): guarda el elemento x en cada celda del arreglo A.sort(A): ordena el arreglo A usando el ordenamiento natural de

sus elementos. Se emplea para ordenar el algoritmo or-denamiento rapido, el cual es mucho mas rapido que lainsercion ordenada.

toString(A): devuelve un String representante del arreglo A.

�import java.util.Arrays;

import java.util.Random;

/** Programa que muestra algunos usos de los arreglos */

public class PruebaArreglo {

public static void main(String [] args) {

int num[] = new int [10];

Random rand = new Random (); // un generador de numeros pseudoaleatorios

rand.setSeed(System.currentTimeMillis ()); // usar el tiempo act. como sem.

// llenar el arreglo num con numeros pseudoaleatorios entre 0 y 99, incl.

for (int i = 0; i < num.length; i++)

num[i] = rand.nextInt (100); // el siguiente numero pseudoaleatorio

int[] ant = (int[]) num.clone (); // clonar el arreglo num

System.out.println("arreglos iguales antes de ordenar: " +

Arrays.equals(ant ,num));

Arrays.sort(num); // ordenar arreglo num (ant esta sin cambios)

System.out.println("arreglos iguales despues de ordenar: " +

Arrays.equals(ant ,num));

System.out.println("ant = " + Arrays.toString(ant));

System.out.println("num = " + Arrays.toString(num));

}

}�Listado 2.4: Programa PruebaArreglo.java que usa varios metodos incorpo-rados en Java

Page 19: Notas Estructuras

2.1 Usando arreglos 19

Un ejemplo usando numeros pseudoaleatorios

El programa PruebaArreglo, listado 2.4, emplea otra caracterıstica enJava—la habilidad para generar numeros pseudoaleatorios, esto es, nume-ros que son estadısticamente aleatorios (pero no realmente aleatorios). Enparticular, este usa un objeto java.util.Random, el cual es un generadorde numeros pseudoaletorios . Tal generador requiere un valor para iniciar, elcual es conocido como su semilla. La secuencia de numeros generados parauna semilla dada sera siempre la misma. En el programa, se pone el valorde la semilla al valor actual del tiempo en milisegundos a partir del 1◦ deenero de 1970, empleando el metodo System.currentTimeMillis, el cualsera diferente cada vez que se ejecute el programa. Una vez que se ha puestoel valor de la semilla, se obtiene repetidamente un numero aleatorio entre 0y 99 llamando al metodo nextInt con el argumento 100. Un ejemplo de lasalida del programa es:

arreglos iguales antes de ordenar: true

arreglos iguales despues de ordenar: false

ant = [49, 41, 8, 10, 48, 87, 52, 2, 97, 81]

num = [2, 8, 10, 41, 48, 49, 52, 81, 87, 97]

Por cierto, hay una leve posibilidad de que los arreglos ant y num per-manezcan igual, aun despues de que num sea ordenado, es decir, si num yaesta ordenado antes de que sea clonado. Pero la probabilidad de que estoocurra es menor que uno en cuatro millones.

Criptografıa simple con cadenas y arreglos de caracteres

Una de las aplicaciones primarias de los arreglos es la representacionde cadenas de caracteres. Los objetos cadena son usualmente representadosinternamente como un arreglo de caracteres. Aun si las cadenas pudieranestar representadas de otra forma, hay una relacion natural entre cadenasy arreglos de caracteres—ambos usan ındices para referirse a sus caracteres.Gracias a esta relacion, Java hace facil la creacion de objetos String desdearreglos de caracteres y viceversa. Para crear un objeto String de un arreglode caracteres A, se usa la expresion,

new String(A)

Page 20: Notas Estructuras

20 Arreglos, listas enlazadas y recurrencia

Esto es, uno de los constructores de la clase String toma un arreglo decaracteres como su argumento y regresa una cadena teniendo los mismoscaracteres en el mismo orden como en el arreglo. De igual forma, dado unString S, se puede crear un arreglo de caracteres de la representacion de Susando la expresion,

S.toCharArray()

En la clase String hay un metodo, toCharArray, el cual regresa un arre-glo, del tipo char[] con los mismos caracteres de S.

El cifrador Cesar

Una area donde se requiere poder cambiar de una cadena a un arreglode caracteres y de regreso es util en criptografıa, la ciencia de los mensa-jes secretos y sus aplicaciones. Esta area estudia varias formas de realizar elencriptamiento, el cual toma un mensaje, denominado el texto plano, y loconvierte en un mensaje cifrado, llamado el ciphertext. Asimismo, la cripto-grafıa tambien estudia las formas correspondientes de realizar el descifrado,el cual toma un ciphertext y lo convierte de regreso en el texto plano original.

Es discutible que el primer esquema de encriptamiento es el cifrado deCesar , el cual es nombrado despues de que Julio Cesar, quien usaba esteesquema para proteger mensajes militares importantes. El cifrador de Cesares una forma simple para oscurecer un mensaje escrito en un lenguaje queforma palabras con un alfabeto.

El cifrador involucra reemplazar cada letra en un mensaje con una letraque esta tres letras despues de esta en el alfabeto para ese lenguaje. Porlo tanto, en un mensaje del espanol, se podrıa reemplazar cada A con D,cada B con E, cada C con F, y ası sucesivamente. Se continua de esta formahasta la letra W, la cual es reemplazada con Z. Entonces, se permite que lasustitucion del patron de la vuelta, por lo que se reemplaza la X con A, Ycon B, y Z con C.

Usando caracteres como ındices de arreglos

Si se fueran a numerar las letras como ındices de arreglos, de tal forma queA sea 0, B sea 1, C sea 2, y ası sucesivamente, entonces se puede escribir laformula del cifrador de Cesar para el alfabeto ingles con la siguiente formula:

Page 21: Notas Estructuras

2.1 Usando arreglos 21

Reemplazar cada letra i con la letra i + 3 mod 26,

donde mod es el operador modulo, el cual regresa el residuo despues de reali-zar una division entera. El operado se denota con % en Java, y es el operadorque se requiere para dar la vuelta en el final del alfabeto. Para el 26 el modu-lo es 0, para el 27 es 1, y el 28 es 2. El algoritmo de desciframiento para elcifrador de Cesar es exactamente lo opuesto—se reemplaza cada letra conuna que esta tres lugares antes que esta, con regreso para la A, B y C.

Se puede capturar esta regla de reemplazo usando arreglos para encriptary descifrar. Porque cada caracter en Java esta actualmente guardado como unnumero—su valor Unicode—se pueden usar letras como ındices de arreglos.Para una letra mayuscula c, por ejemplo, se puede usar c como un ındicede arreglo tomando el valor Unicode de c y restando A. Lo anterior solofunciona para letras mayusculas, por lo que se requerira que los mensajessecretos esten en mayusculas. Se puede entonces usar un arreglo, encriptar,que represente la regla de encriptamiento, por lo que encriptar[i] es la letraque reemplaza la letra numero i, la cual es c − A para una letra mayusculac en Unicode. De igual modo, un arreglo, descrifrar, puede representar laregla de desciframiento, por lo que descifrar[i] es la letra que reemplaza laletra numero i.

En el listado 2.5, se da una clase de Java simple y completa para elcifrador de Cesar, el cual usa la aproximacion anterior y tambien realiza lasconversiones entre cadenas y arreglos de caracteres.�/** Clase para hacer encriptamiento y desciframiento usando el cifrador de

* Cesar. */

public class Cesar {

public static final char[] abc = {’A’,’B’,’C’,’D’,’E’,’F’,’G’,’H’, ’I’,

’J’,’K’,’L’,’M’, ’N’,’O’,’P’,’Q’,’R’,’S’,’T’,’U’,’V’,’W’,’X’,’Y’,’Z’};

protected char[] encriptar = new char[abc.length ]; // Arreglo encriptamiento

protected char[] descifrar = new char[abc.length ]; // Arreglo desciframiento

/** Constructor que inicializa los arreglos de encriptamiento y

* desciframiento */

public Cesar () {

for (int i=0; i<abc.length; i++)

encriptar[i] = abc[(i + 3) % abc.length ]; // rotar alfabeto 3 lugares

for (int i=0; i<abc.length; i++)

descifrar[encriptar[i]-’A’] = abc[i]; // descifrar inverso a encriptar

}

/** Metodo de encriptamiento */

public String encriptar(String secreto) {

char[] mensj = secreto.toCharArray (); // arreglo mensaje

for (int i=0; i<mensj.length; i++) // ciclo de encriptamiento

Page 22: Notas Estructuras

22 Arreglos, listas enlazadas y recurrencia

if (Character.isUpperCase(mensj[i])) // se tiene que cambiar letra

mensj[i] = encriptar[mensj[i] - ’A’]; // usar letra como ındice

return new String(mensj);

}

/** Metodo de desciframiento */

public String descifrar(String secreto) {

char[] mensj = secreto.toCharArray (); // arreglo mensaje

for (int i=0; i<mensj.length; i++) // ciclo desciframiento

if (Character.isUpperCase(mensj[i])) // se tiene letra a cambiar

mensj[i] = descifrar[mensj[i] - ’A’]; // usar letra como ındice

return new String(mensj);

}

/** Metodo main para probar el cifrador de Cesar */

public static void main(String [] args) {

Cesar cifrador = new Cesar (); // Crear un objeto cifrado de Cesar

System.out.println("Orden de Encriptamiento = " +

new String(cifrador.encriptar ));

System.out.println("Orden de Descifrado = " +

new String(cifrador.descifrar ));

String secreto = "ESTRUCTURAS DE DATOS; ARREGLOS Y LISTAS LIGADAS.";

secreto = cifrador.encriptar(secreto );

System.out.println(secreto ); // el texto cifrado

secreto = cifrador.descifrar(secreto );

System.out.println(secreto ); // deberıa ser texto plano

}

}�Listado 2.5: Una clase simple y completa de Java para el cifrador de Cesar

Cuando se ejecuta este programa, se obtiene la siguiente salida:

Orden de Encriptamiento = DEFGHIJKLMNOPQRSTUVWXYZABC

Orden de Descifrado = XYZABCDEFGHIJKLMNOPQRSTUVW

HVWUXFWXUDV GH GDWRV; DUUHJORV B OLVWDV OLJDGDV.

ESTRUCTURAS DE DATOS; ARREGLOS Y LISTAS LIGADAS.

2.1.4. Arreglos bidimensionales y juegos de posicion

Varios juegos de computadora, sean juegos de estrategia, de simulacion,o de conflicto de primera persona, usan un “tablero” bidimensional. Los pro-gramas que manejan juegos de posicion requieren una forma de representarobjetos en un espacio bidimensional. Una forma natural de hacerlo es conun arreglo bidimensional , donde se usan dos ındices, por ejemplo i y j, parareferirse a las celdas en el arreglo. El primer ındice se refiere al numero delrenglon y el segundo al numero de la columna. Dado tal arrreglo se puedenmantener tableros de juego bidimensionales, ası como realizar otros tipos decomputos involucrando datos que estan guardados en renglones y columnas.

Page 23: Notas Estructuras

2.1 Usando arreglos 23

Los arreglos en Java son unidimensionales; se puede usar un solo ındicepara acceder cada celda de un arreglo. Sin embargo, hay una forma como sepueden definir arreglos bidimensionales en Java—se puede crear un arreglobidimensional como un arreglo de arreglos. Esto es, se puede definir un arreglode dos dimensiones para que sea un arreglo con cada una de sus celdas siendootro arreglo. Tal arreglo de dos dimensiones es a veces llamado una matriz .En Java, se declara un arreglo de dos dimensiones como se muestra en elsiguiente ejemplo:

int[][] Y = new int[8][10];

La sentencia crea una “arreglo de arreglos” bidimensional, Y, el cual es8× 10, teniendo ocho renglones y diez columnas. Es decir, Y es un arreglo delongitud ocho tal que cada elemento de Y es un arreglo de longitud de diezenteros. Lo siguiente podrıa entonces ser usos validos del arreglo Y y siendoi y j variables tipo int:

Y[i][i+1] = Y[i][i] + 3; i = a.length; j = Y[4].length;

Se revisa una aplicacion de un arreglo bidimensional implementando unjuego simple posicional.

El gato

Es un juego que se practica en un tablero de tres por tres. Dos jugadores—X y O—alternan en colocar sus respectivas marcas en las celdas del tablero,iniciando con el jugador X. Si algun jugado logra obtener tres marcas suyasen un renglon, columna o diagonal, entonces ese jugador gana.

La idea basica es usar un arreglo bidimensional, tablero, para mantenerel tablero del juego. Las celdas en este arreglo guardan valores para indicarsi la celda esta vacıa o guarda una X o una O. Entonces, el tablero es unamatriz de tres por tres, donde por ejemplo, el renglon central son las celdastablero[1][0], tablero[1][1], tablero[1][2]. Para este caso, se deci-dio que las celdas en el arreglo tablero sean enteros, con un cero indicandouna celda vacıa, un uno indicando una X, y un -1 indicando O. Esta codifi-cacion permite tener una forma simple de probar si en una configuracion deltablero es una victoria para X o para O, a saber, si los valores de un renglon,columna o diagonal suman -3 o 3.

Page 24: Notas Estructuras

24 Arreglos, listas enlazadas y recurrencia

Se da una clase completa de Java para mantener un tablero del gato parados jugadores en el listado 2.6. El codigo es solo para mantener el tablerodel gato y para registrar los movimientos; no realiza ninguna estrategia, nipermite jugar al gato contra la computadora.�/** Simulacion del juego del gato (no tiene ninguna estrategia ). */

public class Gato {

protected static final int X = 1, O = -1; // jugadores

protected static final int VACIA = 0; // celda vacıa

protected int tablero [][] = new int [3][3]; // tablero

protected int jugador; // jugador actual

/** Constructor */

public Gato() { limpiarTablero (); }

/** Limpiar el tablero */

public void limpiarTablero () {

for (int i = 0; i < 3; i++)

for (int j = 0; j < 3; j++)

tablero[i][j] = VACIA; // cada celda debera estar vacıa

jugador = X; // el primer jugador es ’X’

}

/** Poner una marca X u O en la posicion i,j */

public void ponerMarca(int i, int j) throws IllegalArgumentException {

if ( (i < 0) || (i > 2) || (j < 0) || (j > 2) )

throw new IllegalArgumentException("Posicion invalida en el tablero");

if ( tablero[i][j] != VACIA )

throw new IllegalArgumentException("Posicion del tablero ocupada");

tablero[i][j] = jugador; // colocar la marca para el jugador actual

jugador =- jugador; // cambiar jugadores (usar el hecho de que O=-X)

}

/** Revisar si la configuracion del tablero es ganadora para un jugador dado*/

public boolean esGanador(int marca) {

return (( tablero [0][0] + tablero [0][1] + tablero [0][2] == marca *3) // ren. 0

|| (tablero [1][0] + tablero [1][1] + tablero [1][2] == marca *3) // ren. 1

|| (tablero [2][0] + tablero [2][1] + tablero [2][2] == marca *3) // ren. 2

|| (tablero [0][0] + tablero [1][0] + tablero [2][0] == marca *3) // col. 0

|| (tablero [0][1] + tablero [1][1] + tablero [2][1] == marca *3) // col. 1

|| (tablero [0][2] + tablero [1][2] + tablero [2][2] == marca *3) // col. 2

|| (tablero [0][0] + tablero [1][1] + tablero [2][2] == marca *3) // diag.

|| (tablero [2][0] + tablero [1][1] + tablero [0][2] == marca *3)); // diag.

}

/** Regresar el jugador ganador o 0 para indicar empate */

public int ganador () {

if (esGanador(X))

return(X);

else if (esGanador(O))

return(O);

else

return (0);

}

Page 25: Notas Estructuras

2.1 Usando arreglos 25

/** Regresar una cadena simple mostrando el tablero actual */

public String toString () {

String s = "";

for (int i=0; i<3; i++) {

for (int j=0; j<3; j++) {

switch (tablero[i][j]) {

case X: s += "X"; break;

case O: s += "O"; break;

case VACIA: s += " "; break;

}

if (j < 2) s += "|"; // lımite de columna

}

if (i < 2) s += "\n-----\n"; // lımite de renglon

}

return s;

}

/** Ejecucion de prueba de un juego simple */

public static void main(String [] args) {

Gato juego = new Gato ();

/* mueve X: */ /* mueve O: */

juego.ponerMarca (1 ,1); juego.ponerMarca (0,2);

juego.ponerMarca (2 ,2); juego.ponerMarca (0,0);

juego.ponerMarca (0 ,1); juego.ponerMarca (2,1);

juego.ponerMarca (1 ,2); juego.ponerMarca (1,0);

juego.ponerMarca (2 ,0);

System.out.println( juego.toString () );

int jugadorGanador = juego.ganador ();

if (jugadorGanador != 0)

System.out.println(jugadorGanador + " gana");

else

System.out.println("Empate");

}

}�Listado 2.6: Una clase simple y completa para jugar gato entre dos jugadores.

Se muestra en la figura 2.1 el ejemplo de salida de la clase Gato.

O|X|O

-----

O|X|X

-----

X|O|X

Empate

Figura 2.1: Salida del ejemplo del juego del gato

Page 26: Notas Estructuras

26 Arreglos, listas enlazadas y recurrencia

2.2. Listas simples enlazadas

Los arreglos son elegantes y simples para guardar cosas en un cierto orden,pero tienen el inconveniente de no ser muy adaptables, ya que se tiene quefijar el tamano del arreglo por adelantado.

Hay otras formas de guardar una secuencia de elementos, que no tiene elinconveniente de los arreglos. En esta seccion se explora una implementacionalterna, la cual es conocida como lista simple enlazada.

Una lista enlazada, en su forma mas simple, es una coleccion de nodosque juntos forman un orden lineal. El ordenamiento esta determinado de talforma que cada nodo es un objeto que guarda una referencia a un elementoy una referencia, llamado siguiente, a otro nodo.

Podrıa parecer extrano tener un nodo que referencie a otro nodo, pero talesquema trabaja facilmente. La referencia sig dentro de un nodo puede servista como un enlace o apuntador a otro nodo. De igual nodo, moverse de unnodo a otro siguiendo una referencia a sig es conocida como salto de enlaceo salto de apuntador. El primer nodo y el ultimo son usualmente llamados lacabeza y la cola de la lista, respectivamente. Ası, se puede saltar a traves dela lista iniciando en la cabeza y terminando en la cola. Se puede identificarla cola como el nodo que tenga la referencia sig como null, la cual indica elfinal de la lista. Una lista enlazada definida de esta forma es conocida comouna lista simple enlazada.

Como en un arreglo, una lista simple enlazada guarda sus elementos enun cierto orden. Este orden esta determinado por la cadenas de enlaces sig

yendo desde cada nodo a su sucesor en la lista. A diferencia de un arreglo, unalista simple enlazada no tiene un tamano fijo predeterminado, y usa espacioproporcional al numero de sus elementos. Asimismo, no se emplean numerosındices para los nodos en una lista enlazada. Por lo tanto, no se puede decirsolo por examinar un nodo si este es el segundo, quinto u otro nodo en lalista.

Implementacion de una lista simple enlazada

Para implementar una lista simple enlazada, se define una clase Nodo,como se muestra en el listado 2.7, la cual indica el tipo de los objetos guar-dados en los nodos de la lista. Para este caso se asume que los elementos soncadenas de caracteres. Tambien es posible definir nodos que puedan guardartipos arbitrarios de elementos. Dada la clase Nodo, se puede definir una clase,

Page 27: Notas Estructuras

2.2 Listas simples enlazadas 27

ListaSimple, mostrada en el listado 2.8, definiendo la lista enlazada actual.Esta clase guarda una referencia al nodo cabeza y una variable va contandoel numero total de nodos.�/** Nodo de una lista simple enlazada de cadenas. */

public class Nodo {

private String elemento; // Los elementos son cadenas de caracteres

private Nodo sig;

/** Crea un nodo con el elemento dado y el nodo sig. */

public Nodo(String s, Nodo n) {

elemento = s;

sig = n;

}

/** Regresa el elemento de este nodo. */

public String getElement () { return elemento; }

/** Regresa el nodo siguiente de este nodo. */

public Nodo getSig () { return sig; }

// metodos modificadores

/** Poner el elemento de este nodo. */

public void setElement(String nvoElem) { elemento = nvoElem; }

/** Poner el nodo sig de este nodo. */

public void setSig(Nodo nvoSig) { sig = nvoSig; }

}�Listado 2.7: Implementacion de un nodo de una lista simple enlazada

�/** Lista simple enlazada. */

public class ListaSimple {

protected Nodo cabeza; // nodo cabeza de la lista

protected long tam; // cantidad de nodos en la lista

/** constructor por defecto que crea una lista vacıa */

public ListaSimple () {

cabeza = null;

tam = 0;

}

// ... metodos de actualizacion y busqueda deberıan ir aquı

}�Listado 2.8: Implementacion parcial de una lista simple enlazada

Page 28: Notas Estructuras

28 Arreglos, listas enlazadas y recurrencia

2.2.1. Insercion en una lista simple enlazada

Cuando se usa una lista simple enlazada, se puede facilmente insertarun elemento en la cabeza de la lista. La idea principal es que se cree unnuevo nodo, se pone su enlace siguiente para que se refiera al mismo objetoque la cabeza, y entonces se pone que la cabeza apunte al nuevo nodo. Semuestra enseguida como insertar un nuevo nodo v al inicio de una lista simpleenlazada. Este metodo trabaja aun si la lista esta vacıa. Observar que se hapuesto el apuntador siguiente para el nuevo nodo v antes de hacer que lavariable cabeza apunte a v.

Algoritmo agregarInicio(v):v.setSiguiente(cabeza) { hacer que v apunte al viejo nodo cabeza }cabeza← v { hacer que la variable cabeza apunte al nuevo nodo }tam← tam + 1 {incrementar la cuenta de nodos }

2.2.2. Insercion de un elemento en la cola

Se puede tambien facilmente insertar un elemento en la cola de la lista,cuando se ha proporcionado una referencia al nodo cola. En este caso, secrea un nuevo nodo, se asigna su referencia siguiente para que apunte alobjeto null, se pone la referencia siguiente de la cola para que apunte a estenuevo objeto, y entonces se asigna a la referencia cola este nuevo nodo. Elsiguiente algoritmo inserta un nuevo nodo al final de la lista simple enlazada.El metodo tambien trabaja si la lista esta vacıa. Se pone primero el apuntadorsiguiente para el viejo nodo cola antes de hacer que la variable cola apunteal nuevo nodo.

Algoritmo agregarFinal(v):v.setSiguiente(null) { hacer que el nuevo nodo v apunte al objeto null }cola.setSiguiente(v) { el viejo nodo cola apuntara al nuevo nodo }cola← v { hacer que la variable cola apunte al nuevo nodo }tam← tam + 1 {incrementar la cuenta de nodos }

Page 29: Notas Estructuras

2.3 Listas doblemente enlazadas 29

2.2.3. Quitar un elemento de una lista simple enlazada

La operacion contraria de insertar un nuevo elemento en la cabeza de unalista enlazada es quitar un elemento en la cabeza. En el algoritmo siguientese muestra la forma de realizar lo anterior.

Algoritmo removerPrimero():Si cabeza = null Entonces

Indicar un error: la lista esta vacıa.t← cabezacabeza← cabeza.getSiguiente() { cabeza apuntara al siguiente nodo }t.setSiguiente(null) { anular el apuntador siguiente del nodo borrado.}tam← tam− 1 { decrementar la cuenta de nodos }

Desafortunadamente, no se puede facilmente borrar el nodo cola de unalista simple enlazada. Aun si se tiene una referencia cola directamente alultimo nodo de la lista, se necesita poder acceder el nodo anterior al ultimonodo en orden para quitar el ultimo nodo. Pero no se puede alcanzar el nodoanterior a la cola usando los enlaces siguientes desde la cola. La unica formapara acceder este nodo es empezar de la cabeza de la lista y llegar a traves dela lista. Pero tal secuencia de operaciones de saltos de enlaces podrıa tomarun tiempo grande.

2.3. Listas doblemente enlazadas

Como se comentaba en la seccion previa, quitar un elemento de la cola deuna lista simple enlazada no es facil. Ademas, el quitar cualquier otro nodode la lista diferente de la cabeza de una lista simple enlazada es consumidorde tiempo, ya que no se tiene una forma rapida de acceder al nodo que sedesea quitar. Por otra parte, hay varias aplicaciones donde no se tiene unacceso rapido tal como el acceso al nodo predecesor. Para tales aplicaciones,serıa bueno que contaran con una forma de ir en ambas direcciones en unalista enlazada.

Hay un tipo de lista enlazada que permite ir en ambas direcciones—haciaadelante y hacia atras—en una lista enlazada. Esta es la lista doblementeenlazada. Tal lista permite una gran variedad de operaciones rapidas de ac-tualizacion, incluyendo la insercion y el borrado en ambos extremos, y en el

Page 30: Notas Estructuras

30 Arreglos, listas enlazadas y recurrencia

centro. Un nodo en una lista doblemente enlazada guarda dos referencias—un enlace sig, el cual apunta al siguiente nodo en la lista, y un enlace prev,el cual apunta al nodo previo en la lista.

Una implementacion de un nodo de una lista doblemente enlazada semuestra en el listado 2.9, donde nuevamente se asume que los elementos soncadenas de caracteres.�/** Nodo de una lista doblemente enlazada de una lista de cadenas */

public class NodoD {

protected String elemento; // Cadena elemento guardada por un nodo

protected NodoD sig , prev; // Apuntadores a los nodos siguiente y previo

/** Constructor que crea un nodo con los campos dados */

public NodoD(String e, NodoD p, NodoD s) {

elemento = e;

prev = p;

sig = s;

}

/** Regresa el elemento de este nodo */

public String getElemento () { return elemento; }

/** Regresa el nodo previo de este nodo */

public NodoD getPrev () { return prev; }

/** Regresa el nodo siguiente de este nodo */

public NodoD getSig () { return sig; }

/** Pone el elemento de este nodo */

public void setElemento(String nvoElem) { elemento = nvoElem; }

/** Pone el nodo previo de este nodo */

public void setPrev(NodoD nvoPrev) { prev = nvoPrev; }

/** Pone el nodo siguiente de este nodo */

public void setSig(NodoD nvoSig) { sig = nvoSig; }

}�Listado 2.9: Clase NodoD representando un nodo de una lista doblementeenlazada que guarda una cadena de caracteres.

Centinelas encabezado y terminacion

Para simplificar la programacion, es conveniente agregar nodos especialesen los extremos de lista doblemente enlazada: un nodo encabezado o headerjusto antes de la cabeza de la lista, y un nodo terminacion o trailer justodespues de la cola de la lista. Estos nodos “falsos” o centinelas no guardanningun elemento. El encabezado tiene una referencia sig valida pero unareferencia prev nula, mientras terminacion tiene una referencia prev validapero una referencia sig nula. Un objeto lista enlazada podrıa simplementenecesitar guardar referencias a estos dos centinelas y un contador tam queguarde el numero de elementos, sin contar los centinelas, en la lista.

Page 31: Notas Estructuras

2.3 Listas doblemente enlazadas 31

Insertar o remover elementos en cualquier extremo de la lista doblementeenlazada se hace directo. Ademas, el enlace prev elimina la necesidad derecorrer la lista para obtener el nodo que esta antes de la cola. Se muestralos detalles de la operacion remover el ultimo en el siguiente algoritmo.

Algoritmo removerUltimo():Si tam = 0 Entonces

Indicar un error: la lista esta vacıa.v ← terminacion.getPrev() { ultimo nodo }u← v.getPrev() { nodo anterior al ultimo nodo }terminacion.setPrev(u)u.setSig(terminacion)v.setPrev(null)v.setSig(null)tam← tam− 1 { decrementar la cuenta de nodos }

De igual modo, se puede facilmente hacer una insercion de un nuevoelemento al inicio de una lista doblemente enlazada, como se muestra en elsiguiente algoritmo donde se inserta un nuevo nodo v al inicio. La variabletam guarda la cantidad de elementos en la lista. Este algoritmo tambientrabaja con una lista vacıa.

Algoritmo agregarInicio(v):w ← encabezado.getSig() { primer nodo }v.setSig(w)v.setPrev(encabezado)w.setPrev(v)encabezado.setSig(v)tam← tam + 1

2.3.1. Insercion entre los extremos de una lista dobleenlazada

Las listas dobles enlazadas no nada mas son utiles para insertar y re-mover elementos en la cabeza o en la cola de la lista. Estas tambien sonconvenientes para mantener una lista de elementos mientras se permite in-

Page 32: Notas Estructuras

32 Arreglos, listas enlazadas y recurrencia

sercion y remocion en el centro de la lista. Dado un nodo v de una lista dobleenlazada, el cual posiblemente podrıa ser la cabecera pero no la terminacion,se puede facilmente insertar un nuevo nodo z inmediatamente despues dev. Especıficamente, sea w el siguiente nodo de v. Se ejecutan los siguientespasos.

1. Hacer que el enlace prev de z se refiera a v.

2. Hacer que el enlace sig de z se refiera a w.

3. Hacer que el enlace prev de w se refiera a z.

4. Hacer que el enlace sig de v se refiera a z.

Este metodo esta dado en detalle en el siguiente algoritmo. Recordandoel uso de los centinelas cabecera y terminacion, este algoritmo trabaja aunsi v es el nodo cola, el nodo que esta justo antes del centinela terminacion.

Algoritmo agregarDespues(v, z):w ← v.getSig() { nodo despues de v }z.setPrev(v) { enlazar z a su predecesor, v }z.setSig(w) { enlazar z a su sucesor, w }w.setPrev(z) { enlazar w a su nuevo predecesor, z }w.setSig(z) { enlazar v a su nuevo sucesor, z }tam← tam + 1

2.3.2. Remover en el centro de una lista doblementeenlazada

De igual modo, es facil remover un nodo v intermedio de una lista do-blemente enlazada. Se acceden los nodos u y w a ambos lados de v usandosus metodos getPrev y getSig, estos nodos deberan existir, ya que se estanusando centinelas. Para quitar el nodo v, se tiene que hacer que u y w seapunten entre ellos en vez de hacerlo hacia v. Esta operacion es referidacomo desenlazar a v. Tambien se anulan los apuntadores prev y sig de v

para que ya no retengan referencias viejas en la lista. El siguiente algoritmomuestra como remover el nodo v aun si v es el primer nodo, el ultimo o unnodo no centinela.

Page 33: Notas Estructuras

2.3 Listas doblemente enlazadas 33

Algoritmo remover(v):u← v.getPrev() { nodo predecesor a v }w ← v.getSig() { nodo sucesor a v }w.setPrev(u) { desenlazar v }u.setSig(w)v.setPrev(null) { anular los campos de v }v.setNext(null) tam← tam− 1 { decrementar el contador de nodos }

2.3.3. Una implementacion de una lista doblementeenlazada

En el listado 2.10 se muestra una implementacion de una lista doblementeenlazada con nodos que guardan cadenas de caracteres como elementos. Sehacen las siguientes observaciones acerca de la clase ListaDoble.

Los objetos de la clase NodoD, los cuales guardan elementos String, sonusados para todos los nodos de la lista, incluyendo los nodos centinelacabecera y terminacion.

Se puede usar la clase ListaDoble para una lista doblemente enlazadade objetos String solamente. Para construir una lista de otros tipos,se puede usar una declaracion generica.

Los metodos getPrimero y getUltimo dan acceso directo al primernodo y el ultimo en la lista.

Los metodos getPrev y getSig permiten recorrer la lista.

Los metodos tienePrev y tieneSig detectan los lımites de la lista.

Los metodos agregarInicio y agregarFinal agregan un nuevo nodoal inicio y al final de la lista, respectivamente.

Los metodos agregarAntes y agregarDespues agregan un nodo antesy despues de un nodo existente, respectivamente.

Teniendo solamente un solo metodo para borrar, remover, no es actual-mente una restriccion, ya que se puede remover al inicio o al final de una

Page 34: Notas Estructuras

34 Arreglos, listas enlazadas y recurrencia

lista L doblemente enlazada ejecutando L.remover(L.getPrimero())

o L.remover(L.getUltimo()), respectivamente.

El metodo toString para convertir una lista entera en una cadena esutil para propositos de prueba y depuracion.�

/** Lista doblemente enlazada con nodos de tipo NodoD guardando cadenas. */

public class ListaDoble {

protected int tam; // numero de elementos

protected NodoD cabecera , terminacion; // centinelas

/** Constructor que crea una lista vacia */

public ListaDoble () {

tam = 0;

cabecera = new NodoD(null , null , null); // crer cabecera

terminacion = new NodoD(null , cabecera , null); // crear terminacion

cabecera.setSig(terminacion ); // cabecera apunta a terminacion

}

/** Regresa la cantidad de elementos en la lista */

public int tam() { return tam; }

/** Indica si la lista esta vacıa */

public boolean estaVacia () { return (tam == 0); }

/** Regresa el primer nodo de la lista */

public NodoD getPrimero () throws IllegalStateException {

if (estaVacia ()) throw new IllegalStateException("La lista esta vacıa");

return cabecera.getSig ();

}

/** Regresa el ultimo nodo de la lista */

public NodoD getUltimo () throws IllegalStateException {

if (estaVacia ()) throw new IllegalStateException("La lista esta vacıa");

return terminacion.getPrev ();

}

/** Regresa el nodo anterior al nodo v dado. Un error ocurre si v

* es la cabecera */

public NodoD getPrev(NodoD v) throws IllegalArgumentException {

if (v == cabecera) throw new IllegalArgumentException

("No se puede mover hacia atras de la cabecera de la lista");

return v.getPrev ();

}

/** Regresa el nodo siguiente al nodo v dado. Un error ocurre si v

* es la terminacion */

public NodoD getSig(NodoD v) throws IllegalArgumentException {

if (v == terminacion) throw new IllegalArgumentException

("No se puede mover hacia adelante de la terminacion de la lista");

return v.getSig ();

}

/** Inserta el nodo z dado antes del nodo v dado. Un error

Page 35: Notas Estructuras

2.3 Listas doblemente enlazadas 35

* ocurre si v es la cabecera */

public void agregarAntes(NodoD v, NodoD z) throws IllegalArgumentException {

NodoD u = getPrev(v); // podrıa lanzar un IllegalArgumentException

z.setPrev(u);

z.setSig(v);

v.setPrev(z);

u.setSig(z);

tam ++;

}

/** Inserta el nodo z dado despues del nodo v dado. Un error

* ocurre si v es la cabecera */

public void agregarDespues(NodoD v, NodoD z) {

NodoD w = getSig(v); // podrıa lanzar un IllegalArgumentException

z.setPrev(v);

z.setSig(w);

w.setPrev(z);

v.setSig(z);

tam ++;

}

/** Inserta el nodo v dado en la cabeza de la lista */

public void agregarInicio(NodoD v) {

agregarDespues(cabecera , v);

}

/** Inserta el nodo v dado en la cola de la lista */

public void agregarFinal(NodoD v) {

agregarAntes(terminacion , v);

}

/** Quitar el nodo v dado de la lista. Un error ocurre si v es

* la cabecera o la terminacion */

public void remover(NodoD v) {

NodoD u = getPrev(v); // podrıa lanzar IllegalArgumentException

NodoD w = getSig(v); // podrıa lanzar IllegalArgumentException

// desenlazar el nodo v de la lista

w.setPrev(u);

u.setSig(w);

v.setPrev(null);

v.setSig(null);

tam --;

}

/** Regresa si un nodo v dado tiene un nodo previo */

public boolean tienePrev(NodoD v) { return v != cabecera; }

/** Regresa si un nodo v dado tiene un nodo siguiente */

public boolean tieneSig(NodoD v) { return v != terminacion; }

/** Regresa una cadena representando la lista */

public String toString () {

String s = "[";

NodoD v = cabecera.getSig ();

while (v != terminacion) {

s += v.getElemento ();

v = v.getSig ();

Page 36: Notas Estructuras

36 Arreglos, listas enlazadas y recurrencia

if (v != terminacion)

s += ",";

}

s += "]";

return s;

}

}�Listado 2.10: Una clase de lista doblemente enlazada.

2.4. Listas circulares y ordenamiento

En esta seccion se revisan algunas aplicaciones y extensiones de listasenlazadas.

2.4.1. Listas circularmente enlazadas

El juego de ninos, “Pato, Pato, Ganso”, es jugado en varias culturas. Unavariacion de la lista simple enlazada, llamada la lista circularmente enlazada,es usada para una variedad de aplicaciones involucrando juegos circulares,como el juego que se comenta. Se muestra a continuacion este tipo de lista ysu aplicacion en un juego circular.

Una lista circularmente enlazada tiene el mismo tipo de nodos que unalista simple enlazada. Esto es, cada nodo en una lista circularmente enlazadatiene un apuntador siguiente y una referencia a un elemento. Pero no hayuna cabeza o cola en la lista circularmente enlazada. En vez de tener queel apuntador del ultimo nodo sea null, en una lista circularmente enlazada,este apunta de regreso al primer nodo. Por lo tanto, no hay primer nodo oultimo. Si se recorren los nodos de una lista circularmente enlazada desdecualquier nodo usando los apuntadores sig, se ciclara a traves de los nodos.

Aun cuando una lista circularmente enlazada no tiene inicio o termina-cion, no obstante se necesita que algun nodo este marcado como especial,el cual sera llamado el cursor. El nodo cursor permite tener un lugar parainiciar si se requiere recorrer una lista circularmente inversa. Y si se recuerdaesta posicion inicial, entonces tambien se puede saber cuando se haya termi-nado con un recorrido en la lista circularmente enlazada, que es cuando seregresa al nodo que fue el nodo cursor cuando se inicio.

Se pueden entonces definir algunos metodos simples de actualizacion parauna lista circularmente ligada:

Page 37: Notas Estructuras

2.4 Listas circulares y ordenamiento 37

agregar(v): inserta un nuevo nodo v inmediatamente despues del cur-sor; si la lista esta vacıa, entonces v se convierte en elcursor y su apuntador sig apunta a el mismo.

remover(): borra y regresa el nodo v inmediatamente despues delcursor (no el propio cursor, al menos que este sea el uniconodo); si la lista queda vacıa, el cursor es puesto a null.

avanzar(): avanza el cursor al siguiente nodo en la lista.

En el listado 2.11, se muestra una implementacion en Java de una listacircularmente enlazada, la cual usa la clase nodo del listado 2.7 e incluyetambien un metodo toString para generar una representacion de cadena dela lista.�/** Lista circularmente enlazada con nodos de tipo Nodo guardando cadenas. */

public class ListaCircular {

protected Nodo cursor; // el cursor actual

protected int tam; // la cantidad de nodos en la lista

/** Constructor que crea una lista vacia */

public ListaCircular () { cursor = null; tam = 0; }

/** Regresa la cantidad de nodos */

public int tam() { return tam; }

/** Regresa el cursor */

public Nodo getCursor () { return cursor; }

/** Mueve el cursor hacia adelante */

public void avanzar () { cursor = cursor.getSig (); }

/** Agregar un nodo despues del cursor */

public void agregar(Nodo nvoNodo) {

if (cursor == null) { // ¿la lista esta vacia?

nvoNodo.setSig(nvoNodo );

cursor = nvoNodo;

}

else {

nvoNodo.setSig(cursor.getSig ());

cursor.setSig(nvoNodo );

}

tam ++;

}

/** Quitar el nodo despues del cursor */

public Nodo remover () {

Nodo vjoNodo = cursor.getSig (); // el nodo siendo removido

if ( vjoNodo == cursor )

cursor = null; // la lista se esta vaciando

Page 38: Notas Estructuras

38 Arreglos, listas enlazadas y recurrencia

else {

cursor.setSig( vjoNodo.getSig () ); // desenlazando viejo nodo

vjoNodo.setSig( null );

}

tam --;

return vjoNodo;

}

/** Regresar una representacion String de la lista ,

* iniciando desde el cursor */

public String toString () {

if (cursor == null) return "[]";

String s = "[..." + cursor.getElement ();

Nodo oldCursor = cursor;

for ( avanzar (); oldCursor != cursor; avanzar () )

s += ", " + cursor.getElement ();

return s + "...]";

}

}�Listado 2.11: Una clase de lista circularmente ligada con nodos simples.

Algunas observaciones acerca de la clase ListaCircular

Es un programa simple que puede dar suficiente funcionalidad para simu-lar juegos circulares, como Pato, Pato, Ganso, como se ve enseguida. No es unprograma robusto, sin embargo. En particular, si una lista circular esta vacıa,entonces llamar avanzar o remover en esa lista causara una excepcion.

Pato, Pato, Ganso

En el juego de ninos Pato, Pato, Ganso, un grupo de ninos se sienta encırculo. Uno de ellos es ellos es elegido para quedarse parado y este caminaalrededor del cırculo por fuera. El nino elegido toca a cada nino en la cabeza,diciendo “Pato” o “Ganso”. Cuando el elegido decide llamar a alguien el“Ganso”, el “Ganso” se levanta y persigue al otro nino alrededor del cırculo.Si el “Ganso” no logra tocar al elegido, entonces a el le toca quedarse parala proxima ronda, sino el elegido lo seguira siendo en la siguiente ronda. Eljuego continua hasta que los ninos se aburran o un adulto les llame paracomer un refrigerio.

Simular este juego es una aplicacion ideal de una lista circularmente enla-zada. Los ninos pueden representar nodos en la lista. El nino “elegido” puedeser identificado como el nino que esta sentado despues del cursor, y puedeser removido del cırculo para simular la marcha alrededor. Se puede avanzarel cursor con cada “Pato” que el elegido identifique, lo cual se puede simular

Page 39: Notas Estructuras

2.4 Listas circulares y ordenamiento 39

con una decision aleatoria. Una vez que un “Ganso” es identificado, se puederemover este nodo de la lista, hacer una seleccion aleatoria para simular si el“Ganso” alcanzo al elegido, e insertar el ganador en la lista. Se puede enton-ces avanzar el cursor e insertar al ganador y repetir el proceso o termina sieste es la ultima partida del juego.

Usando una lista circularmente enlazada para simular Pato, Pato,Ganso

Se da un codigo de Java para simular el juego Pato, Pato, Ganso en ellistado 2.12.�import java.util.Random;

public class PatoPatoGanso {

/** Simulacion de Pato , Pato , Ganso con una lista circularmente enlazada. */

public static void main(String [] args) {

ListaCircular C = new ListaCircular ();

int N = 3; // numero de elegeraciones del juego

Nodo eleg; // el jugador que es el elegido

Nodo ganso; // el ganso

Random rand = new Random ();

// usar el tiempo actual como semilla

rand.setSeed( System.currentTimeMillis () );

// Los jugadores ...

String [] noms = {"Alex","Eli","Paco","Gabi","Pepe","Luis","To~no","Pedro"};

for ( String nombre: noms ) {

C.agregar( new Nodo( nombre , null) );

C.avanzar ();

}

System.out.println("Los jugadores son: "+C.toString ());

for (int i = 0; i < N; i++) { // jugar Pato , Pato , Ganso N veces

eleg = C.remover ();

System.out.println(eleg.getElement () + " es elegido.");

System.out.println("Jugando Pato , Pato , Ganso con: "+C.toString ());

// marchar alrededor del circulo

while (rand.nextBoolean () || rand.nextBoolean ()) {

C.avanzar (); // avanzar con probabilidad 3/4

System.out.println( C.getCursor (). getElement () + " es un Pato.");

}

ganso = C.remover ();

System.out.println( "¡"+ganso.getElement () + " es el ganso!" );

if ( rand.nextBoolean () ) {

System.out.println("¡El ganso gano!");

C.agregar(ganso); // poner al ganso de regreso en su lugar anterior

C.avanzar (); // ahora el cursor esta sobre el ganso

C.agregar(eleg); // El "elegido" seguira siendolo en la proxima ronda

}

Page 40: Notas Estructuras

40 Arreglos, listas enlazadas y recurrencia

else {

System.out.println("¡El ganso perdio !");

C.agregar(eleg); // poner al elegido en el lugar del ganso

C.avanzar (); // ahora el cursor esta sobre la persona elegida

C.agregar(ganso); // El ganso ahora es el elegido en la siguiente ronda

}

}

System.out.println("El circulo final es " + C.toString ());

}

} // fin de la clase PatoPatoGanso�Listado 2.12: El metodo main de un programa que usa una lista circularmenteenlazada para simular el juego de ninos Pato–Pato–Ganso.

Se muestra un ejemplo de la salida de la ejecucion del programa Pato,Pato, Ganso a continuacion:

Los jugadores son: [...Pedro, Alex, Eli, Paco, Gabi, Pepe, Luis, To~no...]

Alex es elegido.

Jugando Pato, Pato, Ganso con: [...Pedro, Eli, Paco, Gabi, Pepe, Luis, To~no...]

Eli es un Pato.

Paco es el ganso!

El ganso perdio!

Paco es elegido.

Jugando Pato, Pato, Ganso con: [...Alex, Gabi, Pepe, Luis, To~no, Pedro, Eli...]

Gabi es un Pato.

Pepe es un Pato.

Luis es un Pato.

To~no es un Pato.

Pedro es un Pato.

Eli es un Pato.

Alex es un Pato.

Gabi es un Pato.

Pepe es un Pato.

Luis es un Pato.

To~no es un Pato.

Pedro es un Pato.

Eli es el ganso!

El ganso gano!

Paco es elegido.

Jugando Pato, Pato, Ganso con: [...Eli, Alex, Gabi, Pepe, Luis, To~no, Pedro...]

Alex es un Pato.

Gabi es el ganso!

El ganso gano!

El circulo final es [...Gabi, Paco, Pepe, Luis, To~no, Pedro, Eli, Alex...]

Observar que cada iteracion en esta ejecucion del programa genera unasalida diferente, debido a las configuraciones iniciales diferentes y el uso de

Page 41: Notas Estructuras

2.4 Listas circulares y ordenamiento 41

opciones aleatorias para identificar patos y ganso. Asimismo, si el ganso al-canza al ganso depende de opciones aleatorias.

2.4.2. Ordenando una lista enlazada

Se muestra a continuacion el algoritmo de insercion ordenada para unalista doblemente enlazada.

Algoritmo InsercionOrdenada(L):Entrada: Una lista L doblemente enlazada de elementos comparablesSalida: La lista L con elementos rearreglados en orden crecienteSi L.tam() ≤ 1 Entoncesregresar

fin← L.getPrimero()Mientras fin no sea el ultimo nodo en L Hacerpivote← fin.getSig()Quitar pivote de Lins← finMientras ins no sea la cabecera y el elemento de ins sea mayor

que el del pivote Hacerins← ins.getPrev()

Agregar pivote justo despues de ins en LSi ins = fin Entonces { Se agrego pivote despues de fin en este caso}fin← fin.getSig()

Una implentacion en Java del algoritmo de insercion ordenada para unalista doblemente enlazada representada por la clase ListaDoble se muestraen el listado 2.13.�

/** Insercion ordenada para una lista doblemente enlazada

de la clase ListaDoble. */

public static void ordenar(ListaDoble L) {

if (L.tam() <= 1) return; // L ya esta ordenada en este caso

NodoD pivote; // nodo pivote

NodoD ins; // punto de insercion

NodoD fin = L.getPrimero (); // fin de la corrida

while (fin != L.getUltimo ()) {

pivote = fin.getSig (); // obtener el siguiente nodo de pivote

L.remover(pivote ); // quitar pivote de L

ins = fin; // iniciar busqueda desde fin de la corrida ordenada

Page 42: Notas Estructuras

42 Arreglos, listas enlazadas y recurrencia

while (L.tienePrev(ins) &&

ins.getElemento (). compareTo(pivote.getElemento ()) > 0)

ins = ins.getPrev (); // mover a la izquierda

// agregar el pivote de regreso , despues del punto de insercion

L.agregarDespues(ins ,pivote );

if (ins == fin) // se ha agregado pivote despues de fin (se quedo

fin = fin.getSig (); // en el mismo lugar) -> incrementar marcador fin

}

}�Listado 2.13: Ordenamiento por insercion para una lista doblemente enlazadade la clase ListaDoble.

2.5. Recurrencia

La repeticion se puede lograr escribiendo ciclos, como por ejemplo confor o con while. Otra forma de lograr la repeticion es usando recurrencia,la cual ocurre cuando una funcion se llama a sı misma. Se han visto ejemplosde metodos que llaman a otros metodos, por lo que no deberıa ser extranoque muchos de los lenguajes de programacion moderna, incluyendo Java,permitan que un metodo se llame a sı mismo. En esta seccion, se vera porqueesta capacidad da una alternativa y poderosa para realizar tareas repetitivas.

La funcion factorial

Para ilustrar la recurrencia, se inicia con ejemplo sencillo para calcular elvalor de la funcion factorial . El factorial de un entero positivo n, denotadapor n!, esta definida como el producto de los enteros desde 1 hasta n. Sin = 0, entonces n! esta definida como uno por convencion. Mas formalmente,para cualquier entero n ≥ 0,

n! =

{1 si n = 01 · 2 · · · (n− 2) · (n− 1) · n si n ≥ 1

Para hacer la conexion con los metodos mas clara, se usa la notacionfactorial(n) para denotar n!.

La funcion factorial puede ser definida de una forma que sugiera unaformulacion recurrente. Para ver esto, observar que

Page 43: Notas Estructuras

2.5 Recurrencia 43

factorial(5) = 5 · (4 · 3 · 2 · 1) = 5 · factorial(4)

Por lo tanto, se puede definir factorial(5) en terminos del factorial(4).En general, para un entero positivo n, se puede definir factorial(n) comon · factorial(n− 1). Esto lleva a la siguiente definicion recurrente.

factorial(n) =

{1 si n = 0n · factorial(n− 1) si n ≥ 1

(2.1)

Esta definicion es tıpica de varias definiciones recurrentes. Primero, estacontiene uno o mas casos base, los cuales estan definidos no recurrentementeen terminos de cantidades fijas. En este caso, n = 0 es el caso base. Estadefinicion tambien contiene uno o mas casos recurrentes , los cuales estandefinidos apelando a la defincion de la funcion que esta siendo definida. Ob-servar que no hay circularidad en esta definicion, porque cada vez que lafuncion es invocada, su argumento es mas pequeno en uno.

Una implementacion recurrente de la funcion factorial

Se considera una implementacion en Java, listado 2.14, de la funcion facto-rial que esta dada por la formula 2.1 con el nombre factorialRecursivo().Observar que no se requiere ciclo. Las invocaciones repetidas recurrentes dela funcion reemplazan el ciclo.�

public static int factorialRecursivo(int n) { // funcion recursiva factorial

if (n == 0) return 1; // caso base

else return n * factorialRecursivo(n-1); // caso recursivo

}�Listado 2.14: Una implementacion recurrente de la funcion factorial.

¿Cual es la ventaja de usar recurrencia? La implementacion recurrentede la funcion factorial un poco mas simple que la version iterativa, en estecaso no hay razon urgente para preferir la recurrencia sobre la iteracion. Paraalgunos problemas, sin embargo, una implementacion recurrente puede sersignificativamente mas simple y facil de entender que una implementacioniterativa como los ejemplos siguientes.

Dibujando una regla inglesa

Como un ejemplo mas complejo del uso de recurrencia, considerar comodibujar las marcas de una regla tıpica Inglesa. Una regla esta dividida en in-

Page 44: Notas Estructuras

44 Arreglos, listas enlazadas y recurrencia

---- 0 ----- 0 --- 0

- - -

-- -- --

- - -

--- --- --- 1

- - -

-- -- --

- - -

---- 1 ---- --- 2

- - -

-- -- --

- - -

--- --- --- 3

- -

-- --

- -

---- 2 ----- 1

(a) (b) (c)

Figura 2.2: Tres salidas ejemplo de la funcion dibuja regla: (a) una regla de2” con longitud 4 de la marca principal; (b) una regla de 1” con longitud 5 dela marca principal; (c) una regla de 3” con longitud 3 de la marca principal.

tervalos de una pulgada, y cada intervalos consiste de un conjunto de marcascolocados en intervalos de media pulgada, un cuarto de pulgada, y ası suce-sivamente. Como el tamano del intervalo decrece por la mitad, la longitudde la marca decrece en uno, ver figura 2.5.

Cada multiplo de una pulgada tambien tiene una etiqueta numerica. Lalongitud de la marca mas grande es llamada la longitud de la marca princi-pal. No se consideran las distancias actuales entre marcas, sin embargo, seimprime una marca por lınea.

Una aproximacion recurrente para dibujar la regla

La aproximacion para dibujar tal regla consiste de tres funciones. La fun-cion principal dibujaRegla() dibuja la regla entera. Sus argumentos son el

Page 45: Notas Estructuras

2.5 Recurrencia 45

numero total de pulgadas en la regla, nPulgadas, y la longitud de la mar-ca principal, longPrincipal. La funcion utilidad dibujaUnaMarca() dibujauna sola marca de la longitud dada. Tambien se le puede dar una etiquetaentera opcional, la cual es impresa si esta no es negativa.

El trabajo interesante es hecho por la funcion recurrente dibujaMarcas(),la cual dibuja la secuencia de marcas dentro de algun intervalo. Su unico argu-mento es la longitud de la marca asociada con la marca del intervalo central.Considerar la regla de 1” con longitud 5 de la marca principal mostrada enla figura 2.5(b). Ignorando las lıneas conteniendo 0 y 1, considerar como di-bujar la secuencia de marcas que estan entre estas lıneas. La marca central(en 1/2”) tiene longitud 4. Los dos patrones de marcas encima y abajo deesta marca central son identicas, y cada una tiene una marca central de lon-gitud 3. En general, un intervalo con una marca central de longitud L ≥ 1esta compuesta de lo siguiente:

Un intervalo con una longitud de marca central L− 1

Un solo interalo de longitud L

Un intervalo con una longitud de marca central L− 1

Con cada llamada recurrente, la longitud decrece por uno. Cuando la lon-gitud llega a cero, simplemente se debe regresar. Como resultado, este procesorecurrente siempre terminara. Esto sugiere un proceso recurrente, en el cual elprimer paso y ultimo son hechos llamando a dibujaMarcas(L-1) recurrente-mente. El paso central es realizado llamando a la funcion dibujaUnaMarca(L).Esta formulacion recurrente es mostrada en el listado 2.15. Como en el ejem-plo del factorial, el codigo tiene un caso base, cuando L = 0. En este ejemplose hacen dos llamadas recurrentes a la funcion.�

// dibuja una marca sin etiqueta

public static void dibujaUnaMarca(int longMarca) {

dibujaUnaMarca(longMarca , -1);

}

// dibuja una marca

public static void dibujaUnaMarca(int longMarca , int etiqMarca) {

for (int i = 0; i < longMarca; i++)

System.out.print("-");

if (etiqMarca >= 0) System.out.print(" " + etiqMarca );

System.out.print("\n");

}

// dibuja marcas de longitud dada

Page 46: Notas Estructuras

46 Arreglos, listas enlazadas y recurrencia

public static void dibujaMarcas(int longMarca) {

if (longMarca > 0) { // parar cuando la longitud llegue a 0

dibujaMarcas(longMarca -1); // recursivamente dibujar marca superior

dibujaUnaMarca(longMarca ); // dibujar marca principal

dibujaMarcas(longMarca -1); // recursivamente dibujar marca inferior

}

}

// dibuja regla

public static void dibujaRegla(int nPulgadas , int longPrincipal) {

dibujaUnaMarca(longPrincipal , 0); // dibujar marca 0 y su etiqueta

for (int i = 1; i <= nPulgadas; i++) {

dibujaMarcas(longPrincipal -1); // dibujar marcas para esta pulgada

dibujaUnaMarca(longPrincipal , i); // dibujar marca i y su etiqueta

}

}�Listado 2.15: Una implementacion recurrente de una funcion que dibuja unaregla.

Sumando los elementos de un arreglo recurrentemente

Suponer que se da un arreglo, A, de n enteros que se desean sumar juntos.Se puede resolver el problema de la suma usando recurrencia lineal observan-do que la suma de todos los n enteros de A es igual a A[0], si n = 1, o lasuma de los primeros n − 1 enteros mas la suma del ultimo elemento en A.En particular, se puede resolver este problema de suma usando el algoritmorecurrente siguiente.

Algoritmo SumaLineal(A, n):Entrada: Un arreglo A de enteros y un entero n ≥ 1 tal que A tiene

al menos n elementos.Salida: La suma de los primeros n enteros en A.Si n = 1 Entoncesregresar A[0]

Si noregresar SumaLineal(A, n− 1) + A[n− 1]

Este ejemplo, tambien ilustra una propiedad importante que un metodorecurrente deberıa siempre tener—que el metodo termine. Se asegura estoescribiendo una sentencia no recurrente para el caso n = 1. Ademas, siemprese hace una llamada recurrente sobre un valor mas pequeno del parametro

Page 47: Notas Estructuras

2.5 Recurrencia 47

(n−1) que el que fue dado (n), por lo tanto, en algun punto (en el punto masbajo de la recurrencia), se hara la parte no recurrente del computo (regre-sando A[0]). En general, un algoritmo que usa recurrencia lineal tıpicamentetiene la siguiente forma:

Prueba para los casos base. Se inicia probando un conjunto decasos base, deberıa haber al menos uno. Estos casos basos deberıanestar definidos, por lo que cada posible encadenamiento de llamadasrecurrentes eventualmente alcanzara un caso base, y el manejo de cadacaso base no deberıa usar recurrencia.

Recurrencia. Despues de probar los casos base, entonces se realizanllamadas recurrentes simples. Este paso recurrente podrıa involucraruna prueba que decida cual de varias posibles llamadas recurrenteshacer, pero finalmente deberıa escoger hacer solo una de estas posiblesllamadas cada vez que se haga este paso. Mas aun, se deberıa definircada posible llamada recurrente para que esta progrese hacia el casobase.

Invirtiendo un arreglo por recurrencia

Se considera ahora el problema de invertir los n elementos de un arre-glo, A, para que el primer elemento sea el ultimo, el segundo elemento seael penultimo, y ası sucesivamente. Se puede resolver este problema usandorecurrencia lineal, observando que la inversion de un arreglo se puede lograrintercambiando el primer elemento y el ultimo, y entonces invertir recurren-temente el resto de los elementos en el arreglo. Se describen a continuacionlos detalles de este algoritmo, usando la convencion de que la primera vezque se llama el algoritmo se hace como invertirArreglo(A, 0, n− 1).

Algoritmo InvertirArreglo(A, i, j):Entrada: Un arreglo A e ındices enteros no negativos i y j.Salida: La inversion de los elementos en A iniciando en el ındice i

y terminando en j.Si i < j Entonces

Intercambiar A[i] y A[j]InvertirArreglo(A, i + 1, j − 1)

Regresar

Page 48: Notas Estructuras

48 Arreglos, listas enlazadas y recurrencia

En este algoritmo se tienen dos casos base implıcitos, a saber, cuandoi = j y cuando i > j. Sin embargo, en cualquier caso, simplemente se ter-mina el algoritmo, ya que una secuencia con cero elementos o un elemen-to es trivialmente igual a su inversion. Ademas, en el paso recurrente seesta garantizando ir progresando hacia uno de los dos casos base. Si n espar, eventualmente se alcanzara el caso i = j, y si n es impar, eventualmentese alcanzara el caso i > j. El argumento anterior implica que el algoritmorecurrente esta garantizado para terminar.

Definiendo problemas de manera que facilite la recurrencia

Para disenar un algoritmo recurrente para un problema dado, es util pen-sar en las diferentes formas en que se puede subdividir el problema paradefinir problemas que tengan la misma estructura general que el problemaoriginal. Este proceso en ocasiones significa que se necesita redefinir el proble-ma original para facilitar la busqueda de problemas similares. Por ejemplo,con el algoritmo InvertirArreglo, se agregaron los parametros i y j porlo que una llamada recurrente para invertir la parte interna del arreglo Apodrıan tener la misma estructura y misma sintaxis, como la llamada parainvertir cualquier otro par de valores del arreglo A. Entonces, en vez de queinicialmente se llame al algoritmo como InvertirArreglo(A), se llama es-te inicialmente como InvertirArreglo(A, 0, n − 1). En general, si se tienedificultad para encontrar la estructura repetitiva requerida para disenar unalgoritmo recurrente, en algunos casos es util trabajar el problema con algu-nos ejemplos concretos pequenos para ver como los subproblemas deberıanestar definidos.

Recurrencia de cola

Usar la recurrencia puede ser con frecuencia una herramienta util paradisenar algoritmos que tienen definciones cortas y elegantes. Pero esta uti-lidad viene acompanado de un costo modesto. Cuando se usa un algoritmorecurrente para resolver un problema, se tienen que usar algunas de las lo-calidades de la memoria para guardar el estado de las llamadas recurrentesactivas. Cuando la memoria de la computadora es un lujo, entonces, es masutil en algunos casos poder derivar algoritmos no recurrentes de los recurren-tes.

Se puede emplear la estructura de datos pila para convertir un algo-

Page 49: Notas Estructuras

2.5 Recurrencia 49

ritmo recurrente en uno no recurrente, pero hay algunos casos cuando sepuede hacer esta conversion mas facil y eficiente. Especıficamente, se puedefacilmente convertir algoritmos que usan recurrencia de cola. Un algoritmousa recurrencia de cola si este usa recurrencia lineal y el algoritmo haceuna llamada recurrente como su ultima operacion. Por ejemplo, el algoritmoInvertirArreglo usa recurrencia de cola.

Sin embargo, no es suficiente que la ultima sentencia en el metodo defi-nicion incluya una llamada recurrente. Para que un metodo use recurrenciade cola, la llamada recurrente debera ser absolutamente la ultima parte queel metodo haga, a menos que este en el caso base. Por ejemplo, el algoritmoSumaLineal no usa recurrencia de cola, aun cuando su ultima sentencia in-cluye una llamada recurrente. Esta llamada recurrente no es actualmente laultima tarea que el metodo realiza. Despues de esta recibe el valor regresadode la llamada recurrente, le agrega el valor de A[n − 1] y regresa su suma.Esto es, la ultima tarea que hace el algoritmo es una suma, y no una llamadarecurrente.

Cuando un algoritmo emplea recurrencia de cola, se puede convertir elalgoritmo recurrente en uno no recurrente, iterando a traves de las llamadasrecurrentes en vez de llamarlas a ellas explıcitamente. Se ilustra este tipo deconversion revisitando el problema de invertir los elementos de un arreglo.El siguiente algoritmo no recurrente realiza la tarea de invertir los elementositerando a traves de llamadas recurrentes del algoritmo InvertirArreglo.Inicialmente se llama a este algoritmo como InvertirArregloIterativo

(A, 0, n− 1).

Algoritmo InvertirArregloIterativo(A, i, j):Entrada: Un arreglo A e ındices enteros no negativos i y j.Salida: La inversion de los elementos en A iniciando en el ındice i

y terminando en j.Mientras i < j Hacer

Intercambiar A[i] y A[j]i← i + 1j ← j − 1

Regresar

Page 50: Notas Estructuras

50 Arreglos, listas enlazadas y recurrencia

2.5.1. Recurrencia binaria

Cuando un algoritmo hace dos llamadas recurrentes, se dice que usa recu-rrencia binaria. Estas llamadas pueden, por ejemplo, ser usadas para resolverdos hojas similares de algun problema, como se hizo en la seccion 2.5 paradibujar una regla inglesa. Como otra aplicacion de recurrencia binaria, serevisitara el problema de sumar los n elementos de un arreglo de enteros A.En este caso, se pueden sumar los elementos de A: (i) recurrentemente loselementos en la primera mitad de A; (ii) recurrentemente los elementos enla segunda mitad de A; y (iii) agregando estos dos valores juntos. Se danlos detalles en el siguiente algoritmo, el cual es inicialmente llamado comoSumaBinaria(A, 0, n).

Algoritmo SumaBinaria(A, i, n):Entrada: Un arreglo A de enteros y enteros i y n.Salida: La suma de los primeros n enteros en A iniciando en el ındice i.Si n = 1 Entoncesregresar A[i]

regresar SumaBinaria(A, i, dn/2e)+SumaBinaria(A, i + dn/2e, bn/2c)

Encontrando numeros de Fibonacci con recurrencia binaria

Se considera ahora el problema de computar el k-esimo numero de Fibo-nacci. Los numeros de Fibonacci estan recurrentemente definidos como:

F0 = 0F1 = 1Fi = Fi−1 + Fi−2 para i < 1.

Aplicando directamente la definicion anterior, el algoritmo FibBinario,mostrado a continuacion, encuentra la secuencia de los numeros de Fibonacciempleando recurrencia binaria.

Algoritmo FibBinario(k):Entrada: Un entero k no negativo.Salida: El k-esimo numero de Fibonacci Fk.Si k ≤ 1 Entoncesregresar k

Page 51: Notas Estructuras

2.5 Recurrencia 51

Si noregresar FibBinario(k − 1)+FibBinario(k − 2)

Desafortunadamente, a pesar de la definicion de Fibonacci pareciendo unarecurrencia binaria, usando esta tecnica es ineficiente en este caso. De hecho,toma un numero de llamadas exponencial para calcular el k-esimo numerode Fibonacci de esta forma. Especıficamente, sea nk el numero de llamadashechas en la ejecucion de FibBinario(k). Entonces, se tienen los siguientesvalores para las diferentes nk:

n0 = 1n1 = 1n2 = n1 + n0 + 1 = 3n3 = n2 + n1 + 1 = 5n4 = n3 + n2 + 1 = 9n5 = n4 + n3 + 1 = 15n6 = n5 + n4 + 1 = 25n7 = n6 + n5 + 1 = 41n8 = n7 + n6 + 1 = 67

Si se sigue hacia adelante el patron, se ve que el numero de llamadas esmas del doble para un ındice que este dos indices anteriores. Esto es, n4 es masdel doble que n2, n5 es mas del doble que n3, y ası sucesivamente. Por lo tanto,nk > 2k/2, lo que significa que FibBinario(k) hace un numero de llamadasque son exponenciales en k. En otras palabras, empleando recurrencia binariapara computar numeros de Fibonacci es muy ineficiente.

Numeros de Fibonacci con recurrencia lineal

El principal problema con la aproximacion anterior, basada en recurren-cia binaria, es que el computo de los numeros de Fibonacci es realmente unproblema de recurrencia lineal. No es este un buen candidato para emplear re-currencia binaria. Se empleo recurrencia binaria por la forma como el k-esimonumero de Fibonacci, Fk, dependıa de los dos valores previos, Fk−1 y Fk−2.Pero se puede encontrar Fk de una forma mas eficiente usando recurrencialineal.

Para poder emplear recurrencia lineal, se necesita redefinir ligeramenteel problema. Una forma de lograr esta conversion es definir una funcion re-currente que encuentre un par consecutivo de numeros Fibonacci (Fk−1, Fk).

Page 52: Notas Estructuras

52 Arreglos, listas enlazadas y recurrencia

Entonces se puede usar el siguiente algoritmo de recurrencia lineal mostradoa continuacion.

Algoritmo FibLineal(k):Entrada: Un entero k no negativo.Salida: El par de numeros Fibonacci (Fk, Fk−1).Si k ≤ 1 Entoncesregresar (0, k)

Si no(i, j)← FibLineal(k − 1)regresar (i + j, i)

El algoritmo dado muestra que usando recurrencia lineal para encontrarlos numeros de Fibonacci es mucho mas eficiente que usando recurrencia bi-naria. Ya que cada llama recurrente a FibLineal decrementa el argumentok en 1, la llamada original FibLineal(k) resulta en una serie de k − 1 lla-madas adicionales. Esto es, computar el k-esimo numero de Fibonacci usan-do recurrencia lineal requiere k llamadas al metodo. Este funcionamientoes significativamente mas rapido que el tiempo exponencial necesitado pa-ra el algoritmo basado en recurrencia binaria. Por lo tanto, cuando se usarecurrencia binaria, se debe primero tratar de particionar completamente elproblema en dos, o se deberıa estar seguro que las llamadas recurrentes quese traslapan son realmente necesarias.

Usualmente, se puede eliminar el traslape de llamadas recurrentes usandomas memoria para conservar los valores previos. De hecho, esta aproximaciones una parte central de una tecnica llamada programacion dinamica, la cualesta relacionada con la recurrente.

2.5.2. Recurrencia multiple

Generalizando de la recurrencia binaria, se usa recurrencia multiple cuan-do un metodo podrıa hacer varias llamadas recurrentes multiples, siendo esenumero potencialmente mayor que dos. Una de las aplicaciones mas comunesde este tipo de recurrencia es usada cuando se quiere enumerar varias confi-guraciones en orden para resolver un acertijo combinatorio. Por ejemplo, elsiguiente es un acertijo de suma:

seis+de+enero=reyes

Page 53: Notas Estructuras

2.5 Recurrencia 53

Para resolver el acertijo, se necesita asignar un dıgito unico, esto es,0,1,. . . ,9, a cada letra en la ecuacion, para poder hacer la ecuacion verdadera.Tıpicamente, se resuelve el acertijo usando observaciones humanas del acer-tijo particular que se esta intentando resolver para eliminar configuracioneshasta que se pueda trabajar con las configuraciones restantes, probando lacorrectez de cada una.

Sin embargo, si el numero de configuraciones posibles no es muy grande,se puede usar una computadora simplemente para enumerar todas las posi-bilidades y probar cada una, sin emplear observaciones humanas. Ademas,tal algoritmo puede usar recurrencia multiple para trabajar a traves de todaslas configuraciones en una forma sistematica. Se proporciona enseguida pseu-docodigo para tal algoritmo. Para conservar la descripcion general suficientepara ser usado con otros acertijos, el algoritmo enumera y prueba todas lassecuencias de longitud k sin repeticiones de los elementos de un conjuntodado U . Se construyen las secuencias de k elementos mediante los siguientespasos:

1. Generando recurrentemente las secuencias de k − 1 elementos.

2. Agregando a cada una de tales secuencias un elemento que no este con-tenido dentro de esta.

En toda la ejecucion del algoritmo, se usa el conjunto U para saber loselementos no contenidos en la secuencia actual, por lo que un elemento e noha sido usado aun sı y solo sı e esta en U .

Otra forma de ver el algoritmo siguiente es que este enumera cada sub-conjunto ordenado posible de tamano, y prueba cada subconjunto para seruna posible solucion al acertijo.

Para acertijos de sumas, U = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 y cada posicion enla secuencia corresponde a una letra dada. Por ejemplo, la primera posicionpodrıa ser b, la segunda o, la tercera y, y ası sucesivamente.

Algoritmo ResolverAcertijo(k, S, U):Entrada: Un entero k, secuencia S, y conjunto U .Salida: Una enumeracion de todas las extensiones k-longitud para S

usando elementos en U sin repeticiones.Para cada e en U Hacer

Remover e de U { e esta ahora siendo usado }

Page 54: Notas Estructuras

54 Arreglos, listas enlazadas y recurrencia

Agregar e al final de SSi k = 1 Entonces

Probar si S es una configuracion que resuelva el acertijoSi S resuelve el acertijo Entoncesregresar “Solucion encontrada: ” S

Si noResolverAcertijo(k − 1, S, U)

Agregar e de regreso a U { e esta ahora sin uso }Remover e del final de S

Page 55: Notas Estructuras

Capıtulo 3

Pilas y colas

3.1. Genericos

Iniciando con la version 5.0 de Java se incluye una estructura genericapara usar tipos abstractos de manera que evite muchas conversiones de tipos.Un tipo generico es un tipo que no esta definido en tiempo de compilacion,pero se convierte especificado completamente en tiempo de ejecucion. Laestructura generica permite definir una clase en terminos de un conjunto deparametros de tipo formal , los cuales podrıan ser usados, por ejemplo, paraabstraer los tipos de algunas variables internas de la clase. Los parentesisangulares son usados para encerrar la lista de los parametros de tipo formal.Aunque cualquier identificador valido puede ser usado para un parametrode tipo formal, los nombres de una sola letra mayuscula por convencion sonusados. Dada una clase que ha sido definida con tales tipos parametrizados,se instancia un objeto de esta clase usando los parametros de tipo actual paraindicar los tipos concretos que seran usados.

En el listado 3.1, se muestra la clase Par que guarda los pares llave-valor,donde los tipos de la llave y el valor son especificados por los parametros L

y V, respectivamente. El metodo principal crea dos instancias de esta clase,una para un par String-Integer (por ejemplo, para guardar un dimension ysu valor), y otra para un par Estudiante-Double (por ejemplo, para guardarla calificacion dada a un estudiante).�public class Par <L, V> {

L llave;

V valor;

Page 56: Notas Estructuras

56 Pilas y colas

public void set(L k, V v) {

llave = k;

valor = v;

}

public L getLlave () { return llave; }

public V getValor () { return valor; }

public String toString () {

return "[" + getLlave () + ", " + getValor () + "]";

}

public static void main (String [] args) {

Par <String ,Integer > par1 = new Par <String ,Integer >();

par1.set(new String("altura"), new Integer (2));

System.out.println(par1);

Par <Estudiante ,Double > par2 = new Par <Estudiante ,Double >();

par2.set(new Estudiante("8403725A","Hector" ,14), new Double (9.5));

System.out.println(par2);

}�Listado 3.1: Clase Par

La clase Par hace uso de la clase Estudiante que se muestra en el listado3.2.�public class Estudiante {

String matricula;

String nombre;

int edad;

public Estudiante (String m, String n, int e) { // constructor simple

matricula = m;

nombre = n;

edad = e;

}

protected int horasEstudio () { return edad /2; } // solo una suposicion

public String getMatricula () { return matricula; } // mat. del estudiante

public String getNombre () { return nombre; } // obtener nombre del estudiante

public int getEdad () { return edad; } // regresar edad

public boolean igualA (Estudiante otro) { // es la misma persona?

return (matricula.equals(otro.getMatricula ())); // comparar matriculas

}

public String toString () { // para impresion

return "Estudiante(Matricula: " + matricula +

", Nombre: " + nombre +

", Edad: " + edad + ")";

}

}�Listado 3.2: Clase Estudiante

La salida de la ejecucion de la clase Par se muestra enseguida:

[altura, 2]

[Estudiante(Matricula: 8403725A, Nombre: Hector, Edad: 14), 9.5]

Page 57: Notas Estructuras

3.1 Genericos 57

En el ejempo previo, el parametro del tipo actual puede ser un tipo ar-bitrario. Para restringir el tipo de parametro actual, se puede usar la clausulaextends, como se muestra enseguida, donde la clase EstudianteParDirecto-rioGenerico esta definido en terminos de un parametro de tipo generico P,parcialmente especificado al afirmar que extiende la clase Estudiante.�public class EstudianteParDirectorioGenerico <E extends Estudiante > {

//... las variables de instancia podrıan ir aquı ...

public EstudianteParDirectorioGenerico () { /* el constructor

por defecto aquı va */ }

public void insertar (E estudiante , E otro) { /* codigo

para insertar va aqui */ }

public P encontrarOtro (E estudiante) { return null; } // para encontrar

public void remover (E estudiante , P otro) { /* codigo

para remover va aquı */ }

}�Dada la clase anterior, se puede declarar una variable refiriendose a una

instacia de EstudianteParDirectorioGenerico, que guarde pares de obje-tos de tipo Estudiante.

EstudianteParDirectorioGenerico<Estudiante>miDirectorioEstudiante;

Para tal instancia, el metodo encontrarOtro regresa un valor de tipoEstudiante. Ası, la siguiente sentencia, la cual no usa una conversion, escorrecta:

Estudiante carismatico = miDirectorioEstudiante.encontrarOtro(listo);

Los marcos genericos permiten definir versiones genericas de metodos.En este case, se puede incluir la definicion generica entre los metodos modi-ficadores. Por ejemplo, se muestra enseguida la definicion de un metodo quepuede comparar las llaves de cualquier par de objetos, dado que sus llavesimplementen la interfaz Comparable:�

public static <K extends Comparable ,V,L,W> int

comparaPares(Par <K,V> p, Par <L,W> q) {

return p.getLlave (). compareTo(q.getLlave ()); // llave de p implementa compareTo

}�Hay una advertencia importante relacionada a los tipos genericos, a saber,

que los elementos guardados en un arreglo no pueden ser de un tipo variableo parametrizado. Java permite que un arreglo sea definido con un tipo para-metrizado, pero no permite que un tipo parametrizado sea usado para crearun nuevo arreglo. Afortunadamente, Java permite para un arreglo definido

Page 58: Notas Estructuras

58 Pilas y colas

con un tipo parametrizado sea inicializado con un nuevo arreglo creado, noparametrizado. Aun ası, este mecanismo tardıo causa que el compilador deJava emitir una advertencia, porque esto no 100 % tipeo seguro. Se ilustraeste punto en lo siguiente:�

Par <String ,Integer >[] a = new Par [10]; // correcto , pero da una advertencia

Par <String ,Integer >[] b = new Par <String ,Integer >[10]; // incorrecto

a[0] = new Par <String ,Integer >(); // esto esta completamente bien

a[0]. set("Gato" ,10); // esta y la siguiente sentencia tambien estan bien

System.out.println("Primer par es "+a[0]. getLlave ()+","+a[0]. getValor ());

}

}�3.2. Pilas

Una pila (stack) es una coleccion de objetos que son insertados y removi-dos de acuerdo al principio ultimo en entrar, primero en salir LIFO (last-infirst-out). Los objetos pueden ser insertados en una pila en cualquier mo-mento, pero solamente el mas reciente insertado, es decir, el “ultimo” objetopuede ser removido en cualquier momento. Una analogıa de la pila es con eldispensador de platos que se encuentra en el mobiliario de alguna cafeterıao cocina. Para este caso, las operaciones fundamentales involucran “push”(empujar) platos y “pop” (sacar) platos de la pila. Cuando se necesita unnuevo plato del dispensador, se saca el plato que esta encima de la pila, ycuando se agrega un plato, se empuja este hacia abajo en la pila para que seconvierta en el nuevo plato de la cima. Otro ejemplo son los navegadores Webde internet que guardan las direcciones de los sitios recientemente visitadosen una pila. Cada vez que un usuario visita un nuevo sitio, esa direccion delsitio es empujada en la pila de direcciones. El navegador entonces permiteal usuario sacar para regresar a sitios previamente visitados usando el botonregresar.

3.2.1. El tipo de dato abstracto pila

Las pilas son las estructuras de datos mas simples, no obstante estas tam-bien estan entre las mas importantes, ya que son usadas en un sistema dediferentes aplicaciones que incluyen estructuras de datos mucho mas sofisti-cadas. Formalmente, una pila es un tipo de dato abstracto que soporta lossiguientes dos metodos:

Page 59: Notas Estructuras

3.2 Pilas 59

push(e): inserta el elemento e, para que sea la cima de la pila.pop(): quita el elemento de la cima de la pila y lo regresa; un

error ocurre si la pila esta vacıa.

Adicionalmente, tambien se podrıan definir los siguientes metodos:

size(): regresa el numero de elementos en la pila.isEmpty(): regresa un booleano indicando si la pila esta vacıa.

top(): regresa el elemento de la cima de la pila, sin removerlo;un error ocurre si la pila esta vacıa.

La siguiente tabla muestra una serie de operaciones en la pila y su efectoen una pila de enteros inicialmente vacıa.

Operacion Salida Contenidopush(5) – (5)push(3) – (5,3)pop() 3 (5)push(7) – (5,7)pop() 7 (5)top() 5 (5)pop() 5 ()pop() “error” ()

isEmpty() true ()push(9) – (9)push(7) – (9,7)push(3) – (9,7,3)push(5) – (9,7,3,5)size() 4 (9,7,3,5)pop() 5 (9,7,3)push(8) – (9,7,3,8)pop() 8 (9,7,3)pop() 3 (9,7)

Una interfaz pila en Java

Debido a su importancia, la estructura de datos pila esta incluida comouna clase en el paquete java.util. La clase java.util.Stack es una estruc-

Page 60: Notas Estructuras

60 Pilas y colas

tura que guarda objetos genericos Java e incluye, entre otros, los metodospush(), pop(), peek() (equivalente a top()), size(), y empty() (equi-valente a isEmpty()). Los metodos pop() y peek() lanzan la excepcionEmptyStackException del paquete java.util si son llamados con pilasvacıas. Mientras es conveniente solo usar la clase incluida java.util.Stack,es instructivo aprender como disenar e implementar una pila “desde cero”.

Implementar un tipo de dato abstracto en Java involucra dos pasos. Elprimer paso es la definicion de una interfaz de programacion de aplicacioneso Application Programming Interface (API), o simplemente interfaz,la cual describe los nombres de los metodos que la estructura abstracta dedatos soporta y como tienen que ser declarados y usados.

Ademas, se deben definir excepciones para cualquier cualquier condicionde error que pueda originarse. Por ejemplo, la condicion de error que ocurrecuando se llama al metodo pop() o top() en una cola vacıa es senaladolanzando una excepcion de tipo EmptyStackException, la cual esta definidaen el listado 3.3�/**

* Excepcion Runtime lanzada cuando se intenta hacer una operacion top

* o pop en una cola vacıa.

*/

public class EmptyStackException extends RuntimeException {

public EmptyStackException(String err) {

super(err);

}

}�Listado 3.3: Excepcion lanzada por los metodos pop() y pop() de la interfazpila cuando son llamados en una pila vacıa.

Una interfaz completa para el ADT pila esta dado en el listado 3.4. Estainterfaz es muy general ya que esta especıfica que elementos de cualquierclase dada, y sus subclases, pueden ser insertados en la pila. Se obtiene estageneralidad mediante el concepto de genericos (seccion 3.1).

Para que un ADT dado sea para cualquier uso, se necesita dar una claseconcreta que implemente los metodos de la interfaz asociada con ese ADT. Seda una implementacion simple de la interfaz Stack en la siguiente subseccion.�/**

* Interfaz para una pila: una coleccion de objetos que son insertados

* y removidos de acuerdo al principio ultimo en entrar , primero en salir.

* Esta interfaz incluye los metodos principales de java.util.Stack.

*

* @see EmptyStackException

Page 61: Notas Estructuras

3.2 Pilas 61

*/

public interface Stack <E> {

/**

* Regresa el numero de elementos en la pila.

* @return numero de elementos en la pila.

*/

public int size ();

/**

* Indica si la pila esta vacia.

* @return true si la pila esta vacıa , de otra manera false.

*/

public boolean isEmpty ();

/**

* Explorar el elemento en la cima de la pila.

* @return el elemento cima en la pila.

* @exception EmptyStackException si la pila esta vacıa.

*/

public E top()

throws EmptyStackException;

/**

* Insertar un elemento en la cima de la pila.

* @param elemento a ser insertado.

*/

public void push (E elemento );

/**

* Quitar el elemento cima de la pila.

* @return elemento removido.

* @exception EmptyStackException si la pila esta vacıa.

*/

public E pop()

throws EmptyStackException;

}�Listado 3.4: Interfaz Stack documentada con comentarios en estilo Javadoc.Se usa el tipo parametrizado generico – E– el cual implica que una pila puedecontener elementos de cualquier clase especificada.

3.2.2. Una implementacion simple de la pila usando unarreglo

Se puede implementar una pila guardando sus elementos en un arreglo.La pila en esta implementacion consiste de una arreglo S de n elementosademas de una variable entera t que da el ındice del elemento de la cima enel arreglo S.

Page 62: Notas Estructuras

62 Pilas y colas

En Java el ındice para los arreglos inicia con cero, por lo que t se inicializacon -1, y se usa este valor de t para identificar una pila vacıa. Asimismo,se pueda usar t para determinar el numero de elementos (t+1). Se agregatambien una nueva excepcion, llamada FullStackException, para senalarel error que surge si se intenta insertar un nuevo elemento en una pila llena.La excepcion FullStackException es particular a esta implementacion y noesta definida en la ADT pila. Se dan los detalles de la implementacion de lapila usando un arreglo en los siguientes algoritmos.

Algoritmo size():Regresar t + 1

Algoritmo isEmpty():Regresar (t < 0)

Algoritmo top():Si isEmpty() Entonces

lanzar una EmptyStackExceptionRegresar S[t]

Algoritmo push(e):Si size()= N Entonces

lanzar una FullStackExceptiont← t + 1S[t]← e

Algoritmo pop():Si isEmpty() Entonces

lanzar una EmptyStackExceptione← S[t]S[t]← nullt← t− 1Regresar e

Analisis de la implementacion de pila con arreglos

La correctez de los metodos en la implementacipon usando arreglos se tie-ne inmediatamente de la definicion de los propios metodos. Hay, sin embargo,

Page 63: Notas Estructuras

3.2 Pilas 63

un punto medio interesante que involucra la implementacion del metodo pop.Se podrıa haber evitado reiniciar el viejo S[t] a null y se podrıa tener

todavıa un metodo correcto. Sin embargo, hay una ventaja y desventaja enevitar esta asignacion que podrıa pensarse al implementar los algoritmos enJava. El equilibrio involucra el mecanismo colector de basura de Java quebusca en la memoria objetos que ya no son mas referenciados por objetosactivos, y recupera este espacio para su uso futuro. Sea e = S[t] el elementode la cima antes de que el metodo pop sea llamado. Haciendo a S[t] unareferencia nula, se indica que la pila no necesita mas mantener una referenciaal objeto e. Ademas, si no hay otras referencias activas a e, entonces el espaciode memoria tomado por e sera recuperado por el colector de basura.

En la siguiente tabla se muestran los tiempos de ejecucion para los meto-dos en la ejecucion de una pila con un arreglo. Cada uno de los metodos dela pila en el arreglo ejecuta un numero constante de sentencias involucrandooperaciones aritmeticas, comparaciones, y asignaciones. Ademas, pop tam-bien llama a isEmpty, el cual tambien se ejecuta en tiempo constante. Porlo tanto, en esta implementacion del ADT pila, cada metodo se ejecuta entiempo constante, esto es, cada uno corre en tiempo O(1).

Metodo Tiemposize O(1)

isEmpty O(1)top O(1)push O(1)pop O(1)

Una implementacion concreta de Java de los algoritmos para la imple-mentacion de la pila se da en el listado 3.5, con la implementacion Java dela clase ArrayStack implementando la interfaz Stack. Se usa un nombresimbolico, CAPACIDAD, para indicar el tamano del arreglo. Este valor permiteindicar la capacidad del arreglo en un solo lugar en el codigo y que el valorse refleje en todo el codigo.�/**

* Implementacion de la ADT pila usando un arreglo de longitud fija.

* Una excepcion es lanzada si la operacion push es intentada cuando

* el tama~no de la pila es igual a la longitud del arreglo. Esta

* clase incluye los metodos principales de la clase agregada

* java.util.Stack.

*

* @see FullStackException

*/

Page 64: Notas Estructuras

64 Pilas y colas

public class ArrayStack <E> implements Stack <E> {

/**

* Capacidad del arreglo usado para implementar la pila.

*/

protected int capacidad;

/**

* Capacidad por defecto del arreglo.

*/

public static final int CAPACIDAD = 1000;

/**

* Arreglo generico usado para implementar la pila.

*/

protected E S[];

/**

* Indice del elemento cima de la pila en el arreglo

*/

protected int cima = -1;

/**

* Inicializa la pila para usar un arreglo de longitud por defecto.

*/

public ArrayStack () {

this(CAPACIDAD );

}

/**

* Inicializa la pila para usar un arreglo de longitud dada.

* @param cap longitud del arreglo.

*/

public ArrayStack(int cap) {

capacidad = cap;

S = (E[]) new Object[capacidad ]; // el compilador podrıa dar advertencia

// pero esta bien

}

/**

* Regresa el numero de elementos en la pila.

* Este metodo se ejecuta en tiempo O(1)

* @return numero de elementos en la pila.

*/

public int size() {

return (cima + 1);

}

/**

* Prueba si la pila esta vacıa.

* Este metodo se ejecuta en tiempo O(1)

* @return true si la pila esta vacia , false de otra forma.

*/

public boolean isEmpty () {

return (cima < 0);

}

/**

* Inserta un elemento en la cima de la pila.

* Este metodo se ejecuta en tiempo O(1)

* @return elemento insertado.

* @param elemento elemento a ser insertado.

* @exception FullStackException si el arreglo de los elementos esta lleno.

*/

public void push(E elemento) throws FullStackException {

if (size() == capacidad)

Page 65: Notas Estructuras

3.2 Pilas 65

throw new FullStackException("La pila esta llena.");

S[++ cima] = elemento;

}

/**

* Inspecciona el elemento en la cima de la pila.

* Este metodo se ejecuta en tiempo O(1)

* @return elemento cima en la pila.

* @exception EmptyStackException si la pila esta vacia.

*/

public E top() throws EmptyStackException {

if (isEmpty ())

throw new EmptyStackException("La pila esta vacıa.");

return S[cima];

}

/**

* Quita el elemento cima de la pila.

* Este metodo se ejecuta en tiempo O(1)

* @return elemento removido.

* @exception EmptyStackException si la pila esta vacıa.

*/

public E pop() throws EmptyStackException {

E elemento;

if (isEmpty ())

throw new EmptyStackException("La pila esta vacıa.");

elemento = S[cima];

S[cima --] = null; // desreferenciar S[cima] para el colector de basura.

return elemento;

}

/**

* Regresa una representacion de la pila como una lista de elementos ,

* con el elemento cima al final: [ ... , prev , cima ].

* Este metodo corre en tiempo O(n), donde n es el tama~no de la cima.

* @return representacion textual de la pila.

*/

public String toString () {

String s;

s = "[";

if (size() > 0) s+= S[0];

if (size() > 1)

for (int i = 1; i <= size ()-1; i++) {

s += ", " + S[i];

}

return s + "]";

}

/**

* Imprime informacion del estado de una operacion reciente de la pila.

* @param op operacion hecha

* @param elemento elemento regresado por la operacion

* @return informacion acerca de la operacion hecha el elemento

* regresado por la operacion y el contenido de la pila despues de

* la operacion.

*/

public void estado(String op, Object elemento) {

System.out.print("------> " + op); // imprime esta operacion

System.out.println(", regresa " + elemento ); // que fue regresado

System.out.print("resultado: num. elems. = " + size ());

System.out.print(", ¿esta vacio? " + isEmpty ());

Page 66: Notas Estructuras

66 Pilas y colas

System.out.println(", pila: " + this); // contenido de la pila

}

/**

* Probar el programa haciendo una serie de operaciones en pilas ,

* imprimiendo las operaciones realizadas , los elementos regresados y

* el contenido de la pila involucrada , despues de cada operacion.

*/

public static void main(String [] args) {

Object o;

ArrayStack <Integer > A = new ArrayStack <Integer >();

A.estado("new ArrayStack <Integer > A", null);

A.push (7);

A.estado("A.push (7)", null);

o = A.pop();

A.estado("A.pop()", o);

A.push (9);

A.estado("A.push (9)", null);

o = A.pop();

A.estado("A.pop()", o);

ArrayStack <String > B = new ArrayStack <String >();

B.estado("new ArrayStack <String > B", null);

B.push("Paco");

B.estado("B.push (\" Paco \")", null);

B.push("Pepe");

B.estado("B.push (\" Pepe \")", null);

o = B.pop();

B.estado("B.pop()", o);

B.push("Juan");

B.estado("B.push (\" Juan \")", null);

}

}�Listado 3.5: Implementacion de la interfaz Stack usando un arreglo con Java.

Salida ejemplo

Se muestra enseguida la salida del programa ArrayStack. Con el uso detipos genericos, se puede crear un ArrayStack A para guardar enteros y otroArrayStack B que guarda cadenas de caracteres.

------> new ArrayStack<Integer> A, regresa null

resultado: num. elems. = 0, esta vacio = true, pila: []

------> A.push(7), regresa null

resultado: num. elems. = 1, esta vacio = false, pila: [7]

------> A.pop(), regresa 7

resultado: num. elems. = 0, esta vacio = true, pila: []

------> A.push(9), regresa null

resultado: num. elems. = 1, esta vacio = false, pila: [9]

------> A.pop(), regresa 9

Page 67: Notas Estructuras

3.2 Pilas 67

resultado: num. elems. = 0, esta vacio = true, pila: []

------> new ArrayStack<String> B, regresa null

resultado: num. elems. = 0, esta vacio = true, pila: []

------> B.push("Paco"), regresa null

resultado: num. elems. = 1, esta vacio = false, pila: [Paco]

------> B.push("Pepe"), regresa null

resultado: num. elems. = 2, esta vacio = false, pila: [Paco, Pepe]

------> B.pop(), regresa Pepe

resultado: num. elems. = 1, esta vacio = false, pila: [Paco]

------> B.push("Juan"), regresa null

resultado: num. elems. = 2, esta vacio = false, pila: [Paco, Juan]

Un inconveniente con la implementacion de la pila usando un arre-glo

La implementacion con un arreglo de una pila es simple y eficiente. Sinembargo, esta implementacion tiene un aspecto negativo—esta debe asumirun lımite superior fijo, CAPACIDAD, en el tamano de la pila. Para el ejemplomostrado en el listato 3.5 se escogio el valor de la capacidad igual a 1,000 o unvalor arbitrario menor o mayor. Una aplicacion podrıa actualmente necesitarmucho menos espacio que esto, lo cual serıa un desperdicio de memoria.Alternativamente, una aplicacion podrıa necesitar mas espacio que esto, locual causarıa que la implementacion de la pila genere una excepcion tanpronto como un programa cliente intente meter mas de 1,000 objetos a lapila. De esa manera, aun con su simplicidad y eficiencia, la pila implementadacon arreglo no es precisamente ideal.

Afortunadamente, hay otra implementacion, la cual se revisa a continua-cion, que no tiene una limitacion del tamano y usa espacio proporcional alnumero actual de elementos guardados en la pila. Aun, en casos donde setiene una buena estimacion en el numero de elementos que se guardaran enla pila, la implementacion basada en el arreglo es difıcil de vencer. Las pi-las sirven un rol vital en un numero de aplicaciones computacionales, porlo tanto es util tener una implementacion rapida del ADT pila como unaimplementacion simple basada en el arreglo.

Page 68: Notas Estructuras

68 Pilas y colas

3.2.3. Implementando una pila con una lista genericaenlazada

En esta seccion, se explora usando una lista simple enlazada la implemen-tacion del ADT pila. En el diseno de tal implementacion, se requiere decidirsi la cima de la pila esta en la cabeza de la lista o en la cola. Sin embar-go, hay una mejor opcion ya que se pueden insertar elementos y borrar entiempo constante solamente en la cabeza. Por lo tanto, esto es mas eficientetener la cima de la pila en la cabeza de la lista. Tambien, para poder hacerla operacion size en tiempo constante, se lleva la cuenta del numero actualde elementos en una variable de instancia.

En vez de usar una lista ligada que solamente pueda guardar objetos deun cierto tipo, como se mostron en la seccion 2.2, se quiere, en este caso,implementar una pila generica usando una lista generica enlazada. Por lotanto, se necesita usar un nodo de tipo generico para implementar esta listaenlazada. Se muestra tal clase Nodo en el listado 3.6.�/**

* Nodo de una lista simple enlazada , la cual guarda referencias

* a su elemento y al siguiente nodo en la lista.

*/

public class Nodo <E> {

// Variables de instancia:

private E elemento;

private Nodo <E> sig;

/** Crea un nodo con referencias nulas a su elemento y al nodo sig. */

public Nodo() {

this(null , null);

}

/** Crear un nodo con el elemento dado y el nodo sig. */

public Nodo(E e, Nodo <E> s) {

elemento = e;

sig = s;

}

// Metodos accesores:

public E getElemento () {

return elemento;

}

public Nodo <E> getSig () {

return sig;

}

// Metodos modificadores:

public void setElemento(E nvoElem) {

elemento = nvoElem;

}

public void setSig(Nodo <E> nvoSig) {

sig = nvoSig;

}

}

Page 69: Notas Estructuras

3.2 Pilas 69

�Listado 3.6: La clase Nodo que implementa un nodo generico para una listasimple enlazada.

Una clase generica NodoStack

Una implementacion con Java de una pila, por medio de un lista simpleenlazada, esta dada en el listado 3.7. Todos los metodos de la interfaz Stack

son ejecutados en tiempo constante. Ademas de ser eficiente en el tiempo,esta implementacion de lista enlazada tiene un requerimiento de espacio quees O(n), donde n es el numero actual de elementos en la pila. Por lo tanto,esta implementacion no requiere que una nueva excepcion sea creada paramanejar los problemas de desbordamiento de tamano. Se usa una variable deinstancia cima para referirse a la cabeza de la lista, la cual apunta al objetonull si la lista esta vacıa. Cuando se mete un nuevo elemento e en la pila,se crea un nuevo nodo v para e, se referencia e desde v, y se inserta v en lacabeza de la lista. De igual modo, cuando se quita un elemento de la pila,se remueve el nodo en la cabeza de la lista y se regresa el elemento. Por lotanto, se realizan todas las inserciones y borrados de elementos en la cabezade la lista.�/**

* Implementacion del ADT pila por medio de una lista simple enlazada.

*

* @see Nodo

*/

public class NodoStack <E> implements Stack <E> {

protected Nodo <E> cima; // referencia el nodo cabeza

protected int size; // numero de elementos en la pila

/** Crear una pila vacıa. */

public NodoStack () {

cima = null;

size = 0;

}

public int size() { return size; }

public boolean isEmpty () {

if (cima == null) return true;

return false;

}

public void push(E elem) {

Nodo <E> v = new Nodo <E>(elem , cima); // crear un nuevo nodo y enlazarlo

cima = v;

size ++;

}

public E top() throws EmptyStackException {

if (isEmpty ()) throw new EmptyStackException("La pila esta vacıa.");

Page 70: Notas Estructuras

70 Pilas y colas

return cima.getElemento ();

}

public E pop() throws EmptyStackException {

if (isEmpty ()) throw new EmptyStackException("La pila esta vacıa.");

E temp = cima.getElemento ();

cima = cima.getSig (); // desenlazar el antiguo nodo cima

size --;

return temp;

}

/**

* Regresar una representacion de cadena de la pila como una lista

* de elementos con el elemento cima al final: [ ... , prev , cima ].

* Este metodo se ejecuta en tiempo O(n), donde n es el numero de elementos

* en la pila.

* @return representacion textual de la pila.

*/

public String toString () {

String s;

Nodo <E> cur = null;

s = "[";

int n = size ();

if (n > 0) {

cur = cima;

s += cur.getElemento ();

}

if (n > 1)

for (int i = 1; i <= n-1; i++) {

cur = cur.getSig ();

s += ", " + cur.getElemento ();

}

s += "]";

return s;

}

/**

* Imprime informacion acerca de una operacion y la pila.

* @param op operacion realizada

* @param elemento elemento regresado por la operacion

* @return informacion acerca de la operacion realizada , el elemento

* regresado por la operacion y el contenido despues de la operacion.

*/

public static void estado(Stack S, String op, Object elemento) {

System.out.println("---------------------------------");

System.out.println(op);

System.out.println("Regresado: " + elemento );

String emptyStatus;

if (S.isEmpty ())

emptyStatus = "vacıo";

else

emptyStatus = "no vacıo";

System.out.println("tam = " + S.size() + ", " + emptyStatus );

System.out.println("Pila: " + S);

}

/**

Page 71: Notas Estructuras

3.2 Pilas 71

* Programa prueba que realiza una serie de operaciones en una pila

* e imprime la operacion realizada , el elemento regresado

* y el contenido de la pila despues de cada operacion.

*/

public static void main(String [] args) {

Object o;

Stack <Integer > A = new NodoStack <Integer >();

estado (A, "Nueva Pila Vacıa", null);

A.push (5);

estado (A, "push (5)", null);

A.push (3);

estado (A, "push (3)", null);

A.push (7);

estado (A, "push (7)", null);

o = A.pop();

estado (A, "pop()", o);

A.push (9);

estado (A, "push (9)", null);

o = A.pop();

estado (A, "pop()", o);

o = o = A.top ();

estado (A, "top()", o);

}

}�Listado 3.7: Clase NodoStack implementando la interfaz Stack usando unalista simple enlazada con nodos genericos como se muestran en el listado 3.6.

3.2.4. Invirtiendo un arreglo usando una pila

Se puede usar una pila para invertir los elementos en un arreglo, por con-siguiente produciendo un algoritmo no recurrente para el problema indicadoen la seccion 2.5. La idea basica es simplemente meter todos los elementosdel arreglo en orden en una pila y entonces llenar el arreglo de regreso otravez sacando los elementos de la pila. En el listado 3.8 se da una implemen-tacion en Java del algoritmo descrito. En el metodo invertir se muestracomo se pueden usar tipos genericos en una aplicacion simple que usa unapila generica. En particular, cuando los elementos son sacados de la pila enel ejemplo, estos son automaticamente regresados como elementos del tipoE; por lo tanto, estos pueden ser inmediatamente regresados al arreglo deentrada. Se muestra dos ejemplos que usan este metodo.�public class InvertirArreglo {

public static <E> void invertir(E[] a) {

Stack <E> S = new ArrayStack <E>(a.length );

for(int i=0; i<a.length; i++)

S.push(a[i]);

Page 72: Notas Estructuras

72 Pilas y colas

for(int i=0; i<a.length; i++)

a[i] = S.pop ();

}

/** Rutina probadora para invertir arreglos */

public static void main(String args []) {

// autoboxing: a partir de Java 1.5, hace la conversion de tipos

// primitivos a objetos automaticamente y viceversa.

// Se usa a continuacion .

Integer [] a = {4, 8, 15, 16, 23, 42};

String [] s = {"Jorge","Paco","Pedro","Juan","Marta"};

System.out.println("a = "+Arrays.toString(a));

System.out.println("s = "+Arrays.toString(s));

System.out.println("Invirtiendo ...");

invertir(a);

invertir(s);

System.out.println("a = "+Arrays.toString(a));

System.out.println("s = "+Arrays.toString(s));

}

}�Listado 3.8: Un metodo generico que invierte los elementos en un arreglogenerico mediante una pila de la interfaz Stack<E>

La salida del listado 3.8 se muestra a continuacion:

a = [4, 8, 15, 16, 23, 42]

s = [Jorge, Paco, Pedro, Juan, Marta]

Invirtiendo ...

a = [42, 23, 16, 15, 8, 4]

s = [Marta, Juan, Pedro, Paco, Jorge]

3.2.5. Apareando parentesis y etiquetas HTML

En esta subseccion, se exploran dos aplicaciones relacionadas de pilas, laprimera es para el apareamiento de parentesis y sımbolos de agrupamientoen expresiones aritmeticas.

Las expresiones aritmeticas pueden contener varios pares de sımbolos deagrupamiento, tales como:

Parentesis: “(” y “)”

Llaves: “{” y “}”

Corchetes: “[” y “]”

Sımbolos de la funcion piso: “b” y “c”

Page 73: Notas Estructuras

3.2 Pilas 73

Sımbolos de la funcion techo: “d” y “e”

y cada sımbolo de apertura debera aparearse con su correspondiente sımbolode cierre. Por ejemplo, un corchete izquierdo, “[”, debera aparearse con sucorrespondiente corchete derecho, “]”, como en la siguiente expresion:

[(5 + x)− (y + z)].

Los siguientes ejemplos ilustran este concepto:

Correcto: ()(()){([()])}

Correcto: ((()(()){([()])}))

Incorrecto: )(()){([()])}

Incorrecto: ({[ ])}

Incorrecto: (

Un algoritmo para el apareamiento de parentesis

Un problema importante en el procesamiento de expresiones aritmeticases asegurar que los sımbolos de agrupamiento se apareen correctamente. Sepuede usar una pila S para hacer el apareamiento de los sımbolos de agru-pamiento en una expresion aritmetica con una sola revision de izquierda aderecha. El algoritmo prueba que los sımbolos izquierdo y derecho se apareeny tambien que ambos sımbolos sean del mismo tipo.

Suponiendo que se da una secuencia X = x0x1x2 . . . xn−1, donde cadaxi es un sımbolo que puede se un sımbolo de agrupamiento, un nombre devariable, un operador aritmetico, o un numero. La idea basica detras de larevision de que los sımbolos de agrupamiento en S empaten correctamen-te, es procesar los sımbolos en X en orden. Cada vez que se encuentre unsımbolo de apertura, se mete ese sımbolo en X, y cada vez que se encuentreun sımbolo de cierre, se saca el sımbolo de la cima de la pila S, suponiendoque S no esta vacıa, y se revisa que esos sımbolos son del mismo tipo. Su-poniendo que las operaciones push y pop son implementadas para ejecutarseen tiempo constante, este algoritmo corre en tiempo O(n), esto es linea. Seda un pseudocodigo de este algoritmo a continuacion.

Page 74: Notas Estructuras

74 Pilas y colas

Algoritmo ApareamientoParentesis(X,n):Entrada: Un arreglo X de n sımbolos, cada uno de los cuales

es un sımbolo de agrupamiento, una variable, un operadoraritmetico, o un numero.

Salida: true si y solo si todos los sımbolos de agrupamientoen X se aparean.

Sea S una pila vacıaPara i← 0 Hasta n− 1 HacerSi X[i] es un sımbolo de apertura EntoncesS.push(X[i])

Si no Si S[i] es sımbolo de cierre EntoncesSi S.isEmpty() Entoncesregresar false { nada para aparear }

Si S.pop() no empata el tipo de X[i] Entoncesregresar false { tipo incorrecto }

Si S.isEmpty() Entoncesregresar true { cada sımbolo apareado }

Si noregresar false { algunos simbolos no empataron }

Apareamiento de etiquetas en un documento HTML

Otra aplicacion en la cual el apareamiento es importante es en la valida-cion de documentos HTML. HTML es el formato estandar para documentoshiperenlazados en el Internet. En un documento HTML, porciones de tex-to estan delimitados por etiquetas HTML. Una etiqueta simple de aperturaHTML tiene la forma “<nombre>” y la correspondiente etiqueta de cierre tie-ne la forma “</nombre>”. Las etiquetas HTML comunmente usadas incluyen

body: cuerpo del documento

h1: seccion cabecera

blockquote: seccion sangrada

p: parrafo

ol: lista ordenada

Page 75: Notas Estructuras

3.2 Pilas 75

li: elemento de la lista

Idealmente, un documento HTML deberıa tener todas sus etiquetas apa-readas, sin embargo varios navegadores toleran un cierto numero de etiquetasno apareadas.

El algoritmo de la seccion 3.2.5 para el apareamiento de parentesis puedeser usado para aparear las etiquetas en un documento HTML. En el listado3.9 se da un programa en Java para aparear etiquetas en un documentoHTML leıdo de la entrada estandar. Por simplicidad, se asume que todas lasetiquetas son etiquetas simples de apertura y cierre, es decir sin atributos yque no hay etiquetas formadas incorrectamente. El metodo esHTMLApareada

usa una pila para guardar los nombres de las etiquetas�import java.io.IOException;

import java.util.Scanner;

/** Prueba simplificada de apareamiento de etiquetas en un documento HTML. */

public class HTML {

/** Quitar el primer caracter y el ulitmo de una <etiqueta > cadena. */

public static String quitarExtremos(String t) {

if (t.length () <= 2 ) return null; // esta es una etiqueta degenerada

return t.substring (1,t.length ()-1);

}

/** Probar si una etiqueta desnuda (sin los simbolos inicial y final)

* esta vacıa o es una etiqueta verdadera de apertura. */

public static boolean esEtiquetaApertura(String etiqueta) {

return etiqueta.length ()==0 || etiqueta.charAt (0)!= ’/’;

}

/** Probar si etiqueta1 desnuda aparea con etiqueta2 de cierre. */

public static boolean seAparean(String etiqueta1 , String etiqueta2) {

// probar contra el nombre despues de ’/’

return etiqueta1.equals(etiqueta2.substring (1));

}

/** Probar si cada etiqueta de apertura tiene una etiqueta de cierre. */

public static boolean esHTMLApareada(String [] etiqueta) {

Stack <String > S = new NodoStack <String >(); // Pila para aparear etiquetas

for (int i=0; (i<etiqueta.length )&&( etiqueta[i]!= null); i++)

if (esEtiquetaApertura(etiqueta[i]))

S.push(etiqueta[i]); // etiqueta apertura; meterla en la pila

else {

if (S.isEmpty ())

return false; // nada para aparear

if (! seAparean(S.pop(), etiqueta[i]))

return false; // apareamiento incorrecto

}

if (S.isEmpty ()) return true; // se apareo todo

return false; // hay algunas etiquetas que nunca fueron apareadas

}

public final static int CAPACIDAD = 1000; // Capacidad del arreglo etiqueta

/* Dividir un documento HTML en un arreglo de etiquetas html */

public static String [] parseHTML(Scanner s) {

String [] etiqueta = new String[CAPACIDAD ]; // el arreglo etiqueta

Page 76: Notas Estructuras

76 Pilas y colas

int contador = 0; // contador de etiquetas

String elemento; // elemento regresado por el scanner s

while (s.hasNextLine ()) {

while (( elemento=s.findInLine(" <[^>]*>"))!= null) // encontrar sig etiq

etiqueta[contador ++]= quitarExtremos(elemento );

s.nextLine (); // ir a la siguiente lınea

}

return etiqueta; // arreglo de etiquetas desnudas

}

public static void main(String [] args) throws IOException { // probador

System.out.print("El archivo de entrada es un documento ");

if (esHTMLApareada(parseHTML(new Scanner(System.in))))

System.out.println("HTML apareado");

else

System.out.println("HTML NO apareado");

}

}�Listado 3.9: Clase HTML para revisar el apareamiento de las etiquetas en undocumento HTML.

3.3. Colas

Otra estructura de datos fundamental es la cola (queue). Una cola es unacoleccion de objetos que son insertados y removidos de acuerdo al principioprimero en entrar, primero en salir FIFO (first-in first-out). Esto es, loselementos pueden ser insertados en cualquier momento, pero solamente elelemento que ha estado en la cola mas tiempo puede ser removido en cualquiermomento.

Se dice que los elementos entran a una cola por la parte de atras y sonremovidos del frente. La metafora para esta terminologıa es una lınea degente esperando para subir a un juego mecanico. La gente que espera paratal juego entra por la parte de atras de la lınea y se sube al juego, por elfrente de la lınea.

3.3.1. El tipo de dato abstracto cola

El tipo de dato abstracto cola define una coleccion que guarda los objetosen una secuencia, donde el acceso al elemento y borrado estan restringidosal primer elemento en la secuencia, el cual es llamado el frente de la cola, yla insercion del elemento esta restringida al final de la secuencia, la cual esllamada la parte posterior de la cola. Esta restriccion forza la regla de que

Page 77: Notas Estructuras

3.3 Colas 77

los elementos son insertados y borrados en una cola de acuerdo al principioFIFO.

El ADT cola soporta los siguientes dos metodos fundamentales:

enqueue(e): inserta el elemento e en la parte posterior de la cola.dequeue(): quita el elemento del frente de la cola y regresarlo; un

error ocurre si la cola esta vacıa.

Adicionalmente, de igual modo como con el ADT pila, el ADT cola incluyelos siguientes metodos de apoyo:

size(): regresa el numero de elementos en la cola.isEmpty(): regresa un booleano indicando si la cola esta vacıa.

front(): regresa el elemento del frente de la cola, sin removerlo;un error ocurre si la cola esta vacıa.

La siguiente tabla muestra una serie de operaciones de cola y su efectoen una cola C inicialmente vacıa de objetos enteros. Por simplicidad, se usanenteros en vez de objetos enteros como argumentos de las operaciones.

Operacion Salida frente← C ← colaenqueue(5) – (5)enqueue(3) – (5,3)dequeue() 5 (3)enqueue(7) – (3,7)dequeue() 3 (7)front() 7 (7)dequeue() 7 ()dequeue() “error” ()isEmpty() true ()enqueue(9) – (9)enqueue(7) – (9,7)

size() 2 (9,7)enqueue(3) – (9,7,3)enqueue(5) – (9,7,3,5)dequeue() 9 (7,3,5)

Page 78: Notas Estructuras

78 Pilas y colas

Aplicaciones de ejemplo

Hay varias aplicaciones posibles para las colas. Tiendas, teatros, centrosde reservacion, y otros servicios similares que tıpicamente procesen peticionesde clientes de acuerdo al principio FIFO. Una cola serıa por lo tanto unaopcion logica para una estructura de datos que maneje el procesamiento detransacciones para tales aplicaciones.

3.3.2. Una interfaz cola en Java

Una interfaz de Java para el ADT cola se proporciona en el listado 3.10.Esta interfaz generica indica que objetos de tipo arbitrario pueden ser in-sertados en la cola. Por lo tanto, no se tiene que usar conversion explıcitacuando se remuevan elementos.

Los metodos size e isEmpty tienen el mismo significado que sus contra-partes en el ADT pila. Estos dos metodos, al igual que el metodo front, sonconocidos como metodos accesores, por su valor regresado y no cambian elcontenido de la estructura de datos.�/**

* Interfaz para una cola: una coleccion de elementos que son insertados

* y removidos de acuerdo con el principio primero en entrar , primero en

* salir.

*

* @see EmptyQueueException

*/

public interface Queue <E> {

/**

* Regresa el numero de elementos en la cola.

* @return numero de elementos en la cola.

*/

public int size ();

/**

* Indica si la cola esta vacıa

* @return true si la cola esta vacıa , false de otro modo.

*/

public boolean isEmpty ();

/**

* Revisa el elemento en el frente de la cola.

* @return elemento en el frente de la cola.

* @exception EmptyQueueException si la cola esta vacıa.

*/

public E front() throws EmptyQueueException;

/**

* Insertar un elemento en la zaga de la cola.

* @param elemento nuevo elemento a ser insertado.

*/

public void enqueue (E element );

Page 79: Notas Estructuras

3.3 Colas 79

/**

* Quitar el elemento del frente de la cola.

* @return elemento quitado.

* @exception EmptyQueueException si la cola esta vacıa.

*/

public E dequeue () throws EmptyQueueException;

}�Listado 3.10: Interfaz Queue documentada con comentarios en estilo Javadoc.

3.3.3. Una implementacion simple de la cola con unarreglo

Se presenta una realizacion simple de una cola mediante un arreglo, C,de capacidad fija, guardando sus elementos. Como la regla principal con eltipo ADT cola es que se inserten y borren los objetos de acuerdo al principioFIFO, se debe decidir como se va a llevar el frente y la parte posterior de lacola.

Una posibilidad es adaptar la aproximacion empleada para la implemen-tacion de la pila, dejando que C[0] sea el frente de la cola y permitiendo quela cola crezca desde allı. Sin embargo, esta no es una solucion eficiente, ya quese requiere que se muevan todos los elementos hacia adelante una celda cadavez que se haga la operacion dequeue. Tal implementacion podrıa tomar porlo tanto tiempo O(n) para realizar el metodo dequeue, donde n es el numeroactual de objetos en la cola. Si se quiere lograr tiempo constante para cadametodo de la cola, se necesita una aproximacion diferente.

Usando un arreglo en un forma circular

Para evitar mover objetos una vez que son colocados en C, se definen dosvariables f y c, que tienen los siguientes usos:

f es un ındice a la celda de C guardando el primer elemento de la cola,el cual es el siguiente candidato a ser eliminado por una operaciondequeue, a menos que la cola este vacıa, en tal caso f = c.

c es un ındice a la siguiente celda del arreglo disponible en C.

Inicialmente, se asigna f = c = 0, lo cual indica que la cola esta vacıa.Cuando se quita un elemento del frente de la cola, se incrementa f para indi-car la siguiente celda. Igualmente, cuando se agrega un elemento, se guarda

Page 80: Notas Estructuras

80 Pilas y colas

este en la celda C[c] y se incrementa c para indizar la siguiente celda dispo-nible en C. Este esquema permite implementar los metodos front, enqueue,y dequeue en tiempo constante, esto es, tiempo O(1). Sin embargo, todavıahay un problema con esta aproximacion.

Considerar, por ejemplo, que sucede si repetidamente se agrega y se quitaun solo elemento N veces. Se tendrıa f = c = N . Si se intentara entoncesinsertar el elemento una vez mas, se podrıa obtener un error de arreglo fuerade lımites, ya que las N localidades validas en C son de C[0] a C[N − 1],aunque hay espacio en la cola para este caso. Para evitar este problema ypoder utilizar todo el arreglo C, se permite que los ındices reinicien al finalde C. Esto es, se ve ahora a C como un “arreglo circular” que va de C[0] aC[N − 1] y entonces inmediatamente regresan a C[0] otra vez.

Implementar esta vista circular de C es facil. Cada vez que se incrementef o c, se calcula este incremento como “(f + 1) mod N” o “(c+ 1) mod N”,respectivamente.

El operador “mod” es el operador modulo, el cual es calculado tomandoel residuo despues de la division entera. Por ejemplo, 14 dividido por 4 es 3con residuo 2, ası 14 mod 4 = 2. Especıficamente, dados enteros x y y talque x ≥ 0 y y ≥ 0, se tiene x mod y = x−bx/ycy. Esto es, si r = x mod y,entonces hay un entero no negativo q, tal que x = qy + r. Java usa “ %” paradenotar el operador modulo. Mediante el uso del operador modulo, se puedever a C como un arreglo circular e implementar cada metodo de la cola enuna cantidad constante de tiempo, es decir, tiempo O(1). Se describe comousar esta aproximacion para implementar una cola enseguida.

Algoritmo size():Regresar N − f + c mod N

Algoritmo isEmpty():Regresar (f = c)

Algoritmo front():Si isEmpty() Entonces

lanzar una EmptyQueueExceptionRegresar C[f ]

Algoritmo enqueue(e):Si size()= N − 1 Entonces

Page 81: Notas Estructuras

3.3 Colas 81

lanzar una FullQueueExceptionC[c]← er ← (c + 1) mod N

Algoritmo dequeue():Si isEmpty() Entonces

lanzar una EmptyQueueExceptione← C[f ]C[f ]← nullf ← (f + 1) mod NRegresar e

La implementacion anterior contiene un detalle importante, la cual podrıaser ignorada al principio. Considerar la situacion que ocurre si se agregan Nobjetos en C sin quitar ninguno de ellos. Se tendrıa f = c, la cual es la mismacondicion que ocurre cuando la cola esta vacıa. Por lo tanto, no se podrıadecir la diferencia entre una cola llena y una vacıa en este caso. Existen variasformas de manejarlo.

La solucion que se describe es insistir que C no puede tener mas deN − 1 objetos. Esta regla simple para manejar una cola llena se consideraen el algoritmo enqueue dado previamente. Para senalar que no se puedeninsertar mas elementos en la cola, se emplea una excepcion especıfica, llamadaFullQueueException. Para determinar el tamano de la cola se hace conla expresion (N − f + c) mod N , la cual da el resultado correcto en unaconfiguracion “normal”, cuando f ≤ c, y en la configuracion “envuelta”cuando c < f . La implementacion Java de una cola por medio de un arregloes similar al de la pila, y se deja como ejercicio.

Al igual que con la implementacion de la pila con arreglo, la unica realdesventaja de la implementacion de la cola con arreglo es que artificialmentese pone la capacidad de la cola a algun valor fijo. En una aplicacion real,se podrıa actualmente necesitar mas o menos capacidad que esta, pero si setiene una buena estimacion de la capacidad, entonces la implementacion conarreglo es bastante eficiente.

Page 82: Notas Estructuras

82 Pilas y colas

3.3.4. Implementando una cola con una lista genericaligada

Se puede implementar eficientemente el ADT cola usando una lista generi-ca simple ligada. Por razones de eficiencia, se escoge el frente de la cola paraque este en la cabeza de la lista, y la parte posterior de la cola para queeste en la cola de la lista. De esta forma se quita de la cabeza y se inserta enel final. Para esto se necesitan mantener referencias en los dos nodos cabezay cola de la lista. Se presenta a continuacion una implementacion para elmetodo queue de la cola en el listado 3.11 y para dequeue en el listado 3.12.�

public void enqueue(E elem) {

Nodo <E> nodo = new Nodo <E>();

nodo.setElemento(elem);

nodo.setSig(null); // nodo sera el nuevo nodo cola

if (tam == 0)

cabeza = nodo; // caso especial de una cola previamente vacıa

else

cola.setSig(nodo); // agregar nodo en la cola de la lista

cola = nodo; // actualizar la referencia al nodo cola

tam ++;

}�Listado 3.11: Implementacion del metodo enqueue del ADT cola por mediode una lista simple ligada usando nodos de la clase Nodo.�

public E dequeue () throws EmptyQueueException {

if (tam == 0)

throw new EmptyQueueException("La cola esta vacıa.");

E e = cabeza.getElemento ();

cabeza = cabeza.getSig ();

tam --;

if (tam == 0)

cola = null; // la cola esta ahora vacıa

return e;

}�Listado 3.12: Implementacion del metodo dequeue del ADT cola por mediode una lista simple ligada usando nodos de la clase Nodo.

Cada uno de los metodos de la implementacion del ADT cola con unaimplementacion de lista ligada simple corre en tiempo O(1). Se evita la ne-cesidad de indicar un tamano maximo para la cola, como se hizo en la im-plementacion de la cola basada en un arreglo, pero este beneficio viene aexpensas de incrementar la cantidad de espacio usado por elemento. A pe-sar de todo, los metodos en la implemetacion de la cola con la lista ligadason mas complicados que lo que se desearıa, se debe tener cuidado especial

Page 83: Notas Estructuras

3.3 Colas 83

de como manejar los casos especiales donde la cola esta vacıa antes de unenqueue o cuando la cola se vacıa despues de un dequeue.

3.3.5. Planificador Round Robin

Un uso popular de la estructura de datos cola es para implementar unplanificador round robin, donde se itera a traves de una coleccion de elementosen forma circular y se “atiende” cada elemento mediante la realizacion deuna accion dada sobre este. Dicha pauta es usada, por ejemplo, para asignarequitativamente un recurso que debera ser compartido por una coleccionde clientes. Por ejemplo, se puede usar un planificador round robin paraasignar una parte del tiempo del CPU a varias aplicaciones ejecutandoseconcurrentemente en una computadora.

Se puede implementar un planificador round robin usando una cola, C,mediante la repeticion de los siguientes pasos:

1. e← C.dequeue()

2. Servir elemento e

3. C.enqueue(e)

El problema de Josefo

En el juego de ninos de la “Papa Caliente”, un grupo de n ninos sentadosen cırculo se pasan un objeto, llamado la “papa” alrededor del cırculo. Eljuego se inicia con el nino que tenga la papa pasandola al siguiente en elcırculo, y estos continuan pasando la papa hasta que un coordinador suenauna campana, en donde el nino que tenga la papa debera dejar el juego y pasarla papa al siguiente nino en el cırculo. Despues de que el nino seleccionadodeja el juego, los otros ninos estrechan el cırculo. Este proceso es entoncescontinuado hasta que quede solamente un nino, que es declarado el ganador.Si el coordinador siempre usa la estrategia de sonar la campana despues deque la papa ha pasado k veces, para algun valor fijo k, entonces determinarel ganador para una lista de ninos es conocido como el problema de Josefo.

Resolviendo el problema de Josefo con una cola

Se puede resolver el problema de Josefo para una coleccion de n elementosusando una cola, mediante la asociacion de la papa con el elemento en el

Page 84: Notas Estructuras

84 Pilas y colas

frente de la cola y guardando elementos en la cola de acuerdo a su ordenalrededor del cırculo. Por lo tanto, pasar la papa es equivalente a retirar unelemento de la cola e inmediatamente agregarlo otra vez. Despues de queeste proceso se ha realizado k veces, se quita el elemento del frente usandodequeue y descartandolo. Se muestra en el listado 3.13, un programa Javacompleto para resolver el problema de Josefo, el cual describe una solucionque se ejecuta en tiempo O(nk).�public class Josefo {

/** Solucion del problema de Josefo usando una cola. */

public static <E> E Josefo(Queue <E> C, int k) {

if (C.isEmpty ()) return null;

while (C.size() > 1 ) {

System.out.println(" Cola: " + C + " k = " + k);

for (int i=0; i<k; i++)

C.enqueue(C.dequeue ()); // mover el elemento del frente al final

E e = C.dequeue (); // quitar el elemento del frente de la colecccion

System.out.println("\t"+e+" esta eliminado");

}

return C.dequeue (); // el ganador

}

/** Construir una cola desde un arreglo de objetos */

public static <E> Queue <E> construirCola(E a[]) {

Queue <E> C = new ColaNodo <E>();

for(E e: a)

C.enqueue(e);

return C;

}

/** Metodo probador */

public static void main(String [] args) {

String [] a1 = {"Jorge","Pedro","Carlos"};

String [] a2 = {"Gabi","To~no","Paco","Mari","Ivan","Alex","Nestor"};

String [] a3 = {"Rigo","Edgar","Karina","Edgar","Temo","Hector"};

System.out.println("Primer ganador es "+Josefo(construirCola(a1),7));

System.out.println("Segundo ganador es "+Josefo(construirCola(a2) ,10));

System.out.println("Tercer ganador es "+Josefo(construirCola(a3),2));

}

}�Listado 3.13: Un programa Java completo para resolver el problema de Josefousando una cola. La clase ColaNodo es mostrada en los listados 3.11 y 3.12.

3.4. Colas con doble terminacion

Considerar una estructura de datos parecida a una cola que soporte inser-cion y borrado tanto en el frente y la parte posterior de la cola. Tal extensionde una cola es llamada una cola con doble terminacion o en ingles double-ended queue, o deque, esta ultima se pronuncia ‘dek’, para evitar confusion

Page 85: Notas Estructuras

3.4 Colas con doble terminacion 85

con el metodo dequeue de un ADT cola regular, que se pronuncia como‘dikiu’

3.4.1. El tipo de dato abstracto deque

El tipo de dato abstracto es mas completo que los ADT pila y cola. Losmetodos fundamentales del ADT deque son los siguientes:

addFirst(e): inserta un nuevo elemento e al inicio de la deque.addLast(e): inserta un nuevo elemento e al final de la deque.

removeFirst(): retira y regresa el primer elemento de la deque; un errorocurre si la deque esta vacıa.

removeLast(): retira y regresa el ultimo elemento de la deque; un errorocurre si la deque esta vacıa.

Adicionalmente, el ADT deque podrıa tambien incluir los siguientes meto-dos de apoyo:

getFirst(): regresa el primer elemento de la deque; un error ocurresi la deque esta vacıa.

getLast(): regresa el ultimo elemento de la deque; un error ocurresi la deque esta vacıa.

size(): regresa el numero de elementos de la deque.isEmpty(): determina si la deque esta vacıa.

La siguiente tabla muestra una serie de operaciones y sus efectos sobreuna deque D inicialmente vacıa de objetos enteros. Por simplicidad, se usanenteros en vez de objetos enteros como argumentos de las operaciones.

Operacion Salida DaddFirst(3) – (3)addFirst(5) – (5,3)removeFirst() 5 (3)addLast(7) – (3,7)

removeFirst() 3 (7)removeLast() 7 ()removeFirst() “error” ()

isEmpty() true ()

Page 86: Notas Estructuras

86 Pilas y colas

3.4.2. Implementando una deque

Como la deque requiere insercion y borrado en ambos extremos de unalista, usando una lista simple ligada para implementar una deque serıa in-eficiente. Sin embargo, se puede usar una lista doblemente enlazada, paraimplementar una deque eficientemente. La insercion y remocion de elemen-tos en cualquier extremo de una lista doblemente enlazada es directa parahacerse en tiempo O(1), si se usan nodos centinelas para la cabeza y la cola.

Para una insercion de un nuevo elemento e, se puede tener acceso al nodop que estara antes de e y al nodo q que estara despues del nodo e. Parainsertar un nuevo elemento entre los nodos p y q, cualquiera o ambos deestros podrıan ser centinelas, se crea un nuevo nodo t, teniendo los enlacesprev y sig de t referencia a p y q respectivamente.

De igual forma, para quitar una elemento guardado en un nodo t, sepuede acceder los nodos p y q que estan a los lados de t, y estos nodosdeberan existir, ya que se usan centinelas. Para quitar el nodo t entre losnodos p y q, se tiene que hacer que p y q se apunten entre ellos en vez de t.No se requiere cambiar ninguno de los campos en t, por ahora t puede serreclamado por el colector de basura, ya que no hay nadie que este apuntandoa t.

Todos los metodos del ADT deque, como se describen previamente, estanincluidos en la clase java.util.LinkedList<E>. Por lo tanto, si se necesitausar una deque y no se desea implementarla desde cero, se puede simplementeusar la clase anterior.

De cualquier forma, se muestra la interfaz Deque en el listado 3.14 y unaimplementacion de esta interfaz en el listado 3.15.�/**

* Interfaz para una deque: una coleccion de objetos que pueden ser

* insertados y quitados en ambos extremos; un subconjunto de los

* metodos de java.util.LinkedList.

*

*/

public interface Deque <E> {

/**

* Regresa el numero de elementos en la deque.

*/

public int size ();

/**

* Determina si la deque esta vacıa.

*/

public boolean isEmpty ();

/**

* Regresa el primer elemento; una excepcion es lanzada si la deque

Page 87: Notas Estructuras

3.4 Colas con doble terminacion 87

* esta vacıa.

*/

public E getFirst () throws EmptyDequeException;

/**

* Regresa el ultimo elemento; una excepcion es lanzada si la deque

* esta vacıa.

*/

public E getLast () throws EmptyDequeException;

/**

* Insertar un elemento para que sea el primero en la deque.

*/

public void addFirst (E element );

/**

* Insertar un elemento para que sea el ultimo en la deque.

*/

public void addLast (E element );

/**

* Quita el primer elemento; una excepcion es lanzada si la deque

* esta vacıa.

*/

public E removeFirst () throws EmptyDequeException;

/**

* Quita el ultimo elemento; una excepcion es lanzada si la deque

* esta vacıa.

*/

public E removeLast () throws EmptyDequeException;

}�Listado 3.14: Interfaz Deque documentada con comentarios en estilo Javadoc.Se usa el tipo parametrizado generico E con lo cual una deque puede contenerelementos de cualquier clase especificada.�/**

* Implementacion de la interfaz Deque mediante una lista doblemente

* enlazada. Esta clase usa la clase NodoDL , la cual implmenta un nodo de

* la lista.

*

*/

public class DequeNodo <E> implements Deque <E> {

protected NodoDL <E> cabecera , terminacion; // centinelas

protected int tam; // numero de elementos

/** Crea una deque vacıa. */

public DequeNodo () { // inicializar una deque vacıa

cabecera = new NodoDL <E>();

terminacion = new NodoDL <E>();

cabecera.setSig(terminacion ); // hacer que cabecera apunte a terminacion

terminacion.setPrev(cabecera ); // hacer que terminacion apunte a cabecera

tam = 0;

}

/**

* Regresar el tama~no de la deque , esto es el numero de elementos que tiene.

* @return Numero de elementos en la deque

Page 88: Notas Estructuras

88 Pilas y colas

*/

public int size() {

return tam;

}

/**

* Esta funcion regresa true si y solo si la deque esta vacıa.

* @return true si la deque esta vacıa , false de otro modo.

*/

public boolean isEmpty () {

if (tam == 0)

return true;

return false;

}

/**

* Revisar el primer elemento sin modificar la deque.

* @return El primer elemento en la secuencia.

*/

public E getFirst () throws EmptyDequeException {

if (isEmpty ())

throw new EmptyDequeException("Deque esta vacıa.");

return cabecera.getSig (). getElemento ();

}

public E getLast () throws EmptyDequeException {

if (isEmpty ())

throw new EmptyDequeException("Deque esta vacıa.");

return terminacion.getPrev (). getElemento ();

}

public void addFirst(E o) {

NodoDL <E> segundo = cabecera.getSig ();

NodoDL <E> primero = new NodoDL <E>(o, cabecera , segundo );

segundo.setPrev(primero );

cabecera.setSig(primero );

tam ++;

}

public void addLast(E o) {

NodoDL <E> penultimo = terminacion.getPrev ();

NodoDL <E> ultimo = new NodoDL <E>(o, penultimo , terminacion );

penultimo.setSig(ultimo );

terminacion.setPrev(ultimo );

tam ++;

}

public E removeFirst () throws EmptyDequeException {

if (isEmpty ())

throw new EmptyDequeException("Deque esta vacıa.");

NodoDL <E> primero = cabecera.getSig ();

E e = primero.getElemento ();

NodoDL <E> segundo = primero.getSig ();

cabecera.setSig(segundo );

segundo.setPrev(cabecera );

tam --;

return e;

Page 89: Notas Estructuras

3.4 Colas con doble terminacion 89

}

public E removeLast () throws EmptyDequeException {

if (isEmpty ())

throw new EmptyDequeException("Deque esta vacıa.");

NodoDL <E> ultimo = terminacion.getPrev ();

E e = ultimo.getElemento ();

NodoDL <E> penultimo = ultimo.getPrev ();

terminacion.setPrev(penultimo );

penultimo.setSig(terminacion );

tam --;

return e;

}

}�Listado 3.15: Clase DequeNodo implementando la interfaz Deque con la claseNodoDL (que no se muestra). NodoDL es un nodo de una lista doblementeenlazada generica.

Page 90: Notas Estructuras

90 Pilas y colas

Page 91: Notas Estructuras

Capıtulo 4

Listas e Iteradores

4.1. Listas Arreglo

Suponer que se tiene una coleccion S de n elementos guardados en uncierto orden lineal, de este modo se puede referir a los elementos en S comoprimero, segundo, tercero, etc. Tal coleccion es generalmente referida comouna lista o secuencia. Se puede referir unicamente a cada elemento e en Susando un entero en el rango [0, n− 1] esto es igual al numero de elementosde S que preceden a e en S. El ındice de un elemento e en S es el numerode elementos que estan antes que e en S. Por lo tanto, el primer elementoen S tiene ındice cero y el ultimo ındice tiene ındice n − 1. Tambien, si unelemento de S tiene ındice i, su elemento previo, si este existe, tiene ındicei− 1, y su siguiente elemento, si este existe, tiene ındice i+ 1. Este conceptode ındice esta reclacionado al del rango de un elemento en una lista, el cuales usualmente definido para ser uno mas que su ındice; por lo que el primerelemento esta en el rango uno, el segundo esta en rango dos, etc.

Una secuencia que soporte acceso a sus elementos mediante sus ındices esllamado una lista arreglo o tambien con el termino mas viejo vector . Comola definicion de ındice es mas consistente con la forma como los arreglos sonındizados en Java y otros lenguajes de programacion, como C y C++, sehara referencia al lugar donde un elemento es guardado en una lista arreglocomo su “ındice”, no su “rango”.

Este concepto de ındice es simple pero potente, ya que puede ser usadopara indicar donde insertar un nuevo elemento en una lista o de donde quitarun elemento viejo.

Page 92: Notas Estructuras

92 Listas e Iteradores

4.1.1. El tipo de dato abstracto lista arreglo

Como un ADT, una lista arreglo S tiene los siguientes metodos, ademasde los metodos estandares size() y isEmpty():

get(i): regresa el elemento de S con ındice i; una condicion deerror ocurre si i < 0 o i > size()− 1.

set(i, e): reemplaza con e y regresa el elemento en el ındice i; unacondicion de error ocurre si i < 0 o i > size()− 1.

add(i, e): inserta un nuevo elemnto e en S para tener ındice i; unacondicion de error ocurre si i < 0 o i > size()− 1.

remove(i): quita de S el elemento en el ındice i; una condicion deerror ocurre si i < 0 o i > size()− 1.

No se insiste que una arreglo deberıa ser usado para implementar una listade arreglo, ası el elemento en el ındice 0 esta guardado en el ındice 0 en elarreglo, aunque esta es una posibilidad muy natural. La definicion de ındiceofrece una forma para referirse al “lugar” donde un elemento esta guardadoen una secuencia sin tener que preocuparse acerca de la implementacionexacta de esa secuencia. El ındice de un elemento podrıa cambiar cuando lasecuencia sea actualizada, como se muestra en el siguiente ejemplo.

Operacion Salida Sadd(0,7) – (7)add(0,4) – (4,7)get(1) 7 (4,7)add(2,2) – (4,7,2)get(3) “error” (4,7,2)

remove(1) 7 (4,2)add(1,5) – (4,5,2)add(1,3) – (4,3,5,2)add(4,9) – (4,3,5,2,9)get(2) 5 (4,3,5,2,9)set(3,8) 2 (4,3,5,8,9)

4.1.2. El patron adaptador

Las clase son frecuentemente escritas para dar funcionalidad similar aotras clases. El patron de diseno adaptador se aplica a cualquier contexto

Page 93: Notas Estructuras

4.1 Listas Arreglo 93

donde se quiere modificar una clase existente para que sus metodos empatencon los de una clase relacionada diferente o interfaz. Una forma general paraaplicar el patron adaptador es definir la nueva clase de tal forma que estacontenga una instancia de la clase vieja como un campo oculto, e implementarcada metodo de la nueva clase usando metodos de esta variable de instanciaoculta. El resultado de aplicar el patron adaptador es que una nueva claseque realiza casi las mismas funciones que una clase previa, pero en una formamas conveniente, ha sido creada.

Con respecto al ADT lista arreglo, se observa que este ADT es suficientepara definir una clase adaptadora para el ADT deque, como se ve en el cuadro4.4:

Metodo Deque Realizacion con metodosde Lista Arreglo

size(), isEmpty() size(),isEmpty()getFirst() get(0)getLast() get(size()-1)addFirst(e) add(0,e)addLast(e) add(size(),e)removeFirst() remove(0)removeLast() remove(size()-1)

Cuadro 4.4: Realizacion de una deque por medio de una lista arreglo.

4.1.3. Una implementacion simple usando un arreglo

Una opcion obvia para implementar el ADT lista arreglo es usar un arregloA, donde A[i] guarda una referencia al elemento con ındice i. Se escoge eltamano N del arreglo A lo suficientemente grande, y se mantiene el numerode elementos en una variable de instancia, n < N .

Los detalles de esta implementacion del ADT lista arreglo son simples.Para implementar la operacion get(i), por ejemplo, solo se regresa A[i]. Laimplementacion de los metodos add(i, e) y remove(i) son dados a continua-cion. Una parte importante y consumidora de tiempo de esta implementacioninvolucra el desplazamiento de elementos hacia adelante y hacia atras paramantener las celdas ocupadas en el arreglo de forma continua. Estas opera-

Page 94: Notas Estructuras

94 Listas e Iteradores

ciones de desplazamiento son requeridas para mantener la regla de guardarsiempre un elemento cuyo ındice en la lista es i en el ındice i en el arreglo A.

Algoritmo add(i, e):Para j ← n− 1, n− 2, . . . , i HacerA[j + 1]← A[j] {hacer espacio para el nuevo elemento}

A[i]← en← n + 1

Algoritmo remove(i):e← A[i] { e es una variable temporal }Para j ← i, i + 1, . . . , n− 2 HacerA[j]← A[j + 1] {llenar el espacio del elemento quitado}

n← n− 1Regresar e

El rendimiento de una implementacion simple con arreglo

Los metodos isEmpty, size, get y set se ejecutan en tiempo O(1), perolos metodos de insercion y borrado pueden tomar mas tiempo que este. Enparticular, add(i, e) se ejecuta en tiempo O(n). En efecto, el peor caso paraesta operacion ocurre cuando i = 0, ya que todos los n elementos existentestienen que ser desplazados hacia adelante. Un argumento similar se aplicaal metodo remove(i), el cual corre en tiempo O(n), porque se tiene quedesplazar hacia atras n − 1 elementos en el peor caso cuando i = 0. Dehecho, suponiendo que cada ındice posible es igualmente probable de que seapasado como un argumento a estas operaciones, su tiempo promedio es O(n),ya que se tienen que desplazar n/2 elementos en promedio.

Observando mas detenidamente en add(i, e) y remove(i), se deduce quecada tiempo de ejecucion es O(n− i+ 1), solamente para aquellos elementosen el ındice i y mayores tienen que ser desplazadas hacia atras o adelante.Ası, insertar y quitar una elemento al final de una lista arreglo, usando losmetodos add(n, e) y remove(n−1), respectivamente toman tiempo O(1) cadauno. Por otra parte, esta observacion tiene una consecuencia interesante parala adaptacion del ADT lista arreglo al ADT deque dado en la seccion 3.4. Siel ADT lista arreglo en este caso es implementado por medio de un arreglocomo se describe previamente, entonces los metodos addLast y removeLast

Page 95: Notas Estructuras

4.1 Listas Arreglo 95

del deque se ejecutan en tiempo O(1). Sin embargo, los metodos addFirst yremoveFirst del deque se ejecutan en tiempo O(n).

Actualmente, con un poco de esfuerzo, se puede produir una implemen-tacion basada en arreglo del ADT lista arreglos que logre tiempo O(1) parainserciones y borrados en el ındice 0, al igual que inserciones y borrados alfinal de la lista arreglo. Para lograrlo esto se requiere que se de en la regla queun elemento en el ındice es guardado en el arreglo en el ındice i, sin embargo,se tendrıa que usar una aproximacion de un arreglo circular como el que fueusado en la seccion 3.3 para implementar una cola.

4.1.4. Una interfaz simple y la clase java.util.ArrayList

Para prepararse para construir una implementacion Java del ADT listaarreglo, se muestra, en el listado 4.1, una interfaz Java, IndexList, que cap-tura los metodos principales del ADT lista de arreglo. En este caso, se usa unaIndexOutOfBoundsException para senalar un argumento ındice invalido.�/**

* Una interfaz para listas de arreglos.

*/

public interface IndexList <E> {

/** Regresa el numero de elementos en esta lista. */

public int size ();

/** Prueba si la lista vacıa. */

public boolean isEmpty ();

/** Insertar un elemento e para estar en el ındice i,

* desplazando todos los elementos despues de este. */

public void add(int i, E e)

throws IndexOutOfBoundsException;

/** Regresar el elemento en el ındice i, sin quitarlo. */

public E get(int i)

throws IndexOutOfBoundsException;

/** Quita y regresa el elemento en el ındice i,

* desplazando los elementos despues de este. */

public E remove(int i)

throws IndexOutOfBoundsException;

/** Reemplaza el elemento en el ındice i con e,

* regresando el elemento previo en i. */

public E set(int i, E e)

throws IndexOutOfBoundsException;

}�Listado 4.1: La interfaz IndexList para el ADT lista arreglo.

Page 96: Notas Estructuras

96 Listas e Iteradores

La clase java.util.ArrayList

Java proporciona una clase, java.util.ArrayList, que implementa to-dos los metodos que se dieron previamente para el ADT lista arreglo. Estaincluye todos los metodos dados en el listado 4.1 para la interfaz IndexList.Por otra parte, la clase java.util.ArrayList tiene caracterısticas adiciona-les a las que fueron dadas en el ADT lista arreglo simplificado. Por ejemplo,la clase java.util.ArrayList tambien incluye un metodo, clear(), el cualremueve todos los elementos de la lista de arreglo, y un metodo, toArray(),el cual regresa un arreglo conteniendo todos los elementos de la lista arreglosen el mismo orden. Ademas, la clase java.util.ArrayList tambien tienemetodos para buscar en la lista, incluyendo un metodo indexOf(e), el cualregresa el ındice de la primera ocurrencia de una elemento igual a e en lalista arreglos, y un metodo lastIndexOf(e), el cual regresa el ındice de laultima ocurrencia de una elemento igual a e en la lista arreglo. Ambos meto-dos regresan el valor ındice invalido −1 si no se encuentra un elemento iguala e.

4.1.5. Implementando una lista arreglos usando Arre-glos Extendibles

Ademas de implementar los metodos de la interfaz IndexList y algunosotros metodos utiles, la clase java.util.ArrayList proporciona una carac-terıstica interesante que supera una debilidad en la implementacion simplede la lista arreglo.

La mayor debilidad de la implementacion simple para el ADT lista arre-glo dado en la seccion 4.1.3, es que este requiere una especificacion avanzadade una capacidad fija, N , para el numero total de elementos que podrıanser guardados en la lista arreglo. Si el numero actual de elementos, n, dela lista arreglo es mucho menor que N , entonces esta implementacion des-perdiciara espacio. Peor aun, si n se incrementa y pasa N , entonces estaimplementacion tronara.

Para evitar lo anterior, java.util.ArrayList usa una tecnica interesan-te de arreglo extensible ası nunca se debe preocupar de que el arreglo seadesbordado cuando se usa esta clase.

Al igual que la clase java.util.ArrayList, se proporciona un mediopara crecer el arreglo A que guarda los elementos de una lista arreglo S. Porsupuesto, en Java y en otros lenguajes de programacion, no se puede crecer

Page 97: Notas Estructuras

4.1 Listas Arreglo 97

el arreglo A; su capacidad esta fija a algun numero N , como ya se ha visto.Cuando un desbordamiento ocurre, esto es, cuando n = N y se hace unallamada al metodo add, se realizan los siguientes pasos adicionales.

1. Reservar un nuevo arreglo B de capacidad 2N .

2. Hacer B[i]← A[i], para i = 0, . . . , N − 1.

3. Hacer A← B, esto es, se usa B como el arreglo que soporta a S.

4. Insertar el nuevo elemento en A.

Esta estrategia de reemplazo de arreglo es conocida como arreglo extendi-ble, que puede ser vista como extender la terminacion del arreglo subyacentepara hacerle espacio para mas elementos.

Implementando la interfaz IndexList con un arreglo extendible

Se dan porciones de una implementacion Java del ADT lista arreglo usan-do un arreglo extendible en el codigo 4.2. Esta clase solo proporciona losmedios para crecer. Un ejercicio es la implementacion para poder encoger.�/** Realizacion de una lista indizada por medio de un arreglo , el cual es

* doblado cuando el tama~no de la lista excede la capacidad del arreglo.

*

*/

public class ArrayIndexList <E> implements IndexList <E> {

private E[] A; // arreglo almacenando los elementos de la lista indizada

private int capacidad = 16; // tama~no inicial del arreglo A

private int tam = 0; // numero de elementos guardados en la lista

/** Crear la lista indizada con capacidad inicial de 16. */

public ArrayIndexList () {

A = (E[]) new Object[capacidad ]; // el compilador podrıa advertir ,

// pero esta bien

}

/** Insertar un elemento en el ındice dado. */

public void add(int r, E e)

throws IndexOutOfBoundsException {

revisarIndice(r, size() + 1);

if (tam == capacidad) { // un sobreflujo

capacidad *= 2;

E[] B =(E[]) new Object[capacidad ]; // el compilador podrıa advertir

for (int i=0; i<tam; i++)

B[i] = A[i];

A = B;

}

for (int i=tam -1; i>=r; i--) // desplazar elementos hacia adelante

A[i+1] = A[i];

A[r] = e;

Page 98: Notas Estructuras

98 Listas e Iteradores

tam ++;

}

/** Quitar el elemento guardado en el ındice dado. */

public E remove(int r)

throws IndexOutOfBoundsException {

revisarIndice(r, size ());

E temp = A[r];

for (int i=r; i<tam -1; i++) // desplazar elementos hacia atras

A[i] = A[i+1];

tam --;

return temp;�Listado 4.2: Parte de la clase ArrayIndexList realizando el ADT lista arreglopor medio de un arreglo extendible. El metodo revisarIndice vigila que elındice r esta entre 0 y n− 1. Este metodo no es mostrado.

4.2. Listas nodo

Usar un ındice no es la unica forma de referirse al lugar donde un elementoaparece en una secuencia. Si se tiene una secuencia S implementada con unalista enlazada, simple o doble, entonces podrıa ser mas natural y eficienteusar un nodo en vez de un ındice como medio de identificacion para accedera S o para actualizar. Se define en esta seccion el ADT lista nodo el cualabstrae la estructura de datos lista enlazada concreta, secciones 2.2 y 2.3,usando un ADT posicion relacionada que abstrae la nocion de “lugar” enuna lista nodo.

4.2.1. Operaciones basadas en nodos

Sea S una lista enlazada simple o doble. Se desearıa definir metodos paraS que tomen nodos como parametros y proporcionen nodos como tipos deregreso. Tales metodos podrıan dar aumentos de velocidad significativos sobrelos metodos basados en ındices, porque encontrar el ındice de un elementoen una lista enlazada requiere buscar a traves de la lista incrementalmentedesde el inicio o final, contando los elementos conforme se recorra.

Por ejemplo, se podrıa definir un metodo hipotetico remove(v) que quiteel elemento de S guardado en el nodo v de la lista. Usando un nodo como unparametro permite remover un elemento en tiempo O(1) simplemente yendodirectamente al lugar donde esta guardado el nodo y entonces “desenlazando”este nodo mediante una actualizacion a los enlaces sig y prev de sus vecinos.De igual modo, se puede inserta, en tiempo O(1), un nuevo elemento e en S

Page 99: Notas Estructuras

4.2 Listas nodo 99

con una operacion tal como addAfter(v, e), la cual indica el nodo v despuesdel cual el nodo del nuevo elemento deberıa ser insertado. En este caso se“enlaza” el nuevo nodo.

Para abstraer y unificar las diferentes formas de guardar elementos en lasvarias implementaciones de una lista, se introduce el concepto de posicion,la cual formaliza la nocion intuitiva de “lugar” de un elemento relativo a losotros en la lista.

4.2.2. Posiciones

A fin de ampliar de forma segura el conjunto de operaciones para listas, seabstrae una nocion de “posicion” que permite usar la eficiencia de la imple-mentacion de las listas simples enlazadas o dobles sin violar los principios deldiseno orientado a objetos. En este marco, se ve una lista como una coleccionde elementos que guardan cada elemento en una posicion y que mantienenestas posiciones organizadas en un orden lineal. Una posicion es por sı mismaun tipo de dato abstracto que soporta el siguiente metodo simple:

elemento(): regresa el elemento guardado en esta posicion.

Una posicion esta siempre definida relativamente, esto es, en terminos desus vecinos. En una lista, una posicion p siempre estara “despues” de algunaposicion q y “antes” de alguna posicion s, al menos que p sea la primeraposicion o la ultima. Una posicion p, la cual esta asociada con algun elemen-to e en una lista S, no cambia, aun si el ındice de e cambia en S, a menosque se quite explıcitamente e, y por lo tante, se destruya la posicion p. Porotra parte, la posicion p no cambia incluso si se reemplaza o intercambiael elemento e guardado en p con otro elemento. Estos hechos acerca de lasposiciones permiten definir un conjunto de metodos de lista basados en po-siciones que usan objetos posicion como parametros y tambien proporcionanobjetos posicion como valores de regreso.

4.2.3. El tipo de dato abstracto

Usando el concepto de posicion para encapsular la idea de “nodo” en unalista, se puede definir otro tipo de ADT secuencia llamado el ADT lista nodo.Este ADT soporta los siguientes metodos para una lista S:

Page 100: Notas Estructuras

100 Listas e Iteradores

first(): regresa la posicion del primer elemento de S; un errorocurre si S esta vacıa.

last(): regresa la posicion del ultimo elemento de S; un errorocurre si S esta vacıa.

prev(p): regresa la posicion del elemento de S precediendo al dela posicion p; un error ocurre si p esta en la primeraposicion.

next(p): regresa la posicion del elemento de S siguiendo al de laposicion p; un error ocurre si p esta en la ultima posicion.

Los metodos anteriores permiten referirse a posiciones relativas en unalista, comenzando en el inicio o final, y moviendose incrementalmente a laizquierda o a la derecha de la lista. Estas posiciones pueden intuitivamenteser pensadas como nodos en la lista, pero observar que no hay referenciasespecıficas a objetos nodos. Ademas, si se proporciona una posicion como unargumento a un metodo lista, entonces la posicion debera representar unaposicion valida en aquella lista.

Metodos de actualizacion de la lista nodo

Ademas de los metodos anteriores y los metodos genericos size e isEmpty,se incluyen tambien los siguientes metodos de actualizacion para el ADT listanodo, los cuales usan objetos posicion como parametros y regresan objetosposicion como valores de regreso algunos de ellos.

set(p, e): reemplaza el elemento en la posicion p con e, regresandoel elemento anterior en la posicion p.

addFirst(e): inserta un nuevo elemento e en S como el primer elemen-to.

addLast(e): inserta un nuevo elemento e en S como el ultimo elemen-to.

addBefore(p, e): inserta un nuevo elemento e en S antes de la posicion p.addAfter(p, e): inserta un nuevo elemento e en S despues de la posicion

p.remove(p): quita y regresa el elemento en en la posicion p en S,

invalidando esta posicion en S.

El ADT lista nodo permite ver una coleccion ordenada de objetos en

Page 101: Notas Estructuras

4.2 Listas nodo 101

terminos de sus lugares, sin preocuparnos acerca de la forma exacta comoestos lugares estan representados.

Podrıa parecer a primera vista que hay redundancia en el repertorio an-terior de operaciones para el ADT lista nodo, ya que se puede hacer la ope-racion addFirst(e) con addBefore(first(),e), y la operacion addLast(e)con addAfter(last(),e). Pero estas sustituciones solo pueden ser hechaspara una lista no vacıa.

Observar que una condicion de error ocurre si una posicion pasada comoargumento a una de las operaciones de la lista es invalida. Las razones paraque una posicion p sea invalida son:

p = null

p fue previamente borrada de la lista

p es una posicion de una lista diferente

p es la primera posicion de la lista y se llama prev(p)

p es la ultima posicion de la lista y se llama sig(p)

Se muestra enseguida una serie de operaciones para una lista nodo Sinicialmente vacıa. Se usan las variables p1, p2, etc., para denotar diferentesposiciones, y se muestra el objeto guardado actualmente en tal posicion entreparentesis.

Operacion Salida SaddFirst(8) – (8)first() p1(8) (8)

addAfter(p1, 5) – (8,5)next(p1) p2(5) (8,5)

addBefore(p2, 3) – (8,3,5)prev(p2) p3(3) (8,3,5)

addFirst(9) – (9,8,3,5)last() p2(5) (9,8,3,5)

remove(first()) 9 (8,3,5)set(p3, 7) 3 (8,7,5)

addAfter(first(), 2) – (8,2,7,6)

Page 102: Notas Estructuras

102 Listas e Iteradores

El ADT lista nodo, con su nocion incorporada de posicion, es util en unnumero de ajustes. Por ejemplo, un programa que simula un juego de cartaspodrıa simular la mano de cada persona como una lista nodo. Ya que la ma-yorıa de las personas guarda las cartas de la misma figura juntas, insertandoy sacando cartas de la mano de una persona podrıa ser implementado usandolos metodos del ADT lista nodo, con las posiciones siendo determinadas porun orden natural de las figuras. Igualmente, un editor de texto simple encajala nocion de insercion posicional y remocion, ya que tales editores tıpicamen-te realizan todas las actualizaciones relativas a un cursor , el cual representala posicion actual en la lista de caracteres de texto que esta siendo editada.

Una interfaz Java representando el ADT posicion esta dado en el listado4.3.�public interface Posicion <E> {

/** Regresa el elemento guardado en esta posicion. */

E elemento ();

}�Listado 4.3: Una interfaz Java para el ADT posicion.

Una interfaz, con los metodos requeridos, para el ADT lista nodo, llamadaListaPosicion, esta dada en el listado 4.4.�

public int size ();

/** Indica si la lista esta vacıa. */

public boolean isEmpty ();

/** Regresa el primer nodo en la lista. */

public Posicion <E> first ();

/** Regresa el ultimo nodo en la lista. */

public Posicion <E> last ();

/** Regresa el nodo siguiente a un nodo dado en la lista. */

public Posicion <E> next(Posicion <E> p)

throws InvalidPositionException , BoundaryViolationException;

/** Regresa el nodo anterior a un nodo dado en la lista. */

public Posicion <E> prev(Posicion <E> p)

throws InvalidPositionException , BoundaryViolationException;

/** Inserta un elemento en el frente de la lista ,

* regresando la nueva posicion. */

public void addFirst(E e);

/** Inserta un elemento en la parte de atras de la lista ,

* regresando la nueva posicion. */

public void addLast(E e);

/** Inserta un elemento despues del nodo dado en la lista. */

public void addAfter(Posicion <E> p, E e)

throws InvalidPositionException;

/** Inserta un elemento antes del nodo dado en la lista. */

public void addBefore(Posicion <E> p, E e)

throws InvalidPositionException;

/** Quita un nodo de la lista , regresando el elemento guardado. */

public E remove(Posicion <E> p) throws InvalidPositionException;

/** Reemplaza el elemento guardado en el nodo dado ,

Page 103: Notas Estructuras

4.2 Listas nodo 103

regresando el elemento viejo. */

public E set(Posicion <E> p, E e) throws InvalidPositionException;�Listado 4.4: Metodos requeridos para un ADT lista nodo.

La interfaz ListaPosicion usa las siguientes excepciones para indicarcondiciones de error.

BoundaryViolationException: lanzada si un intento es hecho al accederuna elemento cuya posicion esta fuera del rango de posiciones de lalista, por ejemplo llamando al metodo sig en la ultima posicion de lasecuencia.

InvalidPositionException: lanzada si una posicion dada como argumentono es valida, por ejemplo, esta es una referencia null o esta no tienelista asociada.

Otro adaptador Deque

El ADT lista nodo es suficiente para definir una clase adaptadora para elADT deque, como se muestra en la tabla 4.11.

Metodo Deque Realizacion con Metodosde la lista nodo

size(), isEmpty() size(),isEmpty()getFirst() first().elemento()getLast() last().elemento()addFirst(e) addFirst(e)addLast(e) addLast(e)removeFirst() remove(first())removeLast() remove(last())

Cuadro 4.11: Realizacion de una deque por medio de una lista nodo.

4.2.4. Implementacion de una lista doblemente enlaza-da

Si se quisiera implementar el ADT lista nodo usando una lista doblemen-te enlazada, seccion 2.3. Se puede hacer que los nodos de la lista enlazada

Page 104: Notas Estructuras

104 Listas e Iteradores

implementen el ADT posicion. Esto es, se tiene cada nodo implementandola interfaz Posicion y por lo tanto definen un metodo, elemento(), el cualregresa el elemento guardado en el nodo. Ası, los propios nodos actuan comoposiciones. Son vistos internamente por la lista enlazada como nodos, perodesde el exterior, son vistos solamente como posiciones. En la vista interna,se puede dar a cada nodo v variables de instancia prev y sig que respectiva-mente refieran a los nodos predecesor y sucesor de v, los cuales podrıan serde hecho nodos centinelas encabezado y terminacion marcando el inicio o elfinal de la lista. En lugar de usar las variables prev y sig directamente, sedefinen los metodos getPrev, setPrev, getSig, y setSig de un nodo paraacceder y modificar estas variables.

En el listado 4.5, se muestra una clase Java NodoD para los nodos deuna lista doblemente enlazada implementando el ADT posicion. Esta clasees similar a la clase DNodo mostrada en el listado 2.9, excepto que ahora losnodos guardan un elemento generico en lugar de una cadena de caracteres.Observar que las variables de instancia prev y sig en la clase NodoD sonreferencias privadas a otros objetos NodoD.�public class NodoD <E> implements Posicion <E> {

private NodoD <E> prev , sig; // Referencia a los nodos anterior y posterior

private E elemento; // Elemento guardado en esta posicion

/** Constructor */

public NodoD(NodoD <E> nvoPrev , NodoD <E> nvoSig , E elem) {

prev = nvoPrev;

sig = nvoSig;

elemento = elem;

}

// Metodo de la interfaz Posicion

public E elemento () throws InvalidPositionException {

if ((prev == null) && (sig == null))

throw new InvalidPositionException("¡La posicion no esta en una lista!");

return elemento;

}

// Metodos accesores

public NodoD <E> getSig () { return sig; }

public NodoD <E> getPrev () { return prev; }

// Metodos actualizadores

public void setSig(NodoD <E> nvoSig) { sig = nvoSig; }

public void setPrev(NodoD <E> nvoPrev) { prev = nvoPrev; }

public void setElemento(E nvoElemento) { elemento = nvoElemento; }

}�Listado 4.5: Clase NodoD realizando un nodo de una lista doblemente enlazadaimplementando la interfaz Posicion.

Dada una posicion p en S, se puede “desenvolver” p para revelar el nodo vsubyacente. Esto es realizado fundiendo la posicion a un nodo. Una vez que se

Page 105: Notas Estructuras

4.2 Listas nodo 105

tiene un nodo v, se puede, por ejemplo, implementar el metodo prev(p) conv.getPrev, a menos que el nodo regresado por v.get sea el encabezado, ental caso se senala un error. Por lo tanto, posiciones en una implementacion deuna lista doblemente enlazada puede ser soportada en una forma orientadaal objeto sin ningun gasto adicional de tiempo o espacio.

Considerar ahora como se podrıa implementar el metodo addAfter(p, e),para insertar un elemento e despues de la posicion p. Se crea un nuevo nodov para mantener el elemento e, se enlaza v en su lugar en la lista, y entoncesse actualizan las referencias sig y prev de los dos vecinos de v. Este metodoesta dado en el siguiente pseudocodigo, recordando el uso de centinelas, ob-servar que este algoritmo trabaja aun si p es la ultima posicion real.

Algoritmo addAfter(p, e):Crear un nuevo nodo vv.setElemento(e)v.setPrev(p) { enlazar v a su predecesor }v.setSig(p.getSig()) { enlazar v a su sucesor }(p.getSig()).setPrev(v) { enlazar el viejo sucesor de p a v }p.setSig(v) { enlazar p a su nuevo sucesor, v }

Los algoritmos para los metodos addBefore, addFirst, y addLast sonsimilares al metodo addAfter. Se dejan sus detalles como un ejercicio.

Enseguida, considerar el metodo remove(e), el cual quita el elemento eguardado en la posicion p. Para realizar esta operacion, se enlazan los dosvecinos de p para referirse entre ellos como nuevos vecinos—desenlazando p.Observar que despues de que p esta desenlazado, no hay nodos apuntando ap; por lo tanto, el recolector de basura puede reclamar el espacio de p. Estealgoritmo esta dado en el siguiente pseudocodigo, recordando el uso de loscentinelas encabezado y terminacion. Este algoritmo trabaja aun si p es laprimera posicion, la ultima, o solamente una posicion real en la lista.

Algoritmo remove(p):t ← p.elemento() { una variable temporal para guardar el valor de regreso}

(p.getPrev()).setSig(p.getSig()) { desenlazar p }(p.getSig()).setPrev(p.getPrev())

Page 106: Notas Estructuras

106 Listas e Iteradores

p.setPrev(null) { invalidando la posicion p }p.setSig(null)regresar t

En conclusion, usando una lista doblemente enlazada, se pueden ejecu-tar todos los metodos del ADT lista nodo en tiempo O(1). Ası, una listadoblemente enlazada es una implementacion eficiente del ADT lista nodo.

Una implementacion de la lista nodo en Java

Porciones de la clase Java ListaNodoPosicion, la cual implementa elADT lista nodo usando una lista doblemente enlazada, se muestra en el lis-tado 4.6. El listado 4.6 muestra variables de instacia de ListaNodoPosicion,su constructor, y un metodo revisaPosicion, el cual hace realiza comproba-ciones de seguridad y “desenvuelve” una posicion, convirtiendola a un NodoD.Tambien se muestran metodos adicionales accesores y actualizadores.�public class ListaNodoPosicion <E> implements ListaPosicion <E> {

protected int numElts; // numero de elementos en la lista

protected NodoD <E> cabecera , terminacion; // centinelas especiales

/** Constructor que crea una lista vacıa; tiempo O(1) */

public ListaNodoPosicion () {

numElts = 0;

cabecera = new NodoD <E>(null , null , null); // crea cabecera

terminacion = new NodoD <E>(cabecera , null , null); // crea terminacion

cabecera.setSig(terminacion ); // hacer que cabecera apunte a terminacion

}

/** Revisa si la posicion es valida para esta lista y la convierte a

* NodoD si esta es valida: tiempo O(1) */

protected NodoD <E> revisaPosicion(Posicion <E> p)

throws InvalidPositionException {

if (p == null)

throw new InvalidPositionException

("Posicion null pasada a ListaNodo");

if (p == cabecera)

throw new InvalidPositionException

("El nodo cabecera no es una posicion valida");

if (p == terminacion)

throw new InvalidPositionException

("El nodo terminacion no es una posicion valida");

try {

NodoD <E> temp = (NodoD <E>) p;

if ((temp.getPrev () == null) || (temp.getSig () == null))

throw new InvalidPositionException

("Posicion no pertenece a una ListaNodo valida");

return temp;

} catch (ClassCastException e) {

throw new InvalidPositionException

("Posicion es de un tipo incorrecto para esta lista");

}

Page 107: Notas Estructuras

4.2 Listas nodo 107

}

/** Regresa el numero de elementos en la lista; tiempo O(1) */

public int size() { return numElts; }

/** Valida si la lista esta vacıa; tiempo O(1) */

public boolean isEmpty () { return (numElts == 0); }

/** Regresa la primera posicion en la lista; tiempo O(1) */

public Posicion <E> first ()

throws EmptyListException {

if (isEmpty ())

throw new EmptyListException("La lista esta vacıa");

return cabecera.getSig ();

}

/** Regresa la ultima posicion en la lista; tiempo O(1) */

public Posicion <E> last()

throws EmptyListException {

if (isEmpty ())

throw new EmptyListException("La lista esta vacıa");

return terminacion.getPrev ();

}

/** Regresa la posicion anterior de la dada; tiempo O(1) */

public Posicion <E> prev(Posicion <E> p)

throws InvalidPositionException , BoundaryViolationException {

NodoD <E> v = revisaPosicion(p);

NodoD <E> prev = v.getPrev ();

if (prev == cabecera)

throw new BoundaryViolationException

("No se puede avanzar mas alla del principio de la lista");

return prev;

}

/** Regresa la posicion despues de la dada; tiempo O(1) */

public Posicion <E> next(Posicion <E> p)

throws InvalidPositionException , BoundaryViolationException {

NodoD <E> v = revisaPosicion(p);

NodoD <E> next = v.getSig ();

if (next == terminacion)

throw new BoundaryViolationException

("No se puede avanzar mas alla del final de la lista");

return next;

}

/** Insertar el elemento dado antes de la posicion dada: tiempo O(1) */

public void addBefore(Posicion <E> p, E elemento)

throws InvalidPositionException {

NodoD <E> v = revisaPosicion(p);

numElts ++;

NodoD <E> nodoNuevo = new NodoD <E>(v.getPrev(), v, elemento );

v.getPrev (). setSig(nodoNuevo );

v.setPrev(nodoNuevo );

}

/** Insertar el elemento dado despues de la posicion dada: tiempo O(1) */

public void addAfter(Posicion <E> p, E elemento)

throws InvalidPositionException {

NodoD <E> v = revisaPosicion(p);

numElts ++;

NodoD <E> nodoNuevo = new NodoD <E>(v, v.getSig(), elemento );

v.getSig (). setPrev(nodoNuevo );

v.setSig(nodoNuevo );

}

Page 108: Notas Estructuras

108 Listas e Iteradores

/** Insertar el elemento dado al inicio de la lista , regresando

* la nueva posicion; tiempo O(1) */

public void addFirst(E elemento) {

numElts ++;

NodoD <E> nodoNuevo = new NodoD <E>(cabecera , cabecera.getSig(), elemento );

cabecera.getSig (). setPrev(nodoNuevo );

cabecera.setSig(nodoNuevo );

}

/** Insertar el elemento dado al final de la lista , regresando

* la nueva posicion; tiempo O(1) */

public void addLast(E elemento) {

numElts ++;

NodoD <E> ultAnterior = terminacion.getPrev ();

NodoD <E> nodoNuevo = new NodoD <E>( ultAnterior , terminacion , elemento );

ultAnterior.setSig(nodoNuevo );

terminacion.setPrev(nodoNuevo );

}

/** Quitar la posicion dada de la lista; tiempo O(1) */

public E remove(Posicion <E> p)

throws InvalidPositionException {

NodoD <E> v = revisaPosicion(p);

numElts --;

NodoD <E> vPrev = v.getPrev ();

NodoD <E> vNext = v.getSig ();

vPrev.setSig(vNext);

vNext.setPrev(vPrev);

E vElem = v.elemento ();

// desenlazar la posicion de la lista y hacerlo invalido

v.setSig(null);

v.setPrev(null);

return vElem;

}

/** Reemplaza el elemento en la posicion dada con el nuevo elemento

* y regresar el viejo elemento; tiempo O(1) */

public E set(Posicion <E> p, E elemento)

throws InvalidPositionException {

NodoD <E> v = revisaPosicion(p);

E elemVjo = v.elemento ();

v.setElemento(elemento );

return elemVjo;

}

/** Regresa un iterador de todos los elementos en la lista. */

public Iterator <E> iterator () { return new IteradorElemento <E>(this); }

/** Regresa una coleccion iterable de todos los nodos en la lista. */

public Iterable <Posicion <E>> posiciones () { // crea una lista de posiciones

ListaPosicion <Posicion <E>> P = new ListaNodoPosicion <Posicion <E>>();

if (! isEmpty ()) {

Posicion <E> p = first ();

while (true) {

P.addLast(p); // agregar posicion p como el ult. elemento de la lista P

if (p == last ())

break;

p = next(p);

}

}

return P; // regresar P como el objeto iterable

}

Page 109: Notas Estructuras

4.2 Listas nodo 109

// Metodos de conveniencia

/** Valida si una posicion es la primera; tiempo O(1). */

public boolean isFirst(Posicion <E> p)

throws InvalidPositionException {

NodoD <E> v = revisaPosicion(p);

return v.getPrev () == cabecera;

}

/** Valida si una posicion es la ultima; tiempo O(1). */

public boolean isLast(Posicion <E> p)

throws InvalidPositionException {

NodoD <E> v = revisaPosicion(p);

return v.getSig () == terminacion;

}

/** Intercambia los elementos de dos posiciones dadas; tiempo O(1) */

public void swapElements(Posicion <E> a, Posicion <E> b)

throws InvalidPositionException {

NodoD <E> pA = revisaPosicion(a);

NodoD <E> pB = revisaPosicion(b);

E temp = pA.elemento ();

pA.setElemento(pB.elemento ());

pB.setElemento(temp);

}

/** Regresa una representacion textual de una lista nodo usando for -each */

public static <E> String forEachToString(ListaPosicion <E> L) {

String s = "[";

int i = L.size ();

for (E elem: L) {

s += elem; // moldeo implıcito del elemento a String

i--;

if (i > 0)

s += ", "; // separar elementos con una coma

}

s += "]";

return s;

}

/** Regresar una representacion textual de una lista nodo dada */

public static <E> String toString(ListaPosicion <E> l) {

Iterator <E> it = l.iterator ();

String s = "[";

while (it.hasNext ()) {

s += it.next (); // moldeo implıcito del elemento a String

if (it.hasNext ())

s += ", ";

}

s += "]";

return s;

}

/** Regresa una representacion textual de la lista */

public String toString () {

return toString(this);

}

}�Listado 4.6: Clase ListaNodoPosicion implementando el ADT lista nodocon una lista doblemente enlazada.

Page 110: Notas Estructuras

110 Listas e Iteradores

4.3. Iteradores

Un computo tıpico en una lista arreglo, lista, o secuencia es recorrer suselementos en orden, uno a la vez, por ejemplo, para buscar un elementoespecıfico.

4.3.1. Los tipos de dato abstracto Iterador e Iterable

Un iterator (iterador) es un patron de diseno de software que abstrae elproceso de explorar a traves de una coleccion de elementos un elemento ala vez. Un iterador consiste de una secuencia S, un elemento actual en S, yuna forma de pasar al siguiente elemento en S y hacerlo el elemento actual.Ası, un iterador extiende el concepto del ADT posicion que se introdujo enla seccion 4.2. De hecho, una posicion puede ser pensada como un iteradorque no va a ninguna parte. Un iterador encapsula los conceptos de “lugar”y “siguiente” en una coleccion de objetos.

Se define el ADT iterator soportando los siguientes dos metodos:

hasNext(): prueba si hay elementos dejados en el iterador.next(): regresa el siguiente elemento en el iterador.

El ADT iterador tiene la nocion de cursor del elemento “actual” en reco-rrido de una secuencia. El primer elemento en un iterador es regresado porla primera llamada al metodo next, asumiendo que el iterador contiene almenos un elemento.

Un iterador da un esquema unificado para acceder todos los elementos deuna coleccion de objetos en una forma que es independiente de la organizacionespecıfica de la coleccion. Un iterador para una lista arreglo, lista nodo, osecuencia deberan regresar los elementos de acuerdo a su orden lineal.

Iteradores simples en Java

Java proporciona un iterador a traves de su interfaz java.util.Iterator.La clase java.util.Scanner implementa esta interfaz. Esta interfaz sopor-ta un metodo adicional opcional para quitar el elemento previamente regre-sado de la coleccion. Esta funcionalidad, de quitar elementos a traves deun iterador, es algo controversial desde un punto de vista orientado al ob-jeto, sin embargo, no es sorprendente que su implementacion por las cla-ses es opcional. Java tambien da la interfaz java.util.Enumeration, la

Page 111: Notas Estructuras

4.3 Iteradores 111

cual es historicamente mas vieja que la interfaz iterador y usa los nombreshasMoreElements() y nextElement().

El tipo de dato abstracto Iterable

Para dar un mecanismo unificado generico para explorar a traves de unaestructura de datos, los ADT que guardan colecciones de objetos deberıansoportar el siguiente metodo:

iterator(): regresa un iterador de los elementos en la coleccion.

Este metodo esta soportado por la clase java.util.ArrayList. Estemetodo es muy importante, que hay una interfaz completa, java.lang.Iterable,la cual tiene solo este metodo dentro de ella. Este metodo facilita al usuarioespecificar acciones que necesiten recorrer los elementos de una lista. Pa-ra garantizar que una lista nodo soporta el metodo anterior, por ejemplo, sepodrıa agregar este metodo a la interfaz ListaPosicion, como se muestra enel listado 4.11. En este caso, tambien se quisiera indicar que ListaPosicion

extiende Iterable. Por lo tanto, se asume que las listas arreglo y las listasnodo soportan el metodo iterator().�public interface ListaPosicion <E> extends Iterable <E> {

/** Regresa un iterador de todos los elementos en la lista. */

public Iterator <E> iterator ();�Listado 4.7: Agregando el metodo iterator a la interfaz ListaPosicion

Dada tal defiinicion de una ListaPosicion, se podrıa usar un iteradorregresado por el metodo iterator() para crear una representacion de cadenade una lista nodo, como se muestra en el listado 4.8.�

/** Regresar una representacion textual de una lista nodo dada */

public static <E> String toString(ListaPosicion <E> l) {

Iterator <E> it = l.iterator ();

String s = "[";

while (it.hasNext ()) {

s += it.next (); // moldeo implıcito del elemento a String

if (it.hasNext ())

s += ", ";

}

s += "]";

return s;

}�Listado 4.8: Ejemplo de un iterador Java usado para convertir una lista nodoa una cadena.

Page 112: Notas Estructuras

112 Listas e Iteradores

4.3.2. El ciclo Java For-Each

Como recorrer los elementos regresados por un iterador es una construc-cion comun, Java proporciona una notacion breve para tales ciclos, llamadael ciclo for-each. La sintaxis para tal ciclo es como sigue:

for (tipo nombre : expresion)sentencia del ciclo

donde expresion evalua a una coleccion que implementa la interfaz java.lang.-Iterable, tipo es el tipo de objeto regresado por el iterador para esta clase, ynombre es el nombre de una variable que tomara los valores de los elementosde este iterador en la sentencia del ciclo. Esta notacion es la version corta dela siguiente:

for (Iterator<tipo> it=expresion.iterator(); it.hasNext(); ) {Tipo nombre= it.next();sentencia del ciclo}

Por ejemplo, si se tiene una lista, valores, de objetos Integer, y valores

implementa java.lang.Iterable, entonces se pueden agregar todos los en-teros en valores como sigue:

List<Integer> valores;...sentencias que crean una nueva lista y la llenan con Integers.int suma = 0;for (Integer i : valores)

suma += i; // desencajonar permite esto

Se podrıa leer el ciclo anterior como, “para cada Integer i en valores,hacer el cuerpo del ciclo, en este caso, agregar i a suma”

Ademas de la forma anterior del ciclo for-each, Java tambien permite queun ciclo for-each sea definido cuando expresion es un arreglo de tipo Tipo, elcual puede ser un tipo base o un tipo objeto. Por ejemplo se puede totalizarlos enteros en un arreglo, v, el cual guarda los primeros diez enteros positivos,

Page 113: Notas Estructuras

4.3 Iteradores 113

como sigue:

int[] v={1, 2, 3, 4, 5, 6, 7, 8, 9, 10};int total = 0;for (int i:v)

total += i;

4.3.3. Implementando Iteradores

Una forma de implementar un iterador para una coleccion de elementos eshacer una “foto” de esta e iterar sobre esta. Esta aproximacion involucrarıaguardar la coleccion en una estructura de datos separada que soporte accesosecuencial a sus elementos. Por ejemplo, se podrıan insertar todos los ele-mentos de la coleccion en una cola, en tal caso el metodo hasNext() podrıacorresponder a !isEmpty() y next() podrıa corresponder a dequeue(). Conesta aproximacion, el metodo iterator() toma tiempo O(n) para una colec-cion de tamano n. Como esta sobrecarga de copiado es relativamente costosa,se prefiere, en muchos casos, tener iteradores que operen sobre la misma co-leccion, no una copia.

En implementar esta aproximacion directa, se necesita solo realizar unseguimiento a donde el cursor del iterador apunta en la coleccion. Ası, crearun nuevo iterador en este caso simplemente involucra crear un objeto itera-dor que represente un cursor colocado solo antes del primer elemento de lacoleccion. Igualmente, hacer el metodo next() involucra regresar el siguienteelemento, si este existe, y mover el cursor solo pasando esta posicion del ele-mento. Ası, en esta aproximacion, crear un iterador cuesta tiempo O(1), aligual que hacer cada uno de los metodos del iterador. Se muestra una claseimplementando tal iterador en el listado 4.9, y se muestra en el listado 4.10como este iterador podrıa ser usado para implementar el metodo iterator()

en la clase ListaNodoPosicion.�public class ElementoIterador <E> implements Iterator <E> {

protected ListaPosicion <E> lista; // la lista subyacente

protected Posicion <E> cursor; // la siguiente posicion

/** Crea un elemento iterador sobre la lista dada. */

public ElementoIterador(ListaPosicion <E> L) {

lista = L;

cursor = (lista.isEmpty ())? null : lista.first ();

}

/** Valida si el iterador tiene un objeto siguiente. */

public boolean hasNext () { return (cursor != null); }

Page 114: Notas Estructuras

114 Listas e Iteradores

/** Regresa el siguiente objeto en el iterador. */

public E next() throws NoSuchElementException {

if (cursor == null)

throw new NoSuchElementException("No hay elemento siguiente");

E aRegresar = cursor.elemento ();

cursor = (cursor == lista.last ())? null : lista.next(cursor );

return aRegresar;

}�Listado 4.9: Una clase elemento iterador para una ListaPosicion.�

public Iterator <E> iterator () { return new IteradorElemento <E>(this); }

/** Regresa una coleccion iterable de todos los nodos en la lista. */�Listado 4.10: El metodo iterator() de la clase ListaNodoPosicion.

Iteradores posicion

Para ADTs que soporten la nocion de posicion, tal como los ADT lista ysecuencia, se puede tambien dar el siguiente metodo:

posiciones(): regresa un objeto Iterable (como una lista arreglo o listanodo) conteniendo las posiciones en la coleccion comoelementos.

Un iterador regresado por este metodo permite recorrer las posicionesde una lista. Para garantizar que una lista nodo soporta este metodo, sepodrıa agregar la interfaz ListaPosicion, como se muestra en el listado4.11. Entonces se podrıa, por ejemplo, agregar una implementacion de estemetodo a la ListaNodoPosicion, como se muestra en el listado 4.12. Estemetodo usa la propia clase ListaNodoPosicion para crear una lista quecontiene las posiciones de la lista original como sus elementos. Regresandoesta lista posicion como su objeto Iterable permitiendo llamar iterator()sobre este objeto para obtener un iterador de posiciones de la lista original.�public interface ListaPosicion <E> extends Iterable <E> {

// ... todos los otros metodos de la ADT lista ...

/** Regresa una coleccion iterable de todos los nodos en la lista. */

public Iterable <Posicion <E>> posiciones ();

}�Listado 4.11: Agregando metodos iterador a la interfaz ListaPosicion.�/** Regresa una coleccion iterable de todos los nodos en la lista. */

public Iterable <Posicion <E>> posiciones () { // crea una lista de posiciones

Page 115: Notas Estructuras

4.3 Iteradores 115

ListaPosicion <Posicion <E>> P = new ListaNodoPosicion <Posicion <E>>();

if (! isEmpty ()) {

Posicion <E> p = first ();

while (true) {

P.addLast(p); // agregar posicion p como el ult. elemento de la lista P

if (p == last ())

break;

p = next(p);

}

}

return P; // regresar P como el objeto iterable

}�Listado 4.12: El metodo posiciones() de la clase ListaNodoPosicion.

El iterador regresado por iterator() y otros objetos Iterable definenun tipo restringido de iterador que permite solo una pasada a los elemen-tos. Sin embargo, iteradores mas poderosos pueden tambien ser definidos,los cuales permiten moverse hacia atras y hacia adelante sobre un ciertoordenamiento de los elementos.

Page 116: Notas Estructuras

116 Listas e Iteradores

Page 117: Notas Estructuras

Capıtulo 5

Arboles

5.1. Arboles generales

En este capıtulo, se discute una de las estructuras de datos no linealmas importante en computacion—arboles . Las estructuras arbol son ademasun progreso en la organizacion de datos, ya que permiten implementar unconjunto de algoritmos mas rapidos que cuando se usan estructuras lineales dedatos, como una lista. Los arboles tambien dan una organizacion natural paradatos, y consecuentemente se han convertido en estructuras indispensablesen sistemas de archivos, interfaces graficas de usuario, bases de datos, sitiosWeb, y otros sistemas de computo.

Cuando se dice que los arboles “no son lineales”, se hace referencia auna relacion organizacional que es mas rica que las relaciones simples “an-tes” y “despues” entre objetos en secuencias. Las relaciones en un arbol sonjerarquicas , con algunos objetos estando “encima” y algunos otros “abajo”.Actualmente, la terminologıa principal para estructuras de dato arbol vienede los arboles genealogicos, con algunos terminos “padre”, “hijo”, “ancestro”,y “descendiente” siendo las palabras comunes mas usadas pora describir re-laciones.

5.1.1. Definiciones de arboles y propiedades

Un arbol es un tipo de dato abstracto que guarda elementos jerarquica-mente. Con la excepcion del elemento cima, cada elemento en un arbol tieneun elemento padre y cero o mas elementos hijos . Un arbol es visualizadocolocando elementos dentro de ovalos o rectangulos, y dibujando las cone-

Page 118: Notas Estructuras

118 Arboles

xiones entre padres e hijos con lıneas rectas. Se llama al elemento cima laraız del arbol, pero esta es dibujada como el elemento mas alto, con los otroselementos estando conectados abajo, justo lo opuesto de un arbol vivo.

5.1.2. Definicion formal de arbol

Se define un arbol T como un conjunto de nodos guardando elementostal que los nodos tienen una relacion padre-hijo, que satisface las siguientespropiedades:

Si T no esta vacıo, este tiene un nodo especial, llamado la raız de T ,que no tiene padre.

Cada nodo v de T diferente de la raız tiene un nodo padre unico w;cada nodo con padre w es un hijo de w.

De acuerdo a lo anterior, un arbol puede estar vacıo; es decir, que esteno tiene ningun nodo. Esta convencion tambien permite definir un arbolrecurrentemente, tal que un arbol T esta vacıo o consiste de un nodo r,llamado la raız de T , y un conjunto de arboles, posiblemente vacıo, cuyasraıces son los hijos de r.

Dos nodos que son hijos del mismo padre son hermanos . Un nodo v esexterno si v no tiene hijos. Un nodo v es interno si este tiene uno o mas hijos.Los nodo externos son tambien conocidos como hojas .

En la mayorıa de los sistemas operativos, los archivos son organizadosjerarquicamente en directorios anidados, tambien conocidos como carpetas,los cuales son presentados al usuario en la forma de un arbol. Mas especıfi-camente, los nodos internos del arbol estan asociados con directorios y losnodos externos estan asociados con archivos regulares. En el sistema opera-tivo UNIX. la raız del arbol es llamada el “directorio raız”, y es representadapor el sımbolo “/”.

Un nodo u es un ancestro de un nodo v si u = v o u es un ancestro delpadre de v. A la inversa, se dice que un nodo v es un descendiente de unnodo u si u es un ancestro de v. El subarbol enraızado de T en el nodo v es elarbol consistente de todos los descendientes de v en T , incluyendo al propiov.

Page 119: Notas Estructuras

5.1 Arboles generales 119

Arboles y caminos en arboles

Una arista del arbol T es un par de nodos (u, v) tal que u es el padre de v,o viceversa. Un camino de T es una secuencia de nodos tal que cualesquierados nodos consecutivos en la secuencia forman una arista.

Ejemplo. La relacion de herencia entre clases en programa en Java formanun arbol. La raız, java.lang.Object, es un ancestro de todas las otras clases.Cada clase, C, es una descendiente de esta raız y es la raız de un subarbol delas clases que extienden C. Ası, hay un camino de C a la raız, java.lang.Object,en este arbol de herencia.

Arboles ordenados

Un arbol es ordenado si hay un ordenamiento lineal definido para los hijosde cada nodo; esto es, se puede identificar los hijos de un nodo como siendo elprimero, segundo, tercero, etc. Tal ordenamiento es visualizando arreglandolos hermanos de izquierda a derecha, de acuerdo a su ordenamiento. Losarboles ordenados indican el orden lineal entre hermanos listandolos en elorden correcto.

Ejemplo. Los componentes de un documento estructurado, tal como unlibro, estan jerarquicamente organizados como un arbol cuyos nodos internosson partes, capıtulos, y secciones, y cuyos nodos externos son parrafos, tablas,figuras, etc. La raız del arbol corresponde al propio libro. Se podrıa conside-rar expandir mas el arbol para mostrar parrafos consistentes de sentencias,sentencias consistentes de palabras, y palabras consistentes de caracteres. Talarbol es un ejemplo de un arbol ordenado, porque hay un ordenamiento biendefinido enttre los hijos de cada nodo.

5.1.3. El tipo de dato abstracto arbol

El ADT arbol guarda elementos en posiciones, las cuales, al igual queposiciones en una lista, estan definidas relativas a posiciones vecinales. Lasposiciones en un arbol son sus nodos y posiciones vecinales satisfacen lasrelaciones padre-hijo que definen un arbol valido. Por lo tanto, se usan losterminos “posicion” y “nodo” indistintamente para arboles. Al igual que conuna lista posicion, un objeto posicion para un arbol soporta el metodo:

elemento(): regresa el objeto guardado en esta posicion.

Page 120: Notas Estructuras

120 Arboles

Sin embargo, el poder real de posiciones nodo en un arbol, viene de losmetodos accesores del ADT arbol que aceptan y regresan posiciones, talescomo las siguientes:

root(): regresa la raız del arbol; un error ocurre si el arbolesta vacıo.

parent(v): regresa el padre de v; un error ocurre si v es la raız.children(v): regresa una coleccion iterable conteniendo los hijos del

nodo v.

Si un arbol T esta ordenado, entonces la coleccion iterable, children(v),guarda los hijos de v en orden. Si v es un nodo externo, entonces children(v)esta vacıo.

Ademas de los metodos anteriores fundamentales accesores, tambien seincluyen los siguientes metodos de consulta:

isInternal(v): valida si el nodo v es interno.isExternal(v): valida si el nodo v es externo.

isRoot(v): valida si el nodo v es la raız.

Estos metodos hacen la programacion con arboles mas facil y mas legibles,ya que pueden ser usados en las condiciones de las sentencias if y de los cicloswhile, en vez de usar un condicional no intuitivo.

Hay tambien un numero de metodos genericos que un arbol probablemen-te podrıa soportar y que no estan necesariamente relacionados a la estructuradel arbol, incluyendo los siguientes:

size(): regresa el numero de nodos en el arbol.isEmpty(): valida si el arbol tiene algun nodo o no.iterator(): regresa un iterador de todos los elementos guardados en

nodos del arbol.posiciones(): regresa una coleccion iterable de todos los nodos del

arbol.replace(v, e): reemplaza con e y regresa el elemento guardado en el

nodo v.

Cualquier metodo que tome una posicion como un argumento podrıa ge-nerar una condicion de error si esa posicion es invalida. No se definen metodosde actualizacion especializados para arboles. En su lugar, se describen meto-

Page 121: Notas Estructuras

5.1 Arboles generales 121

dos de actualizacion diferentes en conjuncion con aplicaciones especıficas dearboles.

5.1.4. Implementando un arbol

La interfaz Java mostrada en el listado 5.1 representa el ADT arbol. Lascondiciones de error son manejadas como sigue: cada metodo puede tomaruna posicion como un argumento, podrıa lanzar un InvalidPositionExcep-

tion, para indicar que la posicion es invalida. El metodo parent lanza unaEmptyTreeException si este es llamado sobre un arbol vacıo.�import java.util.Iterator;

/**

* Una interfaz para un arbol donde los nodos pueden tener

* una cantidad arbitraria de hijos.

*

*/

public interface Tree <E> {

/** Regresa el numero de nodos en el arbol. */

public int size ();

/** Valida si el arbol esta vacıo. */

public boolean isEmpty ();

/** Regresa un iterador de los elementos guardados en el arbol. */

public Iterator <E> iterator ();

/** Regresa una coleccion iterable de los nodos. */

public Iterable <Posicion <E>> posiciones ();

/** Reemplaza el elemento guardado en un nodo dados. */

public E replace(Posicion <E> v, E e)

throws InvalidPositionException;

/** Regresa la raız del arbol. */

public Posicion <E> root() throws EmptyTreeException;

/** Regresa el padre de un nodo dado. */

public Posicion <E> parent(Posicion <E> v)

throws InvalidPositionException , BoundaryViolationException;

/** Regresa una coleccion iterable de los hijos de un nodo dado. */

public Iterable <Posicion <E>> children(Posicion <E> v)

throws InvalidPositionException;

/** Valida si un nodo dado es interno. */

public boolean isInternal(Posicion <E> v)

throws InvalidPositionException;

/** Valida si un nodo dado es externo. */

public boolean isExternal(Posicion <E> v)

throws InvalidPositionException;

/** Valida si un nodo dado es la raız del arbol. */

public boolean isRoot(Posicion <E> v)

throws InvalidPositionException;

}�Listado 5.1: La interfaz Java Tree representando el ADT arbol. Metodos adi-cionales de actualizacion podrıan ser agregados dependiendo de la aplicacion.

Page 122: Notas Estructuras

122 Arboles

Una estructura enlazada para arboles generales

Una forma natural para crear un arbol T es usar una estructura enlazada,donde se representa cada nodo v de T por un objeto posicion con los siguientescampos: una referencia al elemento guardado en v, un enlace al padre de v, yalgun tipo de coleccion, por ejemplo, una lista o arreglo, para guardar enlacesa los hijos de v. Si v es la raız de T entonces el campo parent de v es null.Tambien, se guarda una referencia a la raız de T y el numero de nodos de Ten variables internas.

La tabla 5.6 resume el rendimiento de la implementacion de un arbolgeneral usando una estructura enlazada. Se observa que usando una coleccionpara guardar los hijos de cada nodo v, se puede implementar children(v)regresando una referencia a esta coleccion.

Operacion Tiemposize, isEmpty O(1)iterator, posiciones O(n)replace O(1)root, parent O(1)children(v) O(cv)isInternal, isExternal, isRoot O(1)

Cuadro 5.6: Tiempos de ejecucion de los metodos de un arbol general n-nodoimplementado con una estructura enlazada. Con cv se denota el numero dehijos de un nodo v. El espacio usado es O(n).

5.2. Algoritmos de recorrido para arbol

En esta seccion, se presentan algoritmos para hacer recorridos en un arbolaccediendo este a traves de los metodos del ADT arbol.

5.2.1. Profundidad y altura

Sea v un nodo de un arbol T . La profundidad de v es el numero de ances-tros de v, excluyendo al propio v. Esta definicion ımplica que la profundidadde la raız de T es cero.

Page 123: Notas Estructuras

5.2 Algoritmos de recorrido para arbol 123

La profundidad de un nodo v puede tambien ser definida recurrentementecomo sigue:

Si v es la raız, entonces la profundidad de v es cero.

De otra forma, la profundidad de v es uno mas la profundidad del padrede v.

Basada en esta definicion se presenta un algoritmo recurrente simple,profundidad para computar la profundidad de un nodo v de T . Este metodose llama a sı mismo recurrentemente en el padre de v, y agrega uno al valorregresado.

Algoritmo profundidad(T, v):Si v es la raız de T EntoncesRegresar 0

Si noRegresar 1+profundidad(T,w), donde w es el padre de v en T

Una implementacion Java simple de este algoritmo se muestra en el listado5.2.�

public static <E> int profundidad (Tree <E> T, Posicion <E> v) {

if ( T.isRoot(v) )

return 0;

else

return 1 + profundidad(T, T.parent(v) );

}�Listado 5.2: El metodo profundidad escrito en Java.

El tiempo de ejecucion del algoritmo profundidad(T, v) es O(dv), dondedv denota la profundidad del nodo v en el arbol T , porque el algoritmorealiza un paso recurrente en tiempo constante para cada ancestro de v. Ası,el algoritmo profundidad(T, v) se ejecuta en tiempo O(n) en el peor caso,donde n es el numero total de nodos de T , ya que un nodo de T podrıatener profundidad n− 1 en el peor caso. Aunque tal tiempo de ejecucion esun funcion del tamano de entrada, es mas preciso caracterizar el tiempo deejecucion en terminos del parametro dv, debido a que este parametro puedeser mucho menor n.

Page 124: Notas Estructuras

124 Arboles

Altura

La altura de un nodo v en un arbol T esta tambien definida recurrente-mente:

Si v es un nodo externo, entonces la altura de v es cero.

De otra forma, la altura de v es uno mas la altura maxima de los hijosde v.

La altura de un arbol no vacıo T es la altura de la raız de T . Ademas laaltura puede tambien ser vista como sigue:

La altura de un arbol no vacıo T es igual a la maxima profundidad de unnodo externo de T .

Se presenta enseguida un algoritmo, altura1, para hallar la altura de unarbol no vacıo T usando la definicion anterior y el algoritmo profundidad.El listado 5.3 muestra su implementacion en Java.

Algoritmo altura1(T ):h← 0Para cada vertice v en T HacerSi v es un nodo externo de T Entoncesh← max(h, profundidad(T, v))

Regresar h

�public static <E> int altura1 (Tree <E> T) {

int h = 0;

for ( Posicion <E> v : T.posiciones () )

if ( T.isExternal(v) )

h = Math.max( h, profundidad(T,v) );

return h;

}�Listado 5.3: El metodo altura1 escrito en Java. Se emplea el metodo max dela clase java.lang.Math

Desafortunadamente, el algoritmo altura1 no es muy eficiente. Comoaltura1 llama al algoritmo profundidad(v) en cada nodo externo v de T ,el tiempo de ejecucion de altura1 esta dado por O(n +

∑v(1 + dv)), donde

n es el numero de nodos de T , dv es la profundidad del nodo v, y E es elconjunto de nodos externos de T . En el peor caso, la suma

∑v(1 + dv) es

Page 125: Notas Estructuras

5.2 Algoritmos de recorrido para arbol 125

proporcional a n2. Ası, el algoritmo altura1 corre en tiempo O(n2).El algoritmo altura2, mostrado a continuacion e implementado en Java

en el listado 5.4, computa la altura del arbol T de una forma mas eficienteusando la definicion recurrente de altura.

Algoritmo altura2(T, v):Si v es un nodo externo de T EntoncesRegresar 0

Si noh← 0Para cada hijo w de v en T Hacerh← max(h, altura2(T,w))

Regresar 1+h

�public static <E> int altura2 (Tree <E> T, Posicion <E> v) {

if ( T.isExternal(v) ) return 0;

int h = 0;

for ( Posicion <E> w : T.children(v) )

h = Math.max(h, altura2( T,w ));

return 1 + h;

}�Listado 5.4: El metodo altura2 escrito en Java.

El algoritmo altura2 es mas eficiente que altura1. El algoritmo es recu-rrente, y, si este es inicialmente llamado con la raız de T , este eventualmentesera llamado con cada nodo de T . Ası, se puede determinar el tiempo deejecucion de este metodo sumando, sobre todos los nodos, la cantidad detiempo gastado en cada nodo. Procesar cada nodo en children(v) tomatiempo O(cv), donde cv es el numero de hijos de un nodo v. Tambien, elciclo while tiene cv iteraciones y cada iteracion en el ciclo toma tiempo O(1)mas el tiempo para la llamada recurrente con un hijo de v. Ası, el algoritmoaltura2 gasta tiempo O(1 + cv) en cada nodo v, y su tiempo de ejecucion esO(∑

v(1 + cv)).Sea un arbol T con n nodos, y cv denota el numero de hijos de un nodo

v de T , entonces sumando sobre los vertices en T ,∑

v cv = n − 1, ya quecada nodo de T , con la excepcion de la raız, es un hijo de otro nodo, yası contribuye una unidad a la suma anterior.

Con el resultado anterior, el tiempo de ejecucion del algoritmo altura2,cuando es llamado con la raız de T , es O(n), donde n es el numero de nodos

Page 126: Notas Estructuras

126 Arboles

de T .

5.2.2. Recorrido en preorden

Un recorrido de un arbol T es una forma de accesar, o “visitar”, todoslos nodos de T . En esta seccion, se presenta un esquema de recorrido basi-co para arboles, llamado recorrido en preorden. En la siguiente seccion, seestudiara otro esquema basico de recorrido, llamado recorrido en postorden.

En un recorrido en preorden de un arbol T , la raız de T es visitada primeroy despues los subarboles enraızados en sus hijos son recorridos recurrente-mente. Si el arbol esta ordenado, entonces los subarboles son recorridos deacuerdo al orden de sus hijos. La accion especıfica asociada con la “visita”de un nodo v depende de la aplicacion de este recorrido, y podrıa involucrarcualquier cosa, desde incrementar un contador para realizar algun computocomplejo para v. El pseudocodigo para el recorrido en preorden del subarbolenraızado en un nodo v se muestra a continuacion. Se llama inicialmente estealgoritmo con preorden(T, T.root()).

Algoritmo preorden(T, v):Realizar la accion “visita” para el nodo vPara cada hijo w de v en T Hacer

preorden(T,w) { recurrentemente recorrer el subarbol enraızado en w }

El algoritmo de recorrido en preorden es util para producir un ordena-miento lineal de los nodos de un arbol donde los padres deberan siemprevenir primero que los hijos en el ordenamiento. Tales ordenamientos tienenvarias aplicaciones diferentes. Se explora un ejemplo simple de tal aplicacionen el siguiente ejemplo.

Ejemplo. El recorrido en preorden del arbol asociado con un documento,examina el documento entero secuencialmente, del inicio al fin.

El recorrido en preorden es tambien una forma eficiente de acceder todoslos nodos de un arbol, ya que el tiempo total de ejecucion del recorrido enpreorden de T es O(n).

El algoritmo toStringPreorder(T.v), implementado en Java en el listado5.5 realiza una impresion en preorden del subarbol de un nodo v de T , esto es,este realiza un recorrido en preorden del subarbol enraızado en v e imprime elelemento guardado en un nodo cuando el nodo es visitado. Recordando que,

Page 127: Notas Estructuras

5.2 Algoritmos de recorrido para arbol 127

para un arbol ordenado T , el metodo T .children(v) regresa una coleccioniterable que accesa los hijos de v en orden.�

public static <E> String toStringPreorder(Tree <E> T, Posicion <E> v) {

String s = v.elemento (). toString (); // accion principal "visita"

for ( Posicion <E> w : T.children(v) )

s += ", " + toStringPreorder( T, w );

return s;

}�Listado 5.5: El metodo toStringPreorder(T, v) que realiza una impresionen preorden de los elementos en el subarbol del nodo v de T .

Hay una aplicacion interesante del algoritmo de recorrido en preordenque produce una representacion de cadena de un arbol entero. Suponiendoque para cada elemento e guardado en un arbol T , llamando e.toString()regresa una cadena asociada con e. La representacion parentetica de cadenaP (T ) del arbol T esta recurrentemente definida como sigue. Si T consiste deun solo nodo v entonces

P (T ) = v.elemento().toString()

De otra forma,

P (T ) = v.elemento().toString() +′′ (′′+P (T1)+′′,′′+ · · ·+′′,′′+P (Tk)+′′)′′,

donde v es la raız de T y T1, T2, . . . , Tk son los subarboles enraızados en loshijos de v, los cuales estan dados en orden si T es un arbol ordenado.

La definicion anterior de P (T ) es recurrente. El sımbolo “+” denota laconcatenacion de cadenas.

El metodo Java parentheticRepresentation, mostrado en el listado 5.6,es una variacion del metodo toStringPreorder, listado 5.5. Este implemen-ta la definicion dada previamente para dar una representacion parenteticade cadena de un arbol T . El metodo parentheticRepresentation usa elmetodo toString que esta definido para cada objeto Java.�

public static <E> String RepresentacionParentetica(Tree <E> T,Posicion <E> v) {

String s = v.elemento (). toString (); // accion principal "visita"

if ( T.isInternal(v) ) {

Boolean primeraVez = true;

for ( Posicion <E> w : T.children(v) )

if ( primeraVez ) {

s += " ( " + RepresentacionParentetica(T, w); // el primer hijo

primeraVez = false;

}

Page 128: Notas Estructuras

128 Arboles

else s+= ", " + RepresentacionParentetica(T, w); // resto de hijos

s += " )"; // cerrar parentesis

}

return s;

}�Listado 5.6: Algoritmo parentheticRepresentation. Se usa el operador +para concatenar dos cadenas.

5.2.3. Recorrido en postorden

Otro algoritmo importante de recorrido en el arbol es el recorrido enpostorden. Este algoritmo puede ser visto como el opuesto al recorrido enpreorden, porque este recurrentemente recorre los subarboles enraızados enlos hijos de la raız primero, y entonces visita la raız. Este es similar al recorri-do en preorden, sin embargo, en aquel se usa este para resolver un problemaparticular especializando una accion asociada con la “visita” de un nodo v.Todavıa, como con el recorrido en preorden, si el arbol esta ordenado, sehacen llamadas recurrentes para los hijos de un nodo v de acuerdo a su or-den especificado. El pseudocodigo para el recorrido en postorden esta dadoa continuacion.

Algoritmo postorden(T, v):Para cada hijo w de v en T Hacer

postorden(T,w) { recurrentemente recorrer el subarbol enraızado en w }Realizar la accion “visita” para el nodo v

El tiempo total gastado en las porciones no recurrentes del algoritmo esproporcional al tiempo gastado en visitar los hijos de cada nodo en el arbol.Ası, un recorrido en postorden de un arbol T con n nodos toma tiempo O(n),suponiendo que visitar cada nodo toma tiempo O(1). Esto es, el recorrido enpostorden corre en tiempo lineal.

Como un ejemplo de recorrido en postorden, se muestra un metodo JavatoStringPostorder en el listado 5.7, el cual hace un recorrido en postordende un arbol T . Este metodo imprime el elemento guardado en un ndo cuandoes visitado.�

public static <E> String toStringPostorder(Tree <E> T, Posicion <E> v) {

String s = "";

for ( Posicion <E> w : T.children(v) )

Page 129: Notas Estructuras

5.2 Algoritmos de recorrido para arbol 129

s += ", " + toStringPreorder( T, w );

s += v.elemento (). toString (); // accion principal "visita"

return s;

}�Listado 5.7: Metodo toStringPostorder(T, v) que hace una impresionpostorden de los elementos en el subarbol de nodo v de T . El metodo implıci-tamente llama a toString en los elementos; cuando ellos estan involucradosen una operacion de concatenacion.

El metodo recorrido en postorden es util para resolver problemas donde sedesea calcular alguna propiedad para cada nodo v en un arbol, pero calcularesa propiedad para v requiere que se tenga computada la misma propiedadpara los hijos de v.

Ejemplo. Considerar un sistema de archivos de arbol T , donde los nodosexternos representan archivos y los nodos internos representan directorios.Supongase que se desea computar el espacio de disco usado por un directorio,el cual es recurrentemente dado por la suma de:

El tamano del propio directorio.

Los tamanos de los archivos en el directorio.

El espacio usado por los directorios de los hijos.

Este calculo puede ser hecho con un recorrido en postorden del arbol T .Despues de que los subarboles de un nodo interno v ha sido recorrido, secalcula el espacio usado por v agregando los tamanos del propio directorio vy de los archivos contenidos en v al espacio usado por cada hijo interno de v,el cual fue computado por los recorridos postorden recurrentes de los hijosde v.

Un metodo recurrente Java para calcular el espacio en disco

De acuerdo al ejemplo anterior, el metodo Java diskSpace, mostrado enel listado 5.8, realiza un recorrido postorden de un sistema de archivos dearbol T , imprimiendo el nombre y el espacio en disco usado por el directorioasociado con cada nodo interno de T . Cuando se llama con la raız del arbolT , diskSpace corre en tiempo O(n), donde n es el numero de nodos de T ,dado que los metodos auxiliares nombre y tama~no toman tiempo O(1).

Page 130: Notas Estructuras

130 Arboles

�public static <E> int espacioDisco(Tree <E> T,Posicion <E> v) {

int s = tama~no(v); // iniciar con el tama~no del propio nodo

for ( Posicion <E> w : T.children(v) )

// agregar el espacio calculado recurrentemente usado por los hijos de v

s += espacioDisco( T, w );

if ( T.isInternal(v) )

// imprimir nombre y espacio de disco usado

System.out.print( nombre(v) + ": " + s );

return s;

}�Listado 5.8: El metodo diskSpace imprime el nombre y el espacio en discousado por el directorio asociado con cada nodo interno de un sistema dearchivos de arbol. Este metodo llama los metodos auxiliares nombre y tama~no

y estos deberıan ser definidos para regresar el nombre y el tamano del archivoo directorio asociado con un nodo.

5.3. Arboles Binarios

Un arbol binario es un arbol ordenado con las siguientes propiedades:

1. Cada nodo tiene a lo mas dos hijos.

2. Cada nodo hijo esta etiquetado como hijo izquierdo o hijo derecho.

3. Un hijo izquierdo precede un hijo derecho en el ordenamiento de loshijos de un nodo.

El subarbol enraızado en un hijo izquierdo o derecho de un nodo internov es llamado un subarbol izquierdo o subarbol derecho, respectivamente, dev. Un arbol binario es propio si cada nodo tiene cero o dos hijos. Algunaspersonas se refieren tambien a tales arboles como arboles binarios completos .Ası, en un arbol binario propio, cada nodo interno tiene exactamente doshijos. Un arbol binario que no es propio es impropio

Ejemplo. Una clase importante de arboles binarios surgen en contextosdonde se desea representar un numero de resultados diferentes que puedenresultar de responder una serie de preguntas de si o no. Cada nodo esta aso-ciado con una pregunta. Iniciando en la raız, se va al hijo izquierdo o al hijoderecho del nodo actual, dependiendo de la si la respuesta es “si” o “no”. Concada decision, se sigue una arista desde un padre a un hijo, eventualmentese traza un camino en el arbol desde la raız a un nodo externo. Tales arboles

Page 131: Notas Estructuras

5.3 Arboles Binarios 131

binarios son conocidos como arboles de decision, porque cada nodo externov en tal arbol representa una decision de que hacer si las preguntas asociadascon los ancestros de v son respondidas en una forma que lleven a v. Un arbolde decision es un arbol binario propio.

Ejemplo. Una expresion aritmetica puede ser representada por un arbolbinario cuyos nodos externos estan asociados con variables o constantes, ycuyos nodos internos estan asociados con uno de los operadores +, −, ×, y/. Cada nodo en tal arbol tiene un valor asociado con este.

Si un nodo es externo, entonces su valor es aquel de su variable oconstante.

Si un nodo es interno, entonces su valor esta definido aplicando suoperacion a los valores de sus hijos.

Una expresion aritmetica es un arbol binario propio, ya que cada ope-rador +, −, ×, y / toma exactamente dos operandos. Por supuesto, si sepermitieran operadores unarios, como negacion, entonces se tendrıa un arbolbinario impropio.

Una definicion recurrente de un arbol binario

Se puede definir un arbol binario en una forma recurrente tal que un arbolbinario esta vacıo o consiste de:

Un nodo r, llamado la raız de T y guardando un elemento.

Un arbol binario, llamado el subarbol izquierdo de T .

Un arbol binario, llamado el subarbol derecho de T .

5.3.1. El ADT arbol binario

Como un tipo de dato abstracto, un arbol binario es una especializacionde un arbol que soporta cuatro metodos adicionales accesores:

izquierdo(v): regresa el hijo izquierdo de v; una condi-cion de error ocurre si v no tiene hijo iz-quierdo.

Page 132: Notas Estructuras

132 Arboles

derecho(v): regresa el hijo derecho de v; una condicionde error ocurre si v no tiene hijo derecho.

tieneIzquierdo(v): valida si v tiene un hijo izquierdo.tieneDerecho(v): valida si v tiene un hijo derecho.

Tal como en la seccion 5.1.3 para el ADT arbol, no se definen metodosde actualizacion especializados para arboles binarios aquı. En vez de eso, seconsideran algunos metodos de actualizacion posibles cuando se describanimplementaciones especıficas y aplicaciones de arboles binarios.

5.3.2. Una interfaz de arbol binario en Java

Se modela un arbol binario como un tipo de dato abstracto que extien-de el ADT arbol y se agregan los cuatro metodos especializados para unarbol binario. En el listado 5.9, se muestra una interfaz Java simple quese puede definir usando esta aproximacion. Por cierto, como los arboles bi-narios son arboles ordenados, la coleccion iterable regresada por el metodochildren(v), heredada de la interfaz Tree, guarda el hijo izquierdo de vantes que el hijo derecho de v.�/**

* Una interfaz para un arbol binario , donde cada nodo tiene cero , uno o

* dos hijos.

*/

public interface ArbolBinario <E> extends Tree <E> {

/** Regresa el hijo izquierdo de un nodo. */

public Posicion <E> izquierdo(Posicion <E> v)

throws InvalidPositionException , BoundaryViolationException;

/** Regresa el hijo derecho de un nodo. */

public Posicion <E> derecho(Posicion <E> v)

throws InvalidPositionException , BoundaryViolationException;

/** Valida si un nodo tiene un hijo izquierdo. */

public boolean tieneIzquierdo(Posicion <E> v) throws InvalidPositionException;

/** Valida si un nodo tiene un hijo derecho. */

public boolean tieneDerecho(Posicion <E> v) throws InvalidPositionException;

}�Listado 5.9: Interfaz Java ArbolBinario para el arbol binario ADT. La in-terfaz ArbolBinario extiende la interfaz Tree (listado 5.1)

5.3.3. Propiedades de arboles binarios

Los arboles binarios tienen varias propiedades interesantes que se ocupande las relaciones entre sus alturas y el numero de nodos. Se denota el conjunto

Page 133: Notas Estructuras

5.3 Arboles Binarios 133

de todos los nodos de un arbol T en la misma profundidad d como el nivel dde T . En un arbol binario, el nivel 0 tiene a lo mas un nodo, la raız, el nivel1 tiene a los mas dos nodos, los hijos de la raız, el nivel 2 tiene a los mascuatro nodos, etc. En general, el nivel d tiene a lo mas 2d nodos.

Se puede ver que el numero maximo de nodos en los niveles de un arbolbinario crece exponencialmente conforme se desciende en el arbol. De estasimple observacion, se pueden derivar las siguientes propiedades relacionandola altura de un arbol binario T con su numero de nodos.

Proposicion. Sea T un arbol binario no vacıo, y sean n, nE, nI y h elnumero de nodos, el numero de nodos externos, el numero de nodos internos,y la altura de T respectivamente. Entonces T tiene las siguientes propiedades:

1. h + 1 ≤ n ≤ 2h+1 − 1

2. 1 ≤ nE ≤ 2h

3. h ≤ nI ≤ 2h − 1

4. log(n + 1)− 1 ≤ h ≤ n− 1.

Tambien, si T es propio, entonces T tiene las siguientes propiedades:

1. 2h + 1 ≤ n ≤ 2h+1 − 1

2. h + 1 ≤ nE ≤ 2h

3. h ≤ nI ≤ 2h − 1

4. log(n + 1)− 1 ≤ h ≤ (n− 1)/2.

Relacionando nodos internos y externos en un arbol binario propio

Ademas de las propiedades de arboles binarios anteriores, se pueden tam-bien tener las siguientes relaciones entre el numero de nodos internos y ex-ternos en un arbol binario propio.

Proposicion. En un arbol binario propio no vacıo T , con nE nodos externosy nI nodos internos, se tiene nE = nI + 1

Demostracion. Removiendo los nodos de T y dividiendolos en dos “pilas”,una pila de nodos internos y una pila de nodos externos, hasta que T sevacıe. Las pilas estan inicialmente vacıas. Al final, la pila de nodos externostendra un nodo mas que la pila de nodos internos. Se consideran dos casos:

Page 134: Notas Estructuras

134 Arboles

Caso 1 : si T tiene un solo nodo v, se quita v y se coloca en la pila de losnodos externos. Ası, la pila de nodos externos tiene un nodo y la pilainterna esta vacıa.

Caso 2 : si T tiene mas de un nodo, se quita de T un nodo externo arbitrariow y sus padre v, el cual es un nodo interno. Se coloca w en la pila denodos externos y v en la pila de nodos internos. Si v tiene un padreu, entonces se reconecta u con el exhermano z de w. Esta operacionquita un nodo interno y un nodo externo, y deja al arbol siendo unarbol binario propio. Repitiendo esta operacion, eventualmente se llegaa un arbol final consistente de un solo nodo. Observar que el mismonumero de nodos internos y externos han sido removidos y colocadosen sus respectivas pilas por la secuencia de operaciones que llevan alarbol final. Ahora, se remueve el nodo del arbol final y se coloca esteen la pila de nodos externos. Ası, la pila de nodos externos tiene unnodo mas que la pila de nodos internos.

La relacion de la proposicion anterior no es valida, en general, para arbolesbinarios impropios y arboles no binarios, sin embargo hay otras relacionesinteresantes que puede ser validas.

5.3.4. Una estructura enlazada para arboles binarios

Al igual que con los arboles generales, una forma natural para hacer unarbol binario T es usar una estructura enlazada, donde se representa cadanodo v de T por un objeto posicion con campos dando referencias al elementoguardado en v y a los objetos posicion asociados con los hijos y el padre dev. Si v es la raız de T , entonces el campo padre de v es null. Si v no tienehijo izquierdo, entonces el campo izquierdo de v es null. Si v no tiene hijoderecho, entonces el campo derecho de v es null. Tambien, se guarda elnumero de nodos de T en una variable, llamada tama~no.

Implementacion Java de un nodo del arbol binario

Se usa una interfaz Java PosicionAB, listado 5.10, para representar un no-do de un arbol binario. Esta interfaz extiende a Posicion, con lo que heredaal metodo elemento, y tiene metodos adicionales para guardar el elemento enel nodo, setElemento, para poner y regresar el hijo izquierdo, setIzquierdo

Page 135: Notas Estructuras

5.3 Arboles Binarios 135

y getIzquierdo, para el hijo derecho, setDerecho y setDerecho, para el pa-dre, setPadre y getPadre del nodo.�/**

* Interfaz para un nodo de un arbol binario. Esta mantiene un elemento ,

* un nodo padre , un nodo izquierdo , y un nodo derecho.

*

*/

public interface PosicionAB <E> extends Posicion <E> { // hereda elemento ()

public void setElemento(E o);

public PosicionAB <E> getIzquierdo ();

public void setIzquierdo(PosicionAB <E> v);

public PosicionAB <E> getDerecho ();

public void setDerecho(PosicionAB <E> v);

public PosicionAB <E> getPadre ();

public void setPadre(PosicionAB <E> v);

}�Listado 5.10: Interfaz PosicionAB que extiende a la interfaz Posicion.

La clase NodoAB, listado 5.11, implementa la interfaz PosicionAB paraun objeto con los campos elemento, izquierdo, derecho, padre, las cuales,para un nodo v, se refieren al elemento en v, el hijo izquierdo de v, el hijoderecho de v, y el padre de v, respectivamente.�/**

* Clase implementando un nodo de un arbol binario guardando referencias

* a un elemento , un nodo padre , un nodo izquierdo , y un nodo derecho.

*/

public class NodoAB <E> implements PosicionAB <E> {

private E elemento; // elemento guardado en este nodo

private PosicionAB <E> izquierdo , derecho , padre; // nodos adyacentes

/** Constructor por default */

public NodoAB () { }

/** Constructor principal */

public NodoAB(E elemento , PosicionAB <E> padre ,

PosicionAB <E> izquierdo , PosicionAB <E> derecho) {

setElemento(elemento );

setPadre(padre);

setIzquierdo(izquierdo );

setDerecho(derecho );

}

/** Regresa el elemento guardado en esta posicion */

public E elemento () { return elemento; }

/** Pone el elemento guardado en esta posicion */

public void setElemento(E o) { elemento=o; }

/** Regresa el hijo izquierdo de esta posicion */

public PosicionAB <E> getIzquierdo () { return izquierdo; }

/** Pone el hijo izquierdo de esta posicion */

public void setIzquierdo(PosicionAB <E> v) { izquierdo=v; }

/** Regresa el hijo derecho de esta posicion */

public PosicionAB <E> getDerecho () { return derecho; }

/** Pone el hijo derecho de esta posicion */

public void setDerecho(PosicionAB <E> v) { derecho=v; }

/** Regresa el padre de esta posicion */

Page 136: Notas Estructuras

136 Arboles

public PosicionAB <E> getPadre () { return padre; }

/** Pone el padre de esta posicion */

public void setPadre(PosicionAB <E> v) { padre=v; }

}�Listado 5.11: Clase auxiliar NodoAB para implementar los nodos del arbolbinario.

Implementacion Java de la estructura enlazada arbol binario

En el listado 5.12, se muestran la clase ArbolBinarioEnlazada que im-plementa la interfaz ArbolBinario, listado 5.9, usando una estructura dedatos enlazada. Esta clase guarda el tamano del arbol y una referencia alobjeto NodoAB asociado con la raız del arbol en variables internas. Ademasde los metodos de la interfaz ArbolBinario, ArbolBinarioEnlazado tieneotros metodos, incluyendo el metodo accesor hermano(v), el cual regresa elhermano de un nodo v, y los siguientes metodos de actualizacion:

agregarRaiz(e): Crea y regresa un nuevo nodo r que guardael elemento e y haciendo r la raız del arbol;un error ocurre si el arbol no esta vacıo.

insertarIzquierdo(v, e): Crea y regresa un nuevo nodo w que guar-da el elemento e, agrega w como el hijoizquierdo de v y regresa w; un error ocurresi v ya tiene un hijo izquierdo.

insertarDerecho(v, e): Crea y regresa un nuevo nodo w que guar-da el elemento e, agrega w como el hijoderecho de v y regresa w; un error ocurresi v ya tiene un hijo derecho.

quitar(v): Quita el nodo v, lo reemplaza con su hijo,si hay, y regresa el elemento guardo en v;un error ocurre si v tiene dos hijos.

adjuntar(v, T1, T2): Adjunta T1 y T2, respectivamente, comosubarboles izquierdo y derecho de un nodoexterno v; una condicion de error ocurre siv no es externo.

La clase ArbolBinarioEnlazado tiene un constructor sin argumentos queregresa un arbol binario vacıo. Iniciando desde este arbol vacıo, se puede

Page 137: Notas Estructuras

5.3 Arboles Binarios 137

construir cualquier arbol binario mediante la creacion del primer nodo conel metodo agregarRaız y repetidamente aplicar los insertarIzquierdo einsertarDerecho o el metodo adjuntar. De igual forma, se puede desman-telar cualquier arbol binario T usando la operacion quitar, reduciendo alfinal tal arbol T a un arbol binario vacıo.

Cuando una posicion v es pasada como un argumento a uno de los meto-dos de la clase ArbolBinarioEnlazado, su validacion es revisada llamandoun metodo auxiliar de ayuda, revisaPosicion(v). Una lista de los nodosvisitados en un recorrido en preorden del arbol es construıda por el metodo re-currente posicionesPreorden. Condiciones de error son indicadas lanzandoexcepciones InvalidPositionException, BoundaryViolationException,EmptyTreeException, y NonEmptyTreeException.�import java.util.Iterator;

/**

* Una implementacion de la interfaz ArbolBinario por medio de una

* estructura enlazada.

*

* @see ArbolBinario

*/

public class ArbolBinarioEnlazado <E> implements ArbolBinario <E> {

protected PosicionAB <E> raiz; // referencia a la raız

protected int tama~no; // numero de nodes

/** Crear un arbol binario vacıo. */

public ArbolBinarioEnlazado () {

raiz = null; // iniciar con un arbol vacıo

tama~no = 0;

}

/** Regresa el numero de nodos en el arbol. */

public int size() {

return tama~no;

}

/** Valida si el arbol esta vacıo. */

public boolean isEmpty () {

return (tama~no == 0);

}

/** Valida si un nodo es interno. */

public boolean isInternal(Posicion <E> v) throws InvalidPositionException {

revisaPosicion(v); // metodo auxiliar

return (tieneIzquierdo(v) || tieneDerecho(v));

}

/** Valida si un nodo es externo. */

public boolean isExternal(Posicion <E> v) throws InvalidPositionException {

return !isInternal(v);

}

/** Valida si un nodo es la raız. */

public boolean isRoot(Posicion <E> v) throws InvalidPositionException {

revisaPosicion(v);

return (v == root ());

}

/** Valida si un nodo tiene hijo izquierdo. */

Page 138: Notas Estructuras

138 Arboles

public boolean tieneIzquierdo(Posicion <E> v) throws InvalidPositionException {

PosicionAB <E> vv = revisaPosicion(v);

return (vv.getIzquierdo () != null);

}

/** Valida si un nodo tiene hijo derecho. */

public boolean tieneDerecho(Posicion <E> v) throws InvalidPositionException {

PosicionAB <E> vv = revisaPosicion(v);

return (vv.getDerecho () != null);

}

/** Regresa la raız del arbol. */

public Posicion <E> root() throws EmptyTreeException {

if (raiz == null)

throw new EmptyTreeException("El arbol esta vacıo");

return raiz;

}

/** Regresa el hijo izquierdo de un nodo. */

public Posicion <E> izquierdo(Posicion <E> v)

throws InvalidPositionException , BoundaryViolationException {

PosicionAB <E> vv = revisaPosicion(v);

Posicion <E> posIzq = vv.getIzquierdo ();

if (posIzq == null)

throw new BoundaryViolationException("No hay hijo izquierdo");

return posIzq;

}

/** Regresa el hijo derecho de un nodo. */

public Posicion <E> derecho(Posicion <E> v)

throws InvalidPositionException , BoundaryViolationException {

PosicionAB <E> vv = revisaPosicion(v);

Posicion <E> posDer = vv.getDerecho ();

if (posDer == null)

throw new BoundaryViolationException("No hay hijo izquierdo");

return posDer;

}

/** Regresa el padre de un nodo. */

public Posicion <E> parent(Posicion <E> v)

throws InvalidPositionException , BoundaryViolationException {

PosicionAB <E> vv = revisaPosicion(v);

Posicion <E> posPadre = vv.getPadre ();

if (posPadre == null)

throw new BoundaryViolationException("No hay padre");

return posPadre;

}

/** Regresa una coleccion iterable de los hijos de un nodo. */

public Iterable <Posicion <E>> children(Posicion <E> v)

throws InvalidPositionException {

ListaPosicion <Posicion <E>> hijos = new ListaNodoPosicion <Posicion <E>>();

if (tieneIzquierdo(v))

hijos.addLast(izquierdo(v));

if (tieneDerecho(v))

hijos.addLast(derecho(v));

return hijos;

}

/** Regresa una coleccion iterable de los nodos del arbol. */

public Iterable <Posicion <E>> posiciones () {

ListaPosicion <Posicion <E>> posiciones =

new ListaNodoPosicion <Posicion <E>>();

if(tama~no != 0)

Page 139: Notas Estructuras

5.3 Arboles Binarios 139

posicionesPreorden(root(), posiciones );// asignar posiciones en preorden

return posiciones;

}

/** Regresa un iterador de los elementos guardados en los nodos. */

public Iterator <E> iterator () {

Iterable <Posicion <E>> posiciones = posiciones ();

ListaPosicion <E> elements = new ListaNodoPosicion <E>();

for (Posicion <E> pos: posiciones)

elements.addLast(pos.elemento ());

return elements.iterator (); // Un iterador de elementos

}

/** Reemplaza el elemento en un nodo. */

public E replace(Posicion <E> v, E o)

throws InvalidPositionException {

PosicionAB <E> vv = revisaPosicion(v);

E temp = v.elemento ();

vv.setElemento(o);

return temp;

}

// Metodos accesores adicionales

/** Regresa el hermano de un nodo. */

public Posicion <E> hermano(Posicion <E> v)

throws InvalidPositionException , BoundaryViolationException {

PosicionAB <E> vv = revisaPosicion(v);

PosicionAB <E> posPadre = vv.getPadre ();

if (posPadre != null) {

PosicionAB <E> posHermano;

PosicionAB <E> posIzq = posPadre.getIzquierdo ();

if (posIzq == vv)

posHermano = posPadre.getDerecho ();

else

posHermano = posPadre.getIzquierdo ();

if (posHermano != null)

return posHermano;

}

throw new BoundaryViolationException("No hay hermano");

}

// metodos adicionales de actualizacion

/** Agrega un nodo raız a un arbol vacıo. */

public Posicion <E> agregarRaiz(E e) throws NonEmptyTreeException {

if(! isEmpty ())

throw new NonEmptyTreeException("El arbol ya tiene una a raız");

tama~no = 1;

raiz = crearNodo(e,null ,null ,null);

return raiz;

}

/** Insertar un hijo izquierdo en un nodo dado. */

public Posicion <E> insertarIzquierdo(Posicion <E> v, E e)

throws InvalidPositionException {

PosicionAB <E> vv = revisaPosicion(v);

Posicion <E> posIzq = vv.getIzquierdo ();

if (posIzq != null)

throw new InvalidPositionException("El nodo ya tiene un hijo izquierdo");

PosicionAB <E> ww = crearNodo(e, vv, null , null);

vv.setIzquierdo(ww);

tama~no ++;

return ww;

Page 140: Notas Estructuras

140 Arboles

}

/** Insertar un hijo derecho en un nodo dado. */

public Posicion <E> insertarDerecho(Posicion <E> v, E e)

throws InvalidPositionException {

PosicionAB <E> vv = revisaPosicion(v);

Posicion <E> posDer = vv.getDerecho ();

if (posDer != null)

throw new InvalidPositionException("El nodo ya tiene un hijo derecho");

PosicionAB <E> w = crearNodo(e, vv, null , null);

vv.setDerecho(w);

tama~no ++;

return w;

}

/** Quitar un nodo con cero o un hijo. */

public E quitar(Posicion <E> v)

throws InvalidPositionException {

PosicionAB <E> vv = revisaPosicion(v);

PosicionAB <E> posIzq = vv.getIzquierdo ();

PosicionAB <E> posDer = vv.getDerecho ();

if (posIzq != null && posDer != null)

throw new

InvalidPositionException("No se puede quitar nodo con dos hijos");

PosicionAB <E> ww; // el unico hijo de v, si tiene

if (posIzq != null)

ww = posIzq;

else if (posDer != null)

ww = posDer;

else // v es una hoja

ww = null;

if (vv == raiz) { // v es la raız

if (ww != null)

ww.setPadre(null);

raiz = ww;

}

else { // v no es la raız

PosicionAB <E> uu = vv.getPadre ();

if (vv == uu.getIzquierdo ())

uu.setIzquierdo(ww);

else

uu.setDerecho(ww);

if(ww != null)

ww.setPadre(uu);

}

tama~no --;

return v.elemento ();

}

/** Adjunta dos arboles a un nodo externo. */

public void adjuntar(Posicion <E> v, ArbolBinario <E> T1, ArbolBinario <E> T2)

throws InvalidPositionException {

PosicionAB <E> vv = revisaPosicion(v);

if (isInternal(v))

throw new

InvalidPositionException("No se puede adjuntar a un nodo interno");

if (!T1.isEmpty ()) {

PosicionAB <E> r1 = revisaPosicion(T1.root ());

vv.setIzquierdo(r1);

Page 141: Notas Estructuras

5.3 Arboles Binarios 141

r1.setPadre(vv); // T1 deberıa ser invalidado

}

if (!T2.isEmpty ()) {

PosicionAB <E> r2 = revisaPosicion(T2.root ());

vv.setDerecho(r2);

r2.setPadre(vv); // T2 deberıa ser invalidado

}

}

/** Intercambiar los elementos de dos nodos. */

public void intercambiarElementos(Posicion <E> v, Posicion <E> w)

throws InvalidPositionException {

PosicionAB <E> vv = revisaPosicion(v);

PosicionAB <E> ww = revisaPosicion(w);

E temp = w.elemento ();

ww.setElemento(v.elemento ());

vv.setElemento(temp);

}

/** Expandir un nodo externo en un nodo interno

con dos nodos externos hijos. */

public void expandirExterno(Posicion <E> v, E l, E r)

throws InvalidPositionException {

if (! isExternal(v))

throw new InvalidPositionException("El nodo no es externo");

insertarIzquierdo(v, l);

insertarDerecho(v, r);

}

/** Quitar un nodo externo v y su padre */

public void quitarPadreExterno(Posicion <E> v)

throws InvalidPositionException {

if (! isExternal(v))

throw new InvalidPositionException("El nodo no es externo");

if (isRoot(v))

quitar(v);

else {

Posicion <E> u = parent(v);

quitar(v);

quitar(u);

}

}

// metodos auxiliares

/** Si v es un nodo bueno del arbol binario , convertirlo a PosicionAB ,

si no lanzar excepcion. */

protected PosicionAB <E> revisaPosicion(Posicion <E> v)

throws InvalidPositionException {

if (v == null || !(v instanceof PosicionAB ))

throw new InvalidPositionException("La posicion es invalida");

return (PosicionAB <E>) v;

}

/** Crear un nuevo nodo del arbol binario. */

protected PosicionAB <E> crearNodo(E element , PosicionAB <E> padre ,

PosicionAB <E> izquierdo , PosicionAB <E> derecho) {

return new NodoAB <E>(element ,padre ,izquierdo ,derecho ); }

/** Crea una lista guardando los nodos en el subarbol de un nodo ,

* ordenados de acuerdo al recorrido en preorden del subarbol. */

protected void posicionesPreorden(Posicion <E> v,

ListaPosicion <Posicion <E>> pos)

throws InvalidPositionException {

Page 142: Notas Estructuras

142 Arboles

pos.addLast(v);

if (tieneIzquierdo(v))

posicionesPreorden(izquierdo(v), pos); // recurrencia en el hijo izquierdo

if (tieneDerecho(v))

posicionesPreorden(derecho(v), pos); // recurrencia en el hijo izquierdo

}

/** Crea una lista guardando los nodos en el subarbol de un nodo ,

* ordenados de acuerdo al recorrido en orden del subarbol. */

protected void posicionesEnOrden(Posicion <E> v,

ListaPosicion <Posicion <E>> pos)

throws InvalidPositionException {

if (tieneIzquierdo(v))

posicionesEnOrden(izquierdo(v), pos); // recurrencia en el hijo izquierdo

pos.addLast(v);

if (tieneDerecho(v))

posicionesEnOrden(derecho(v), pos); // recurrencia en el hijo derecho

}

}�Listado 5.12: Clase ArbolBinarioEnlazado el cual implementa la interfazArbolBinario.

Desempeno de la implementacion arbol binario enlazado

Se analizan los tiempos de ejecucion de los metodos de la clase ArbolBina-rioEnlazado, el cual emplea una representacion de estructura enlazada:

Los metodos size() e isEmpty() usan una variable de instancia lacual guarda el numero de nodos de T , y cada una toma tiempo O(1).

Los metodos accesores root, parent, izquierdo, derecho, hermanotoman tiempo O(1).

El metodo replace(v, e) toma tiempo O(1).

Los metodos iterator() y posiciones() estan implementados rea-lizando un recorrido preorden del arbol, mediante el metodo auxiliarposicionesPreorden. Los nodos visitados por el recorrido son guarda-dos en una lista posicion implementada por la clase ListaNodoPosicion,listado 4.2.4, y el iterador salida es generado con el metodo iterator()

de la clase ListaNodoPosicion. Los metodos iterator() y posiciones()

toman tiempo O(n) y los metodos hasNext() y next() de los iterado-res regresados se ejecutan en tiempo O(1).

Page 143: Notas Estructuras

5.3 Arboles Binarios 143

El metodo children usa una aproximacion similar para construir lacoleccion iterable regresada, pero se ejecuta en tiempo O(1), ya quehay a lo mas dos hijos para cualquier ndo en un arbol binario.

Los metodos de actualizacion insertarIzquierdo, insertarDerecho,adjuntar, y quitar corren en tiempo O(1), ya que involucran mani-pulacion tiempo constante de un numero constante de nodos.

Considerando el espacio requerido por esta estructura de datos para unarbol con n nodos, considerando que hay un objeto de la clase NodoAB, listado5.11 , para cada nodo del arbol T . Por lo tanto, el espacio total requerido esO(n).

5.3.5. Una representacion lista arreglo de un arbol bi-nario

Una representacion alternativa de un arbol binario T esta basado en unaforma de numerar los nodos de T . Para cada nodo v de T , sea p(v) el enterodefinido como sigue.

Si v es la raız de T , entonces p(v) = 1.

Si v es el hijo izquierdo del nodo u, entonces p(v) = 2p(u).

Si v es el hijo derecho del nodo u, entonces p(v) = 2p(u) + 1.

La funcion de numeracion p es conocida como nivel de numeracion delos nodos en un arbol binario T , la funcion numera los nodos en cada nivelde T en orden creciente desde la izquierda a la derecha, sin embargo podrıasaltarse algunos numeros.

La funcion nivel de numeracion p sugiere una representacion de un arbolbinario T por medio de una lista arreglo S tal que cada nodo v de T esel elemento de S en el ındice p(v). Como se indico en el capıtulo previo,se puede realizar la lista arreglo S por medio de una arreglo extendible,ver seccion 4.1.5. Tal implementacion es simple y eficiente, para realizarlos metodos root, parent, isRoot, isInternal, isExternal, izquierdo,derecho, tieneIzquierdo, tieneDerecho usando operaciones aritmeticascon los numeros p(v) asociados con cada nodo v involucrado en la operacion.

Page 144: Notas Estructuras

144 Arboles

Sea n el numero de nodos de T , y sea pM el valor maximo de p(v) sobretodos los nodos de T . La lista arreglo S tiene tamano N = pM + 1 yaque elemento de S en el ındice 0 no esta asociado con ningun nodo de T .Tambien, S tendra, en general, un numero de elementos vacıos que no serefieren a nodos existentes de T . De hecho, en el peor caso, N = 2n. Ası, apesar del uso de espacio en el peor caso, hay aplicaciones para las cuales larepresentacion lista arreglo de un arbol binario es espacio eficiente. Todavıa,para los arboles binarios generales, el requerimiento de espacio exponencialen el peor caso de esta representacion es prohibitivo. En la siguiente tabla seresumen los tiempos de ejecucion de los metodos de una implementacion arbolbinario con una lista arreglo. No se incluyen ningun metodo de actualizacion.

Metodo Tiemposize, isEmpty O(1)iterator, posiciones O(n)replace O(1)root, parent, isRoot, isInternal, isExternal,children

O(1)

izquierdo, derecho, tieneIzquierdo, tieneDerecho O(1)

5.3.6. Recorridos de Arboles Binarios

Al igual que con los arboles generales, los calculos de arboles binariosfrecuentemente incluyen recorridos.

Construyendo un arbol de expresion

Considerar el problema de construir una arbol de expresion de una expre-sion aritmetica completamente con parentesis de tamano n. Por ejemplo, laexpresion ((((3+1)×3)/((9−5)+2))−((3×(7−4))+6)). El siguiente algorit-mo construirExpresion crea un arbol de expresion, suponiendo que todaslas operaciones aritmeticas son binarias y las variables no tiene parentesis.Ası cada subexpresion con parentesis contiene un operador enmedio.

Algoritmo construirExpresion(E):Entrada: una expresion aritmetica completamente con parentesis

E = e0, e1, . . . , en−1 con cada ei siendo una variable, operador, o parente-sis.

Page 145: Notas Estructuras

5.3 Arboles Binarios 145

Salida: un arbol binario T representando la expresion aritmetica E.S ← una pila nueva inicialmente vacıaPara i← 0 Hasta n− 1 HacerSi ei es una variable o un operador EntoncesT ← un arbol nuevo binario vacıoT .addRoot(ei)S.push(T )

Si no Si ei =′ (′ EntoncesContinuar el ciclo

Si no { ei =′)′ }T2 ← S.pop() { el arbol representando E2 }T ← S.pop() { el arbol representando ◦ }T1 ← S.pop() { el arbol representando E2 }T .adjuntar(T .root(), T1, T2)S.push(T )

Regresar S.pop()

El algoritmo usa una pila S mientras se lee la expresion de entrada Ebuscando variables, operadores, y parentesis derechos.

Cuando se ve una variable o un operador ◦, se crea un arbol binarioT con un solo nodo, cuya raız guarda la variable o el operador ◦ y semete en la pila T .

Cuando se ve un parentesis dierecho, ′)′, se sacan los tres arboles de lapila S, los cuales representan una subexpresion (E1 ◦ E2). Entonces seadjuntan los arboles E1 y E2 al arbol ◦, y se mete el arbol resultantede regreso en S.

Se repite esto hasta que la expresion E ha sido procesada, en ese momentoel elemento de la cima de la pila es el arbol de expresion para E. El tiempototal de ejecucion es O(n).

Recorrido en preorden de un arbol binario

Como cualquier arbol binario pueda ser visto como un arbol general, elrecorrido en preorden para arboles generales, seccion 5.2.2, puede ser aplicadoa cualquier arbol binario. Se puede simplificar el algoritmo en el caso de unrecorrido de un arbol binario, como se muestra en el siguiente pseudocodigo.

Page 146: Notas Estructuras

146 Arboles

Algoritmo preordenBinario(T, v):Realizar la accion “visita” para el nodo vSi v tiene un hijo izquierdo u en T Entonces

preordenBinario(T, u) { recurrentemente recorrer el subarbol izquierdo }Si v tiene un hijo derecho w en T Entonces

preordenBinario(T,w) { recurrentemente recorrer el subarbol derecho }Como en el caso de los arboles generales, hay varias aplicaciones del re-

corrido en preorden para arboles binarios.

Recorrido en postorden de un arbol binario

Analogamente, el recorrido en postorden para arboles generales, seccion5.2.3, el algoritmo dado puede ser especializado para arboles binarios, comose muestra en el siguiente pseudocodigo.

Algoritmo postordenBinario(T, v):Si v tiene un hijo izquierdo u en T Entonces

postordenBinario(T, u) { recurrentemente recorrer el subarbol izquierdo }Si v tiene un hijo derecho w en T Entonces

postordenBinario(T,w) { recurrentemente recorrer el subarbol derecho }Realizar la accion “visita” para el nodo v

Evaluacion de un arbol de expresion

El recorrido en postorden de un arbol binario puede ser usado para resol-ver el problema de evaluacion de un arbol de expresion. En este problema,se da un arbol de expresion aritmetica, esto es, un arbol binario donde cadanodo externo tiene un valor asociado con este y cada nodo interno tiene unaoperacion aritmetica asociada con este, y se quiere computar el valor de laexpresion aritmetica representada por el arbol.

El siguiente algoritmo evaluarExpresion evalua la expresion asociadacon el subarbol enraızado en un nodo v de un arbol de expresion aritmeticaT realizando un recorrido postorden de T inciando en v. En este caso, laaccion “visita” consiste en hacer una sola operacion aritmetica. Observarque se usa el hecho de que un arbol de expresion aritmetica es un arbolbinario propio.

Page 147: Notas Estructuras

5.3 Arboles Binarios 147

Algoritmo evaluarExpresion(T, v):Si v es un nodo interno en T Entonces

Sea ◦ el operador guardado en vx← evaluarExpresion(T, T.izquierdo(v))y ← evaluarExpresion(T, T.derecho(v))Regresar x ◦ y

Si noRegresar el valor guardado en v

La aplicacion evaluacion del arbol de expresion del recorrido en postordenda un algoritmo tiempo O(n) para evaluar una expresion aritmetica represen-tada por un arbol binairo con n nodos. Ademas, como el recorrido postordengeneral, el recorrido postorden para arboles binarios puede ser aplicado aotros problemas de evaluacion “bottom-up” (ascendente).

Recorrido en orden de un arbol binario

Un metodo adicional de recorrido para un arbol binario es el recorrido enorden. En este recorrido, se visita un nodo entre los recorridos recurrentesde sus subarboles izquierdo y derecho. El recorrido en orden del subarbolenraızado en un nodo v en un arbol binario T esta dado en el siguientealgoritmo.

Algoritmo enorden(T, v):Si v tiene un hijo izquierdo u en T Entonces

enorden (T, u) { recurrentemente recorrer el subarbol izquierdo }realizar la accion “visita” para el nodo vSi v tiene un hijo derecho w en T Entonces

enorden (T,w) { recurrentemente recorrer el subarbol derecho }

El recorrido en orden de un arbol binario T puede ser informalmente vistocomo visitar los nodos de T “de izquierda a derecha”. En realidad, para cadanodo v, el recorrido en orden visita v despues de que todos los nodos en elsubarbol izquierdo de v y antes de todos los nodos en subarbol derecho de v.

Page 148: Notas Estructuras

148 Arboles

Arboles binarios de busqueda

Sea S un conjunto cuyos elementos tienen una relacion de orden. Porejemplo, S podrıa ser un conjunto de enteros. Un arbol binario de busquedapara S es un arbol propio binario T tal que

Cada nodo interno v de T guarda un elemento de S, denotado con x(v).

Para cada nodo interno v de T , los elementos guardados en el subarbolizquierdo de v son menores que o iguales a x(v) y los elementos guar-dados en el subarbol derecho de v son mayores que o igual a x(v).

Los nodos externos de T no guardan ningun elemento.

Un recorrido en orden de los nodos internos de un arbol binario de busque-da T visita los elementos en orden creciente.

Se puede usar un arbol binario de busqueda T para un conjunto S paraencontrar si un valor dado de busqueda y esta en S, recorriendo un caminohacia abajo en el arbol T , iniciando en la raız. En cada nodo interno vencontrado, se compara el valor de busqueda y con el elemento x(v) guardadoen v. Si y < x(v), entonces la busqueda continua en el subarbol izquierdode v. Si y = x(v), entonces la busqueda termina con exito. Si y ≥ x(v),entonces la busqueda continua en el subarbol derecho de v. Finalmente, sise alcanza un nodo externo, la busqueda termina sin exito. El arbol binariode busqueda puede ser visto como un arbol binario de decision, donde lapregunta hecha en cada nodo interno es si el elemento en ese nodo es menorque, igual a, o mayor que el elemento que esta siendo buscado. En realidad,esta correspondencia a un arbol binario de decision motiva la restriccion enlos arboles binarios de busqueda para que sean arboles binarios propios.

El tiempo de ejecucion de la busqueda en un arbol binario de busquedaT es proporcional a la altura de T . La altura de un arbol binario propiocon n nodos puede ser tan pequena como log(n + 1)− 1 o tan grande como(n − 1)/2. Ası, los arboles binarios de busqueda son mas eficientes cuandotienen altura pequena.

Usando recorrido en orden para dibujar arboles

El recorrido en orden puede tambien ser aplicado al problema de calcularun dibujo de un arbol binario. Se puede dibujar un arbol binario T con un

Page 149: Notas Estructuras

5.3 Arboles Binarios 149

algoritmo que asigna coordenadas en x e y a un nodo v de T usando lassiguientes dos reglas:

x(v) es el numero de nodos visitados antes que v en el recorrido enorden de T .

y(v) es la profundidad de v en T .

En esta aplicacion, se toma la convencion comun en graficas computacio-nales de que la coordenada x se incrementa de izquierda a derecha y lacoordenada y se incrementa de arriba hacia abajo. Ası el origen esta en laesquina superior izquierda de la pantalla de la computadora.

El circuito Euleriano de un arbol binario

Los algoritmos de recorrido de arboles que se han presentado son todosformas de iteradores. Cada recorrido visita los nodos de un arbol en un ciertoorden, y se garantiza visitar cada nodo exactamente una vez. Sin embargo,se pueden unificar los algoritmos de recorrido dados previamente en un solosistema, relajando los requerimientos de que cada nodo sera visitado exac-tamente una vez. El metodo resultante de recorrido es llamado el circuitoEuleriano, el cual se estudia enseguida. La ventaja de este recorrido es quepermite a mas algoritmos generales ser expresados facilmente.

El circuito Euleriano de un arbol binario T puede ser informalmente defi-nido como un “caminata” alrededor de T , donde se inicia yendo desde la raızhacia su hijo izquierdo, viendo las aristas de T como “paredes” que siemprese mantienen a la izquierda. Cada nodo v de T es encontrado tres veces porel circuito Euleriano:

“A la izquierda”, antes del circuito Euleriano del subarbol izquierdo dev.

“De abajo”, entre los recorridos Eulerianos de los dos subarboles de v.

“A la derecha”, despues del circuito Euleriano del subarbol derecho dev.

Si v es un nodo externo, entonces estas tres “visitas” suceden al mismotiempo. Se puede describir el circuito Euleriano del subarbol enraızado en vpor medio del siguiente algoritmo.

Page 150: Notas Estructuras

150 Arboles

Algoritmo circuitoEuleriano(T, v):Si v tiene un hijo izquierdo u en T Entonces

circuitoEuleriano(T, u) { recurrentemente recorrer el subarbol izquierdo }realizar la accion para visitar el nodo vSi v tiene un hijo derecho w en T Entonces

circuitoEuleriano(T,w) { recurrentemente recorrer el subarbol derecho }

El tiempo de ejecucion del recorrido Euleriano de un arbol de n-nodos esfacil analizar suponiendo que cada accion visita toma tiempo O(1). Como segasta una cantidad constante de tiempo en cada nodo del arbol durante elrecorrido, el total del tiempo de ejecucion es O(n).

El recorrido en preorden de un arbol binario es equivalente a un recorridode un circuito Euleriano tal que cada nodo tiene una accion asociada “vi-sita” que ocurre solo cuando este es encontrado a la izquierda. Igualmente,los recorridos en order y postorden de un arbol binario son equivalentes a uncircuito Euleriano tal que cada nodo tiene una accion asociada “visita” queocurre solo cuando este es encontrado abajo o a la derecha, respectivamente.El recorrido del circuito Euleriano extiende los recorridos preorden, en orden,y postorden, pero tambien puede realizar otros tipos de recorridos. Por ejem-plo, suponer que se desea calcular el numero de descendientes de cada nodov en un arbol binario de n-nodos. Se inicia un ciclo Euleriano inicializandoun contador a cero, y entonces se incrementa el contador cada vez que sevisita un nodo a la izquierda. Para determinar el numero de descendientesde un nodo v, se calcula la diferencia entre los valores del contador cuando ves visitado a la izquierda y cuando es visitado a la derecha, y se agrega uno.Esta regla simple da el numero de descendientes de v, porque cada nodo enel subarbol enraızado en v es contado entre la visita de v a la izquierda yen la visita de v a la derecha. Por lo tanto, se tiene un metodo tiempo O(n)para calcular el numero de descendientes de cada nodo.

Otra aplicacion del recorrido del circuito Euleriano es la impresion deuna expresion aritmetica con parentesis a partir de su arbol expresion. Elalgoritmo imprimeExpresion, mostrada enseguida, lleva a cabo esta tarearealizando las siguientes acciones en un circuito Euleriano:

Accion “a la izquierda”: si el nodo es interno, imprimir “(”.

Accion “desde abajo”: mostrar el valor u operador guardado en el nodo.

Page 151: Notas Estructuras

5.3 Arboles Binarios 151

Accion “a la derecha”: si el nodo es interno, imprimir “)”.

Algoritmo imprimirExpresion(T, v):Si T.esInterno(v) Entonces

imprimir “(”Si T.tieneIzquierdo(v) Entonces

imprimirExpresion(T, T.izquierdo(v))Si T.esInterno(v) Entonces

imprimir el operador guardado en vSi no T.tieneIzquierdo(v) Entonces

imprimir el operador guardado en vSi T.tieneIzquierdo(v) Entonces

imprimirExpresion(T, T.derecho(v))Si T.esInterno(v) Entonces

imprimir “)”

5.3.7. El metodo patron plantilla

Los metodos de recorrido de arbol descrito previamente son ejemplos deun patron de diseno de software interesante orientado al objeto, el metodopatron plantilla. El metodo patron plantilla describe un mecanismo genericocomputacional que puede ser especializado para una aplicacion particularredefiniendo ciertos pasos. Siguiendo el patron plantilla metodo, se disena unalgoritmo que implementa un recorrido generico ciclo Euleriano de un arbolbinario. Este algoritmo, llamado plantillaCircuitoEuleriano, se muestraenseguida.

Algoritmo plantillaCircuitoEuleriano(T, v):r ← nuevo objeto de tipo ResultadoCircuitovisitaIzquierda(T, v, r)Si T.tieneIzquierdo Entoncesr.izquierdo← plantillaCircuitoEuleriano(T, T.Izquierdo(v))

visitaAbajo(T, v, r)Si T.tieneIzquierdo Entoncesr.derecho← plantillaCircuitoEuleriano(T, T.Derecho(v))

Page 152: Notas Estructuras

152 Arboles

visitaDerecha(T, v, r)Regresa r.salida

Cuando se llama con un nodo v, el metodo plantillaCircuitoEulerianollama a varios metodos auxiliares en diferentes fases del recorrido. A saber,esta

Crea una variable local r de tipo ResultadoCircuito, la cual es usa-da para guardar resultados intermedios del calculo y tiene camposizquierdo, derecho y salida.

Llama al metodo auxiliar visitaIzquierda(T, v, r), el cual realiza loscalculos asociados cuando encuentra el nodo a la izquierda.

Si v tiene un hijo izquierdo, recurrentemente se llama a sı mismo conel hijo izquierdo de v y guarda el valor regresado en r.izquierdo.

Llama al metodo auxiliar visitaAbajo(T, v, r), el cual realiza los calcu-los asociados cuando se encuentra con el nodo abajo.

Si v tiene un hijo derecho, recurrentemente se llama a sı mismo con elhijo derecho de v y guarda el valor regresado en r.derecho.

Llama al metodo auxiliar visitaDerecha(T, v, r), el cual realiza loscalculos asociados cuando encuentra el nodo a la derecha.

Regresa r.salida.

El metodo plantillaCircuitoEuleriano puede ser visto como una plan-tilla o “esqueleto” de un circuito Euleriano.

Implementacion Java

La clase CircuitoEuleriano, mostrada en el listado 5.13, implementaun recorrido de un circuito Euleriano usando el metodo patron plantilla.El recorrido recurrente es realizado por el metodo circuitoEuleriano. Losmetodos auxiliares llamados por el circuitoEuleriano son lugares vacıosdisponibles, es decir, tienen un cuerpo vacıo o solo regresan null. La claseCircuitoEuleriano es abstracta y por lo tanto no puede ser instanciada.La clase contiene un metodo abstracto, llamado ejecutar, el cual necesitaser especificado en la subclase concreta de CircuitoEuleriano. Tambien se

Page 153: Notas Estructuras

5.3 Arboles Binarios 153

muestra la clase ResultadoRecorrido, con campos izquierda, derecha, ysalida.�/**

* Plantilla para algoritmos que recorren un arbol binario usando un circuito

* Euleriano. Las subclases de esta clase redefiniran algunos de los

* metodos de esta clase para crear un recorrido especıfico.

*/

public abstract class CircuitoEuleriano <E, R> {

protected ArbolBinario <E> arbol;

/** Ejecucion del recorrido. Este metodo abstracto debera ser

* especificado en una subclase concreta. */

public abstract R ejecutar(ArbolBinario <E> A);

/** Inicializacion del recorrido. */

protected void inicializar(ArbolBinario <E> A) { arbol = A; }

/** Metodo plantilla */

protected R circuitoEuleriano(Posicion <E> v) {

ResultadoRecorrido <R> r = new ResultadoRecorrido <R>();

visitaIzquierda(v, r);

if (arbol.tieneIzquierdo(v))

r.izquierda = circuitoEuleriano(arbol.izquierdo(v));// recorrido recurrente

visitaAbajo(v, r);

if (arbol.tieneDerecho(v))

r.derecha = circuitoEuleriano(arbol.derecho(v)); // recorrido recurrente

visitaDerecha(v, r);

return r.salida;

}

// Metodos auxiliares que pueden ser redefinidos por las subclases:

/** Metodo llamado para la visita a la izquierda. */

protected void visitaIzquierda(Posicion <E> v, ResultadoRecorrido <R> r) {}

/** Metodo llamado para la visita abajo. */

protected void visitaAbajo(Posicion <E> v, ResultadoRecorrido <R> r) {}

/** Metodo llamado para la visita a la derecha. */

protected void visitaDerecha(Posicion <E> v, ResultadoRecorrido <R> r) {}

public class ResultadoRecorrido <R> {

public R izquierda;

public R derecha;

public R salida;

}

}�Listado 5.13: La clase CircuitoEuleriano definiendo un recorrido Eulerianogenerico de un arbol binario. Esta clase logra el metodo patron plantilla ydebera ser especializada para realizar el calculo adecuado.

La clase, CircuitoEuleriano, por sı misma no realiza ningun calcu-lo util. Sin embargo, se puede extender y sobreescribir los metodos vacıosauxiliares para realizar tareas utiles. Se ilustra este concepto usando arbo-les de expresiones, ver seccion 5.3. Se supone que un arbol de expresionaritmetica tiene objetos de tipo TerminoExpresion en cada nodo. La cla-se TerminoExpresion tiene subclases VariableExpresion para variables

Page 154: Notas Estructuras

154 Arboles

y OperadorExpresion para operadores. La clase OperadorExpresion tie-ne subclases para los operadores aritmeticos, tales como OperadorAdicion

y OperadorMultiplicacion. El metodo valor de TerminoExpresion es so-breescrito por sus subclases. Para una variable, este regresa el valor de lavariable. Para un operador, este regresa el resultado de aplicar el operadora sus operandos. Los operandos de un operador son puesto por el metodosetOperandos de OperadorExpresion. En el listado 5.14 se muestran lasclases de TerminoExpresion, VariableExpresion, OperadorExpresion yOperadorAdicion.�/** Clase para un termino (operador o variable) de una expresion aritmetica. */

public class TerminoExpresion {

public Integer getValor () { return 0; }

public String toString () { return new String(""); }

}��/** Clase para una variable de una expresion aritmetica. */

public class VariableExpresion extends TerminoExpresion {

protected Integer var;

public VariableExpresion(Integer x) { var = x; }

public void setVariable(Integer x) { var = x; }

public Integer getValor () { return var; }

public String toString () { return var.toString (); }

}��/** Clase para un operador de una expresion aritmetica. */

public class OperadorExpresion extends TerminoExpresion {

protected Integer primerOperando , segundoOperando;

public void setOperandos(Integer x, Integer y) {

primerOperando = x;

segundoOperando = y;

}

}��/** Clase para el operador adicion en una expresion aritmetica. */

public class OperadorAdicion extends OperadorExpresion {

public Integer getValor () {

return ( primerOperando + segundoOperando ); // desencajonar y

// entonces desencajonar

}

}�Listado 5.14: Clases para una variable; operador generico; y operador adicionde una expresion aritmetica.

En los listados 5.15 y 5.16 se muestran las clases EvaluarExpresionRecorridoe ImprimirExpresionRecorrido, especializando CircuitoEuleriano, que

Page 155: Notas Estructuras

5.3 Arboles Binarios 155

evalua e imprime la expresion aritmetica guardada en un arbol binario, res-pectivamente. La clase EvaluarExpresionRecorrido sobreescribe el metodoauxiliar visitaDerecha(T, v, r) con el siguiente calculo:

Si v es un nodo externo, poner r.salida igual al valor de la variableguardada en v.

Si no, v es un nodo interno, combinar r.izquierda y r.derecha conel operador guardado en v, y poner r.salida igual al resultado de laoperacion.

La clase ImprimirExpresionRecorrido sobreescribe los metodos visitaIzquierda,visitaAbajo, y visitaDerecha siguiendo la aproximacion del pseudocodigodel algoritmo plantillaCircuitoEuleriano de la seccion 5.3.7.�public class EvaluarExpresionRecorrido

extends CircuitoEuleriano <TerminoExpresion , Integer > {

public Integer ejecutar(ArbolBinario <TerminoExpresion > T) {

inicializar(T); // llama al metodo de la superclase

return circuitoEuleriano(arbol.root ());

}

protected void visitaDerecha(Posicion <TerminoExpresion > v,

ResultadoRecorrido <Integer > r) {

TerminoExpresion term = v.elemento ();

if (arbol.isInternal(v)) {

OperadorExpresion op = (OperadorExpresion) term;

op.setOperandos(r.izquierda , r.derecha );

}

r.salida = term.getValor ();

}

}�Listado 5.15: La clase EvaluarExpresionRecorrido que especializaCircuitoEuleriano para evaluar la expresion asociada con un arbol de ex-presion aritmetica.�/** Imprime la expresion guardada en un arbol de expresion aritmetica. */

public class ImprimirExpresionRecorrido

extends CircuitoEuleriano <TerminoExpresion , String > {

public String ejecutar(ArbolBinario <TerminoExpresion > A) {

inicializar(A);

System.out.print("Expresion: ");

circuitoEuleriano(A.root ());

System.out.println ();

return null; // nada que regresar

}

protected void visitaIzquierda(Posicion <TerminoExpresion > v,

ResultadoRecorrido <String > r) {

if (arbol.isInternal(v)) System.out.print("("); }

protected void visitaAbajo(Posicion <TerminoExpresion > v,

Page 156: Notas Estructuras

156 Arboles

ResultadoRecorrido <String > r) {

if (arbol.isInternal(v)) System.out.print(v.elemento ()); }

protected void visitaDerecha(Posicion <TerminoExpresion > v,

ResultadoRecorrido <String > r) {

if (arbol.isInternal(v)) System.out.print(")"); }

}�Listado 5.16: La clase ImprimirExpresionRecorrido que especializaCircuitoEuleriano para evaluar la expresion asociada con un arbol de ex-presion aritmetica.

Page 157: Notas Estructuras

Bibliografıa

[1] R. Lafore, Data Structures & Algorithms, Second Edition, Sams, 2003.

[2] M. Goodrich, R. Tamassia, Data Structures and Algorithms in Java,Fourth Edition, John Wiley & Sons, 2005.

Page 158: Notas Estructuras

Indice alfabetico

ındice, 91arbol, 117arbol binario, 130arbol binario completo, 130arbol binario de busqueda, 147arbol ordenado, 119arboles, 117arboles de decision, 131

Abstract Data Type, 7adaptador, 92ADT, 7algoritmos, 7altura, 124API, 60Application Programming Interface,

60arista, 119arreglo bidimensional, 22arreglo extendible, 97

camino, 119caso base, 43caso recursivo, 43ciclo for-each, 112cifrado de Cesar, 20circuito Euleriano, 149cola con doble terminacion, 84criptografıa, 20cursor, 102

definicion recursiva, 43deque, 84descifrado, 20double-ended queue, 84

encriptamiento, 20estructura de datos, 7estructura enlazada, 134estructura generica, 55etiquetas HTML, 74externo, 118

FIFO, 76funcion factorial, 42

generador de numeros pseudoaleato-rios, 19

hermanos, 118hijo derecho, 130hijo izquierdo, 130hijos, 117hojas, 118

insercion ordenada, 16interfaz de programacion de aplica-

ciones, 60iterador, 110iterator, 110

jerarquicas, 117

Page 159: Notas Estructuras

INDICE ALFABETICO 159

LIFO, 58lista, 91lista arreglo, 91lista circularmente enlazada, 36lista doblemente enlazada, 29lista enlazada, 26lista nodo, 99lista simple enlazada, 26

metodo patron plantilla, 151modulo, 21, 80matriz, 23

nivel, 132nivel de numeracion, 143nodos, 26

padre, 117parametros de tipo actual, 55parametros de tipo formal, 55pila, 58posicion, 99posiciones en un arbol, 119problema de Josefo, 83profundidad, 122propio, 130

rango, 91recorrido de un arbol, 126recorrido en postorden, 128recorrido en preorden, 126recurrencia, 42recurrencia binaria, 49recurrencia de cola, 49recurrencia multiple, 52relativamente, 99round robin, 83

secuencia, 91

semilla, 19subarbol derecho, 130

tipo generico, 55

vector, 91