Estructuras de Datos Con Java
Estructuras de Datos Con Java
Hector Tejeda V
Abril, 2010
Indice general
1. Introduccion 5
1.1. Que son las estructuras de datos . . . . . . . . . . . . . . . . . 5
1.2. Generalidades de las Estructuras de Datos . . . . . . . . . . . 5
3. Herramientas de an alisis 61
3.1. Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
3.1.1. La funcion constante . . . . . . . . . . . . . . . . . . . 61
INDICE GENERAL 3
4. Pilas y colas 75
4.1. Genericos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
4.2. Pilas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
4.2.1. El tipo de dato abstracto pila . . . . . . . . . . . . . . 78
4.2.2. Implementacion de una pila usando un arreglo . . . . . 81
4.2.3. Implementacion de una pila usando lista simple . . . . 87
4.2.4. Invertir un arreglo con una pila . . . . . . . . . . . . . 91
4.2.5. Aparear parentesis y etiquetas HTML . . . . . . . . . . 92
4.3. Colas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
4.3.1. Tipo de dato abstracto cola . . . . . . . . . . . . . . . 96
4.3.2. Interfaz cola . . . . . . . . . . . . . . . . . . . . . . . . 97
4.3.3. Implementacion simple de la cola con un arreglo . . . . 98
4.3.4. Implementacion de una cola con una lista enlazada . . 102
4.3.5. Planificador Round Robin . . . . . . . . . . . . . . . . 103
4.4. Colas con doble terminacion . . . . . . . . . . . . . . . . . . . 104
4.4.1. Tipo de dato abstracto deque . . . . . . . . . . . . . . 105
4.4.2. Implementacion de una deque . . . . . . . . . . . . . . 106
Introducci
on
ejemplo, el ATD pila puede ser implementado con un arreglo o bien con una
lista enlazada.
Varios de los algoritmos que seran discutidos en este material se apli-
caran directamente a estructuras de datos especficas. Para la mayora de las
estructuras de datos, se requiere que hagan las siguientes tareas:
2.1. Arreglos
En esta seccion, se exploran unas cuantas aplicaciones con arreglos.
tos de depuracion. La cadena sera una lista separada con comas de objetos
EntradaJuego del arreglo entradas. La lista se genera con un ciclo for, el
cual agrega una coma justo antes de cada entrada que venga despues de la
primera.
Inserci
on
Una de las actualizaciones mas comunes que se quiere hacer al arreglo
entradas es agregar una nueva entrada. Suponiendo que se quiera insertar un
nuevo objeto e del tipo EntradaJuego se considera como se hara la siguiente
operacion en una instancia de la clase Puntuaciones:
Remoci
on de un objeto
Suponiendo que alg un jugador hace trampa y obtiene una puntuacion
alta, se deseara tener un metodo que permita quitar la entrada del juego de
14 Arreglos, listas enlazadas y recurrencia
Figura 2.3: Remocion en el ndice 3 en el arreglo que guarda las referencias a objetos
entradaJuego.
Descripci
on de inserci
on ordenada con Java
1 /* * Inserci
o n ordenada de un arreglo de caracteres en orden creciente */
2 public static void i n se r c i o n O r d e n a d a ( char [] a ) {
3 int n = a . length ;
4 for ( int i =1; i < n ; i ++) { // ndice desde el segundo car
a cter en a
5
6 char actual = a [ i ]; // el car a
cter actual a ser insertado
7 int j = i -1; // iniciar comparando con la celda izq . de i
8 while (( j >=0) && ( a [ j ] > actual )) // mientras a [ j ] no est
a ordenado con actual
9 a [ j +1 ] = a [ j - - ]; // mover a [ j ] a la derecha y decrementar j
10 a [ j +1 ] = actual ; // este es el lugar correcto para actual
11 }
12 }
2.1.3. M
etodos para arreglos y n
umeros aleatorios
Algunos m
etodos simples
Un ejemplo con n
umeros pseudoaleatorios
El programa PruebaArreglo, listado 2.4, emplea otra caracterstica en
Javala habilidad para generar n umeros pseudoaleatorios, esto es, nume-
ros que son estadsticamente aleatorios (pero no realmente aleatorios). En
particular, este usa un objeto java.util.Random, el cual es un generador
de n
umeros pseudoaletorios. Tal generador requiere un valor para iniciar, el
20 Arreglos, listas enlazadas y recurrencia
El cifrador C
esar
Una area donde se requiere poder cambiar de una cadena a un arreglo de
caracteres y de regreso es util en criptografa, la ciencia de los mensajes secretos
y sus aplicaciones. Esta area estudia varias formas de realizar el encriptamiento,
el cual toma un mensaje, denominado el texto plano, y lo convierte en un
mensaje cifrado, llamado el ciphertext. Asimismo, la criptografa 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 de
Cesar , el cual es nombrado despues de que Julio Cesar, quien usaba este
esquema para proteger mensajes militares importantes. El cifrador de Cesar
es una forma simple para oscurecer un mensaje escrito en un lenguaje que
forma palabras con un alfabeto.
El cifrador involucra reemplazar cada letra en un mensaje con una letra
que esta tres letras despues de esta en el alfabeto para ese lenguaje. Por lo
tanto, en un mensaje del espa nol, se podra reemplazar cada A con D, cada B
con E, cada C con F, y as sucesivamente. Se contin ua de esta forma hasta la
letra W, la cual es reemplazada con Z. Entonces, se permite que la sustitucion
del patron de la vuelta, por lo que se reemplaza la X con A, Y con B, y Z
con C.
Para una letra may uscula C, por ejemplo, se puede usar C como un ndice
de arreglo tomando el valor Unicode de C y restando A. Lo anterior solo
funciona para letras may usculas, por lo que se requerira que los mensajes
secretos esten en may usculas. Se puede entonces usar un arreglo, encriptar,
que represente la regla de encriptamiento, por lo que encriptar[i] es la letra
que reemplaza la letra n umero i, la cual es c A para una letra may uscula
C en Unicode. De igual modo, un arreglo, descrif rar, puede representar la
regla de desciframiento, por lo que descif rar[i] es la letra que reemplaza la
letra n
umero i.
En el listado 2.5, se da una clase de Java simple y completa para el
cifrador de Cesar, el cual usa la aproximacion anterior y tambien realiza las
conversiones entre cadenas y arreglos de caracteres.
1 /* * Clase para hacer encr iptamien to y de sciframi ento usando el cifrador de
2 * Ce sar . */
3 public class Cesar {
4
5 public static final char [] abc = { A , B , C , D , E , F , G , H , I ,
6 J , K , L , M , N , O , P , Q , R , S , T , U , V , W , X , Y , Z };
7 protected char [] encriptar = new char [ abc . length ]; // Arreglo encrip tamiento
8 protected char [] descifrar = new char [ abc . length ]; // Arreglo descif ramiento
9
10 /* * Constructor que inicializa los arreglos de encripta miento y
11 * descif ramiento */
12 public Cesar () {
13 for ( int i =0; i < abc . length ; i ++)
14 encriptar [ i ] = abc [( i + 3) % abc . length ]; // rotar alfabeto 3 lugares
15 for ( int i =0; i < abc . length ; i ++)
16 descifrar [ encriptar [ i ] - A ] = abc [ i ]; // descifrar inverso a encriptar
17 }
18
19 /* * M e
todo de encri ptamien to */
20 public String encriptar ( String secreto ) {
21 char [] mensj = secreto . toCharArray (); // arreglo mensaje
22 for ( int i =0; i < mensj . length ; i ++) // ciclo de e ncripta miento
23 if ( Character . isUpperCase ( mensj [ i ])) // se tiene que cambiar letra
24 mensj [ i ] = encriptar [ mensj [ i ] - A ]; // usar letra como ndice
25 return new String ( mensj );
26 }
27
28 /* * M e
todo de desci framien to */
29 public String descifrar ( String secreto ) {
30 char [] mensj = secreto . toCharArray (); // arreglo mensaje
31 for ( int i =0; i < mensj . length ; i ++) // ciclo desc iframien to
32 if ( Character . isUpperCase ( mensj [ i ])) // se tiene letra a cambiar
33 mensj [ i ] = descifrar [ mensj [ i ] - A ]; // usar letra como ndice
34 return new String ( mensj );
35 }
36
37 /* * Metodo main para probar el cifrador de C
e sar */
38 public static void main ( String [] args ) {
39 Cesar cifrador = new Cesar (); // Crear un objeto cifrado de C
e sar
2.1 Arreglos 23
Listado 2.5: Una clase simple y completa de Java para el cifrador de Cesar
Y[i][i+1] = Y[i][i] + 3;
i = a.length;
j = Y[4].length;
El gato
Es un juego que se practica en un tablero de tres por tres. Dos jugadores
X y Oalternan en colocar sus respectivas marcas en las celdas del tablero,
iniciando con el jugador X. Si algun jugado logra obtener tres marcas suyas
en un renglon, columna o diagonal, entonces ese jugador gana.
La idea basica es usar un arreglo bidimensional, tablero, para mantener
el tablero del juego. Las celdas en este arreglo guardan valores para indicar
si la celda esta vaca o guarda una X o una O. Entonces, el tablero es
una matriz de tres por tres, donde por ejemplo, el renglon central son las
celdas tablero[1][0], tablero[1][1], tablero[1][2]. Para este caso,
se decidio que las celdas en el arreglo tablero sean enteros, con un cero
indicando una celda vaca, un uno indicando una X, y un -1 indicando O. Esta
codificacion permite tener una forma simple de probar si en una configuracion
del tablero es una victoria para X o para O, a saber, si los valores de un
renglon, columna o diagonal suman -3 o 3. Se ilustra lo anterior en la figura
2.5.
Se da una clase completa de Java para mantener un tablero del gato para
dos jugadores en el listado 2.6. El codigo es solo para mantener el tablero
del gato y para registrar los movimientos; no realiza ninguna estrategia, ni
permite jugar al gato contra la computadora.
1 /* * Simulaci
o n del juego del gato ( no tiene ninguna estrategia ). */
2 public class Gato {
3
4 protected static final int X = 1 , O = -1; // jugadores
5 protected static final int VACIA = 0; // celda vac
a
6 protected int tablero [][] = new int [3][3]; // tablero
2.1 Arreglos 25
Figura 2.5: Una ilustracion del juego del gato a la izquierda, y su representacion
empleando el arreglo tablero.
Listado 2.6: Una clase simple y completa para jugar gato entre dos jugadores.
O|X|O
-----
O|X|X
-----
X|O|X
Empate
fijar el tama
no del arreglo por adelantado.
Hay otras formas de guardar una secuencia de elementos, que no tiene el
inconveniente de los arreglos. En esta seccion se explora una implementacion
alterna, la cual es conocida como lista simple enlazada.
Una lista enlazada, en su forma mas simple, es una coleccion de nodos
que juntos forman un orden lineal. El ordenamiento esta determinado de tal
forma que cada nodo es un objeto que guarda una referencia a un elemento y
una referencia, llamado siguiente, a otro nodo. Ver la figura 2.7.
Figura 2.7: Ejemplo de una lista simple enlazada cuyos elementos son cadenas
indicando codigos de aeropuertos. El apuntador sig de cada nodo se muestra como
una flecha. El objeto null es denotado por .
Implementaci
on 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 guardados en
los nodos de la lista. Para este caso se asume que los elementos son String.
Tambien es posible definir nodos que puedan guardar tipos arbitrarios de
elementos. Dada la clase Nodo, se puede definir una clase, ListaSimple,
mostrada en el listado 2.8, definiendo la lista enlazada actual. Esta clase
guarda una referencia al nodo cabeza y una variable va contando el n umero
total de nodos.
1 /* * Nodo de una lista simple enlazada de cadenas . */
2
3 public class Nodo {
4
5 private String elemento ; // Los elementos son cadenas de caracteres
6 private Nodo sig ;
7
8 /* * Crea un nodo con el elemento dado y el nodo sig . */
9 public Nodo ( String e , Nodo n ) {
10 elemento = e ;
11 sig = n ;
12 }
13
14 /* * Regresa el elemento de este nodo . */
15 public String getElemento () { return elemento ; }
16
17 /* * Regresa el nodo siguiente de este nodo . */
18 public Nodo getSig () { return sig ; }
19
20 // m
e todos modificadores
21 /* * Poner el elemento de este nodo . */
22 public void setElemento ( String nvoElem ) { elemento = nvoElem ; }
23
24 /* * Poner el nodo sig de este nodo . */
25 public void setSig ( Nodo nvoSig ) { sig = nvoSig ; }
26 }
2.2 Listas simples enlazadas 29
2.2.1. Inserci
on en una lista simple enlazada
Cuando se usa una lista simple enlazada, se puede facilmente insertar un
elemento en la cabeza de la lista, como se muestra en la figura 2.8.
La idea principal es que se cree un nuevo nodo, se pone su enlace siguiente
para que se refiera al mismo objeto que la cabeza, y entonces se pone que la
cabeza apunte al nuevo nodo. Se muestra enseguida como insertar un nuevo
nodo v al inicio de una lista simple enlazada. Este metodo trabaja a un si la
lista esta vaca. Observar que se ha puesto el apuntador siguiente para el
nuevo nodo v antes de hacer que la variable 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. Inserci
on 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, como se muestra en
la figura 2.9.
En este caso, se crea un nuevo nodo, se asigna su referencia siguiente para
que apunte al objeto null, se pone la referencia siguiente de la cola para
30 Arreglos, listas enlazadas y recurrencia
Figura 2.8: Insercion de un elemento en la cabeza de una lista simple enlazada: (a)
antes de la inserci
on; (b) creacion de un nuevo nodo; (c) despues de la insercion.
2.2 Listas simples enlazadas 31
Figura 2.9: Insercion en la cola de una lista simple enlazada: (a) antes de la inserci
on;
(b) creacion de un nuevo nodo; (c) despues de la insercion. Observar que se puso el
enlace sig para tail en (b) antes de asignar a la variable tail para que apunte al
nuevo nodo en (c).
32 Arreglos, listas enlazadas y recurrencia
que apunte a este nuevo objeto, y entonces se asigna a la referencia cola este
nuevo nodo. El algoritmo agregarFinal inserta un nuevo nodo al final de
la lista simple enlazada. El metodo tambien trabaja si la lista esta vaca. Se
pone primero el apuntador siguiente para el viejo nodo cola antes de hacer
que la variable cola apunte al 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 }
Algoritmo removerPrimero():
si cabeza = null Entonces
Indicar un error: la lista esta vaca.
t cabeza
cabeza 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 una
lista simple enlazada. A un si se tiene una referencia cola directamente al
u
ltimo nodo de la lista, se necesita poder acceder el nodo anterior al u ltimo
nodo en orden para quitar el ultimo nodo. Pero no se puede alcanzar el nodo
anterior a la cola usando los enlaces siguientes desde la cola. La u nica forma
para acceder este nodo es empezar en la cabeza de la lista y moverse a traves
de la lista. Pero tal secuencia de operaciones de saltos de enlaces podra tomar
un tiempo grande.
3
4 protected String elemento ; // Cadena elemento guardada por un nodo
5 protected NodoD sig , prev ; // Apuntadores a los nodos siguiente y previo
6
7 /* * Constructor que crea un nodo con los campos dados */
8 public NodoD ( String e , NodoD p , NodoD s ) {
9 elemento = e ;
10 prev = p ;
11 sig = s ;
12 }
13
14 /* * Regresa el elemento de este nodo */
15 public String getElemento () { return elemento ; }
16 /* * Regresa el nodo previo de este nodo */
17 public NodoD getPrev () { return prev ; }
18 /* * Regresa el nodo siguiente de este nodo */
19 public NodoD getSig () { return sig ; }
20 /* * Pone el elemento de este nodo */
21 public void setElemento ( String nvoElem ) { elemento = nvoElem ; }
22 /* * Pone el nodo previo de este nodo */
23 public void setPrev ( NodoD nvoPrev ) { prev = nvoPrev ; }
24 /* * Pone el nodo siguiente de este nodo */
25 public void setSig ( NodoD nvoSig ) { sig = nvoSig ; }
26 }
Listado 2.9: Clase NodoD representando un nodo de una lista doblemente enlazada
que guarda una cadena de caracteres.
Figura 2.11: Una lista doblemente enlazada con centinelas, cabeza y cola, marcando
el final de la lista.
Figura 2.12: Remoci on del nodo al final de una lista doblemente enlazada con
centinelas: (a) antes de borrar en la cola; (b) borrando en la cola; (c) despues del
borrado.
figura 2.13.
2.3.1. Inserci
on entre los extremos de una lista doble
enlazada
Las listas dobles enlazadas no nada mas son utiles para insertar y remover
elementos en la cabeza o en la cola de la lista. Estas tambien son convenientes
para mantener una lista de elementos mientras se permite insercion y remocion
en el centro de la lista. Dado un nodo v de una lista doble enlazada, el cual
posiblemente podra ser la cabecera pero no la cola, se puede facilmente
insertar un nuevo nodo z inmediatamente despues de v. Especficamente, sea
w el siguiente nodo de v. Se ejecutan los siguientes pasos.
Figura 2.14: Insercion de un nuevo nodo despues del nodo que guarda JFK: (a)
creacion de un nuevo nodo con el elemento BWI y enlace de este; (b) despues de la
insercion.
apunten entre ellos en vez de hacerlo hacia v. Esta operacion es referida como
desenlazar a v. Tambien se anulan los apuntadores prev y sig de v para que
ya no retengan referencias viejas en la lista. El algoritmo remover muestra
como quitar el nodo v a un si v es el primer nodo, el u ltimo o un nodo no
centinela y se ilustra en la figura 2.15.
Figura 2.15: Borrado del nodo que guarda PVD: (a) antes del borrado; (b) desenlace
del viejo nodo; (c) despues de la remocion.
Algoritmo remover(v):
u v.getP rev() { nodo predecesor a v }
w v.getSig() { nodo sucesor a v }
w.setP rev(u) { desenlazar v }
u.setSig(w)
v.setP rev(null) { anular los campos de v }
v.setSig(null)
tam tam 1 { decrementar el contador de nodos }
Los objetos de la clase NodoD, los cuales guardan elementos String, son
usados para todos los nodos de la lista, incluyendo los nodos centinela
cabecera y terminacion.
55 }
Listado 2.11: Una clase de lista circularmente ligada con nodos simples.
Listado 2.12: El metodo main de un programa que usa una lista circularmente
enlazada para simular el juego de ni
nos PatoPatoGanso.
46 Arreglos, listas enlazadas y recurrencia
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 gan
o!
Alex es elegido.
Jugando Pato, Pato, Ganso con: [...Paco, Gabi, Pepe, Luis, To~
no, Pedro, Eli...]
Gabi es un Pato.
Pepe es el ganso!
El ganso perdio!
Pepe es elegido.
Jugando Pato, Pato, Ganso con: [...Alex, Luis, To~
no, Pedro, Eli, Paco, Gabi...]
Luis es un Pato.
To~
no es un Pato.
Pedro es el ganso!
El ganso gan
o!
El circulo final es [...Pedro, Pepe, Eli, Paco, Gabi, Alex, Luis, To~no...]
Observar que cada iteracion en esta ejecucion del programa genera una
salida diferente, debido a las configuraciones iniciales diferentes y el uso de
opciones aleatorias para identificar patos y ganso. Ademas, que el ganso
alcance al que esta parado depende de opciones aleatorias.
Algoritmo InsercionOrdenada(L):
Entrada: Una lista L doblemente enlazada de elementos comparables
Salida: La lista L con elementos rearreglados en orden creciente
si L.tam() 1 entonces
regresar
f in L.getP rimero()
Mientras f in no sea el ultimo nodo en L hacer
pivote f in.getSig()
Quitar pivote de L
ins f in
Mientras ins no sea la cabecera y el elemento de ins sea mayor
que el del pivote hacer
ins ins.getP rev()
Agregar pivote justo despues de ins en L
si ins = f in entonces { Se agrego pivote despues de f in en este caso}
f in f in.getSig()
Listado 2.13: Ordenamiento por insercion para una lista doblemente enlazada de la
clase ListaDoble.
2.5. Recurrencia
La repeticion se puede lograr escribiendo ciclos, como por ejemplo con
for, 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 ejemplos de
metodos que llaman a otros metodos, por lo que no debera ser extra no que
muchos de los lenguajes de programacion actual, incluyendo Java, permitan
que un metodo se llame a s mismo. En esta seccion, se vera porque esta
capacidad da una alternativa poderosa para realizar tareas repetitivas.
La funci
on factorial
Para ilustrar la recurrencia, se inicia con un ejemplo sencillo para calcular
el valor de la funcion factorial . El factorial de un entero positivo n, denotada
por n!, esta definida como el producto de los enteros desde 1 hasta n. Si n = 0,
entonces n! esta definida como uno por convencion. Mas formalmente, para
cualquier entero n 0,
1 si n = 0
n! =
1 2 (n 2) (n 1) n si n 1
Para denotar n! se empleara el siguiente formato usado en los metodos de
Java, factorial(n)
La funcion factorial puede ser definida de una forma que sugiera una
formulacion recursiva. Para ver esto, observar que
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) como
n factorial(n 1). Esto lleva a la siguiente definicion recursiva.
1 si n = 0
factorial(n) = (2.1)
n factorial(n 1) si n 1
2.5 Recurrencia 49
Implementaci
on recursiva de la funci
on 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 recursivas de la
funcion reemplazan el ciclo.
1 public static int f a c t o r i a l R e c u r s i v o ( int n ) { // funci o
n recursiva factorial
2 if ( n == 0) return 1; // caso base
3 else return n * f a c t o r i a l R e c u r s i v o (n -1); // caso recursivo
4 }
Figura 2.16: Ejemplos de la funcion dibuja regla: (a) regla de 2 con longitud 4 de
la marca principal; (b) regla de 1 con longitud 5; (c) regla de 3 con longitud 3.
2.5 Recurrencia 51
Una aproximaci
on recursiva para dibujar la regla
La aproximacion para dibujar tal regla consiste de tres funciones. La
funcion principal dibujaRegla() dibuja la regla entera. Sus argumentos son
el numero total de pulgadas en la regla, nPulgadas, y la longitud de la marca
principal, longPrincipal. La funcion utilidad dibujaUnaMarca() dibuja una
sola marca de la longitud dada. Tambien se le puede dar una etiqueta entera
opcional, la cual es impresa si esta no es negativa.
El trabajo interesante es hecho por la funcion recursiva dibujaMarcas(),
la cual dibuja la secuencia de marcas dentro de alg un intervalo. Su u nico
argumento 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
en la figura 2.16(b). Ignorando las lneas conteniendo 0 y 1, considerar como
dibujar la secuencia de marcas que estan entre estas lneas. La marca central
(en 1/2) tiene longitud 4. Los dos patrones de marcas encima y abajo de
esta marca central son identicas, y cada una tiene una marca central de
longitud 3. En general, un intervalo con una marca central de longitud L 1
esta compuesta de lo siguiente:
Listado 2.15: Una implementacion recursiva de una funcion que dibuja una regla.
Recurrencia de cola
Usar la recurrencia puede ser con frecuencia una herramienta u til para
dise
nar algoritmos que tienen definiciones cortas y elegantes. Pero esta utilidad
viene acompa nado de un costo modesto. Cuando se usa un algoritmo recursivo
para resolver un problema, se tienen que usar algunas de las localidades de la
memoria para guardar el estado de las llamadas recursivas activas. Cuando la
memoria de la computadora es un lujo, entonces, es mas util en algunos casos
poder derivar algoritmos no recurrentes de los recurrentes.
Se puede emplear la estructura de datos pila para convertir un algoritmo
recurrente en uno no recurrente, pero hay algunos casos cuando se puede hacer
esta conversion mas facil y eficiente. Especficamente, se puede facilmente con-
vertir algoritmos que usan recurrencia de cola. Un algoritmo usa recurrencia
de cola si este usa recursion lineal y el algoritmo hace una llamada recursiva
2.5 Recurrencia 55
Encontrar n
umeros de Fibonacci con recurrencia binaria
Se considera ahora el problema de calcular el k-esimo n
umero de Fibonacci.
Los n
umeros de Fibonacci estan recursivamente definidos como:
F0 = 0
F1 = 1
Fi = Fi1 + Fi2 para i > 1.
Aplicando directamente la definicion anterior, el algoritmo FibBinario,
mostrado a continuacion, encuentra la secuencia de los n
umeros de Fibonacci
empleando recurrencia binaria.
Algoritmo FibBinario(k):
Entrada: Un entero k no negativo.
Salida: El k-esimo numero de Fibonacci Fk .
si k 1 entonces
regresar k
sino
regresar FibBinario(k 1)+FibBinario(k 2)
Desafortunadamente, a pesar de que la definicion de Fibonacci parece una
recursion binaria, esta tecnica es ineficiente en este caso. De hecho, toma un
numero de llamadas exponencial para calcular el k-esimo n umero de Fibonacci
de esta forma. Especficamente, sea nk el n umero de llamadas hechas en la
ejecucion de FibBinario(k). Entonces, se tienen los siguientes valores para
las diferentes nk :
n0 = 1
n1 = 1
n2 = n1 + n0 + 1 = 3
n3 = n2 + n1 + 1 = 5
n4 = n3 + n2 + 1 = 9
n5 = n4 + n3 + 1 = 15
n6 = n5 + n4 + 1 = 25
n7 = n6 + n5 + 1 = 41
n8 = n7 + n6 + 1 = 67
Si se sigue hacia adelante el patron, se ve que el n umero de llamadas es
mas del doble para un ndice que este dos ndices anteriores. Esto es, n4 es mas
del doble que n2 , n5 es mas del doble que n3 , y as sucesivamente. Por lo tanto,
58 Arreglos, listas enlazadas y recurrencia
N
umeros de Fibonacci con recurrencia lineal
El principal problema con la aproximacion anterior, basada en recurrencia
binaria, es que el calculo de los n
umeros de Fibonacci es realmente un problema
de recurrencia lineal. No es este un buen candidato para emplear recurrencia
binaria. Se empleo recurrencia binaria por la forma como el k-esimo n umero
de Fibonacci, Fk , dependa de los dos valores previos, Fk1 y Fk2 . Pero se
puede encontrar Fk de una forma mas eficiente usando recurrencia lineal.
Para poder emplear recurrencia lineal, se necesita redefinir ligeramente
el problema. Una forma de lograr esta conversion es definir una funcion
recursiva que encuentre un par consecutivo de n umeros Fibonacci (Fk1 , Fk ).
Entonces se puede usar el siguiente algoritmo de recurrencia lineal mostrado
a continuacion.
Algoritmo FibLineal(k):
Entrada: Un entero k no negativo.
Salida: El par de numeros Fibonacci (Fk , Fk1 ).
si k 1 entonces
regresar (0, k)
sino
(i, j) FibLineal(k 1)
regresar (i + j, i)
El algoritmo dado muestra que usando recurrencia lineal para encontrar
los numeros de Fibonacci es mucho mas eficiente que usando recurrencia
binaria. Ya que cada llama recurrente a FibLineal decrementa el argumento
k en 1, la llamada original FibLineal(k) resulta en una serie de k 1
llamadas adicionales. Esto es, calcular el k-esimo n
umero de Fibonacci usando
recurrencia lineal requiere k llamadas al metodo. Este funcionamiento es
significativamente mas rapido que el tiempo exponencial necesitado para
el algoritmo basado en recurrencia binaria. Por lo tanto, cuando se usa
recurrencia binaria, se debe primero tratar de particionar completamente el
problema en dos, o se debera estar seguro que las llamadas recursivas que se
traslapan son realmente necesarias.
Usualmente, se puede eliminar el traslape de llamadas recursivas usando
mas memoria para conservar los valores previos. De hecho, esta aproximacion
2.5 Recurrencia 59
2.5.2. Recurrencia m
ultiple
La generalizacion de la recurrencia binaria lleva a la recurrencia m
ultiple,
que es cuando un metodo podra hacer varias llamadas recursivas, siendo
ese numero potencialmente mayor que dos. Una de las aplicaciones mas
comunes de este tipo de recursion es usada cuando se quiere enumerar varias
configuraciones en orden para resolver un acertijo combinatorio. Por ejemplo,
el siguiente es un acertijo de suma:
seis+de+enero=reyes
Otra forma de ver el algoritmo siguiente es que este enumera cada sub-
conjunto ordenado posible de tama no, y prueba cada subconjunto para ser
una posible solucion al acertijo.
Para acertijos de sumas, U = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 y cada posicion en la
secuencia corresponde a una letra dada. Por ejemplo, la primera posicion
podra 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 }
Agregar e al final de S
si k = 1 entonces
Probar si S es una configuracion que resuelva el acertijo
si S resuelve el acertijo entonces
regresar Solucion encontrada: S
sino
ResolverAcertijo(k 1, S, U )
Agregar e de regreso a U { e esta ahora sin uso }
Remover e del final de S
Captulo 3
Herramientas de an
alisis
3.1. Funciones
Se comenta brevemente, en esta seccion, las siete funciones mas importantes
usadas en el analisis de algoritmos.
3.1.1. La funci
on constante
La funcion mas simple que se emplea es la funcion constante. Esta es la
funcion
f (n) = c
para alguna constante fija c, tal como c = 5, c = 27, o c = 210 . Con esta
funcion, para cualquier argumento n, la funcion constante f (n) asigna el valor
c, es decir, no importa cual es el valor de n, f (n) siempre sera igual al valor
constante c.
Como se emplean generalmente funciones enteras en el analisis, la funcion
constante mas usada es g(n) = 1, y cualquier otra funcion constante, f (n) = c,
puede ser escrita como una constante c veces g(n), es decir, f (n) = cg(n).
Esta funcion caracteriza el numero de pasos requeridos para hacer una
operacion basica en la computadora, como la suma de dos n umeros, asignar
un valor a una variable, o comparar dos n umeros.
62 Herramientas de an
alisis
3.1.2. La funci
on logartmica
La funcion logartmica, f (n) = logb n, para alguna constante b > 1 es
ubicua, ya que aparece con bastante frecuencia en el analisis de estructuras
de datos y algoritmos. Esta funcion esta definida como sigue:
x = logb n s y solo s bx = n
Por definicion, logb 1 = 0. El valor de b es conocido como la base del
logaritmo.
Para encontrar el valor exacto de esta funcion para cualquier entero n
se requiere usar calculo, pero se puede usar una buena aproximacion que es
buena para el analisis sin emplear calculo. En particular, se puede calcular el
entero mas peque no que sea mayor o igual a loga n, ya que este n umero es
igual al numero de veces que se puede dividir n por a hasta que se obtenga un
numero menor que o igual a 1. Por ejemplo, log3 27 es 3, ya que 27/3/3/3 = 1.
De igual modo log4 64 es 3, porque 64/4/4/4 = 1, y la aproximacion a log2 12
es 4, ya que 12/2/2/2/2 = 0.75 1. La aproximacion base dos surge en
el analisis de algoritmos, ya que es una operacion com un en los algoritmos
divide y venceras por que repetidamente dividen la entrada por la mitad.
Ademas, como las computadoras almacenan los enteros en binario, la base
mas com un para la funcion logartmica en ciencias de la computacion es dos,
por lo que tpicamente no se indica cuando es dos, entonces se tiene,
log n = log2 n.
Hay algunas reglas importantes para logaritmos, parecidas a las reglas de
exponentes.
Proposici on 3.1: (reglas de logaritmos) dados n
umeros reales a > 0, b > 1,
c > 0, y d > 1, se tiene:
1. logb ac = logb a + logb c
2. logb a/c = logb a logb c
3. logb ac = c logb a
4. logb a = (logd a)/ logd b
5. blogd a = alogd b
Para usar una notacion abreviada, se usara logc n para denotar la funcion
(log n)c .
3.1 Funciones 63
3.1.3. La funci
on lineal
Otra funcion simple e importante es la funcion lineal ,
f (n) = n
Esto es, dado un valor de entrada n, la funcion lineal f asigna el propio
valor n.
Esta funcion surge en el analisis de algoritmos cuando se tiene que ha-
cer una operacion basica para cada uno de los n elementos. Por ejemplo,
comparar un n umero x con cada uno de los elementos de una arreglo de
tamano n requerira n comparaciones. La funcion lineal tambien representa el
mejor tiempo de ejecucion que se espera lograr para cualquier algoritmo que
procese una coleccion de n objetos que no esten todava en la memoria de la
computadora, ya que introducir los n objetos requiere n operaciones.
3.1.5. La funci
on cuadr
atica
La funcion cuadr
atica tambien aparece muy seguido en el analisis de
algoritmos,
f (n) = n2
Para un valor de entrada n, la funcion f asigna el producto de n con ella
misma, es decir, n cuadrada.
La razon principal por la que la funcion cuadratica aparece en el analisis de
algoritmos es porque hay muchos algoritmos que tienen ciclos anidados, donde
el ciclo interno realiza un numero lineal de operaciones y el ciclo externo es
realizado un n umero lineal de veces. As, en tales casos, el algoritmo realiza
n n = n2 operaciones.
64 Herramientas de an
alisis
1 + 2 + 3 + + (n 2) + (n 1) + n.
n(n + 1)
1 + 2 + 3 + + (n 2) + (n 1) + n = .
2
Lo que se puede aprender de este resultado es que si ejecuta un algoritmo
con ciclos anidados tal que las operaciones del ciclo anidado se incrementan
en uno cada vez, entonces el numero total de operaciones es cuadratico en el
n
umero de veces, n, que se hace el ciclo exterior. En particular, el n
umero de
2
operaciones es n /2 + n/2, en este caso, lo cual es un poco mas que un factor
constante (1/2) veces la funcion cuadratica n2 .
3.1.6. La funci
on c
ubica y otras polinomiales
De las funciones que son potencias de la entrada, se considera la funcion
c
ubica,
f (n) = n3
la cual asigna a un valor de entrada n el producto de n con el mismo tres
veces. Esta funcion aparece con menor frecuencia en el contexto del analisis
de algoritmos que las funciones constante, lineal o cuadratica pero aparece de
vez en vez.
Polinomiales
Algunas de las funciones que se han revisado previamente pueden ser vistas
como una parte de una clase de funciones mas grandes, las polinomiales.
3.1 Funciones 65
f (n) = a0 + a1 n + a2 n2 + a3 n3 + + ad nd ,
Sumatorias
Una notacion que aparece varias veces en el analisis de las estructuras de
datos y algoritmos es la sumatoria, la cual esta definida como sigue:
b
X
f (i) = f (a) + f (a + 1) + f (a + 2) + + f (b),
i=a
3.1.7. La funci
on exponencial
Otra funcion usada en el analisis de algoritmos es la funcion exponencial
f (n) = bn ,
1. (ba )c = bac
2. ba bc = ba+c
3. ba /bc = bac
Sumas geom
etricas
Cuando se tiene un ciclo donde cada iteracion toma un factor multipli-
cativo mayor que la previa, se puede analizar este ciclo usando la siguiente
proposicion.
Proposici on 3.4: Para cualquier entero n 0 y cualquier n
umero real a tal
que a > 0 y a 6= 1, considerar la sumatoria
n
X
ai = 1 + a + a2 + + an
i=0
an+1 1
.
a1
3.2. An
alisis de algoritmos
La herramienta de analisis primario que se usara involucra la caracte-
rizacion de los tiempos de ejecucion de los algoritmos y las estructuras de
datos, as como los espacios usados. El tiempo de ejecucion es una medida
natural de la bondad, ya que el tiempo es un recurso precioso las soluciones
computacionales deberan correr tan rapido como sea posible.
En general, el tiempo de ejecucion de un algoritmo o de un metodo de una
estructura de datos se incrementa con el tama no de la entrada, sin embargo
podra variar para entradas diferentes con el mismo tama no. Tambien, el
tiempo de ejecucion es afectado por el ambiente del hardware (como se refleja
en el procesador, frecuencia del reloj, memoria, disco, etc.) y el ambiente de
3.2 An
alisis de algoritmos 67
Llamar un metodo.
Comparar dos n
umeros.
Indexar en un arreglo.
3.2.3. Notaci
on asint
otica
En general, cada paso basico en una descripcion en pseudo-codigo o en una
implementacion de lenguaje de alto nivel corresponde a un n umero peque no
de operaciones primitivas, excepto por las llamadas a metodos. Por lo tanto
se puede hacer un analisis simple de un algoritmo que estime el n umero de
operaciones primitivas ejecutadas hasta un factor constante, por lo pasos en
el pseudo-codigo.
En el analisis de algoritmos se enfoca en la relacion de crecimiento del
tiempo de ejecucion como una funcion del tama no de la entrada n, tomando
una aproximacion o una foto grande.
Se analizan los algoritmos usando una notacion matematica para funciones
que no tiene en cuenta los factores constantes. Se caracterizan los tiempos
de ejecucion de los algoritmos usando funciones que mapean el tama no de la
entrada, n, a valores que corresponden al factor principal que determina la
relacion de crecimiento en terminos de n. Esta aproximacion permite enfocarse
en los aspectos de foto grande del tiempo de ejecucion de un algoritmo.
La notaci
on O-Grande
Sean f (n) y g(n) funciones que mapean enteros no negativos a n umeros
reales. Se dice que f (n) es O(g(n)) si hay una constante c > 0 y una constante
entera n0 1 tal que
Propiedades de la notaci
on O-Grande
f (n) = a0 + a1 n + ad nd ,
y ad > 0, entonces f (n) es O(nd ).
on: observar que, para n 1, se tiene 1 n n2 nd
Justificaci
por lo tanto
a0 + a1 n + + ad nd (a0 + a1 + + ad )nd .
Caracterizaci
on de funciones en t
erminos m
as simples
Se podra usar la notacion O-grande para caracterizar una funcion tan
cercana como sea posible. Para la funcion f (n) = 4n3 + 3n2 es cierto que es
O(n5 ) o aun O(n4 ), pero es mas preciso decir que f (n) es O(n3 ). Tambien
se considera de mal gusto incluir factores constantes y terminos de orden
inferior en la notacion O-grande. No esta de moda decir que la funcion 2n2
es O(4n2 + 6n log n), sin embargo esto es completamente correcto. Se debe
intentar describir la funcion en O-grande en los terminos mas simples.
72 Herramientas de an
alisis
-grande
La notacion O-grande proporciona una forma asintotica de decir que una
funcion es menor que o igual a otra funcion, la siguiente notacion da una
forma asintotica para indicar que una funcion crece a un ritmo que es mayor
que o igual a otra funcion.
Sean f (n) y g(n) funciones que mapean enteros no negativos a n umeros
reales. Se dice que f (n) es (g(n)) (pronunciada f(n) es Omega-grande de
g(n)) si g(n) es O(f (n)), esto es, existe una constante real c > 0 y una
constante entera n0 1 tal que
f (n) cg(n), para n n0 .
Esta definicion permite decir asintoticamente que una funcion es mayor
que o igual a otra, hasta un factor constante.
Ejemplo 3.8. La funcion 3n log n + 2n es (n log n)
Justificacion. 3n log n + 2n 3n log n, para n 2.
(Tetha-grande)
Ademas, hay una notacion que permite decir que dos funciones crecen a la
misma velocidad, hasta unos factores constantes. Se dice que f (n) es (g(n))
(pronunciado f (n) es Tetha-grande de g(n)) si f (n) es O(g(n)) y f (n) es
(g(n)), esto es, existen constantes reales c0 > 0 y c00 > 0, y una constante
entera n0 1 tal que
c0 g(n) f (n) c00 g(n), para n n0 .
Ejemplo 3.9. 3n log n + 4n + 5 log n es (n log n)
on. 3n log n 3n log n + 4n + 5 log n (3 + 4 + 5)n log n para
Justificaci
n 2.
3.2 An
alisis de algoritmos 73
3.2.4. An
alisis asint
otico
Suponiendo dos algoritmos que resuelven el mismo problema: un algoritmo
A, el cual tiene tiempo de ejecuion O(n), y un algoritmo B, con tiempo de
ejecucion O(n2 ). Se desea sabe cual algoritmo es mejor. Se sabe que n es
O(n2 ), lo cual implica que el algoritmo A es asintoticamente mejor que el
algoritmo B, sin embargo para un valor peque no de n, B podra tener un
tiempo de ejecucion menor que A.
Se puede emplear la notacion O-grande para ordenar clases de funciones
por su velocidad de crecimiento asintotico. Las siete funciones son ordenadas
por su velocidad de crecimiento incremental en la siguiente secuencia, esto es,
si una funcion f (n) precede a una funcion g(n) en la secuencia, entonces f (n)
es O(g(n)):
1 log n n n log n n2 n3 2n
n log n n n log n n2 n3 2n
2 1 2 2 4 8 4
4 2 4 8 16 64 16
8 3 8 24 64 512 256
16 4 16 64 256 4,096 65,536
32 5 32 160 1,024 32,768 4,294,967,296
64 6 64 384 4,096 262,144 1.84 1019
128 7 128 896 16,384 2,097,152 3.40 1038
256 8 256 2,048 65,536 16,777,216 1.15 1077
512 9 512 4,608 262,144 134,217,728 1.34 10154
Pilas y colas
4.1. Gen
ericos
Iniciando con la version 5.0 de Java se incluye una estructura generica para
usar tipos abstractos en una forma que evite muchas conversiones de tipos.
Un tipo generico es un tipo que no esta definido en tiempo de compilacion,
pero queda especificado completamente en tiempo de ejecucion. La estructura
generica permite definir una clase en terminos de un conjunto de parametros de
tipo formal , los cuales podran ser usados, por ejemplo, para abstraer los tipos
de algunas variables internas de la clase. Los parentesis angulares son usados
para encerrar la lista de los parametros de tipo formal. Aunque cualquier
identificador valido puede ser usado para un parametro de tipo formal, los
nombres de una sola letra may uscula por convencion son usados. Dada una
clase que ha sido definida con tales tipos parametrizados, se instancia un
objeto de esta clase usando los parametros de tipo actual para indicar los
tipos concretos que seran usados.
En el listado 4.1, se muestra la clase Par que guarda el par llave-valor,
donde los tipos de la llave y el valor son especificados por los parametros L y V,
respectivamente. El metodo main crea dos instancias de esta clase, una para
un par String-Integer (por ejemplo, para guardar un tipo de dimension y
su valor), y otro para un par Estudiante-Double (por ejemplo, para guardar
la calificacion dada a un estudiante).
1 public class Par <L , V > { // par a
metros de tipo formal entre < >
2 // variables de instancia usando un tipo gen e rico
3 L llave ;
4 V valor ;
5
76 Pilas y colas
[altura, 2]
[Estudiante(Matricula: 8403725A, Nombre: Hector, Edad: 14), 9.5]
En el ejemplo, el parametro del tipo actual puede ser un tipo arbitrario. Pa-
ra restringir el tipo de parametro actual se puede usar la clausula extends, co-
mo se muestra enseguida, donde la clase EstudianteParDirectorioGenerico
esta definido en terminos de un parametro de tipo generico E, parcialmente
especificando que este extiende la clase Estudiante.
1 public class E s tu d i a nt e P ar D i r ec t ori o G e ne r i co < E extends Estudiante > {
2
3 // ... las variables de instancia podr
an ir aqu
...
4
5 public E s t u d i a n t e P a r D i r e c t o r i o G e n e r i c o () {
6 /* el constructor por defecto aqu va */ }
7
8 public void insertar ( E estudiante , E otro ) {
9 /* c
o digo para insertar va aqui */ }
10
11 public P encontrarOtro ( E estudiante ) { return null ; } // para encontrar
12
13 public void remover ( E estudiante , E otro ) {
14 /* c
o digo para remover va aqu
*/ }
15 }
11 System . out . println ( " Primer par es " + a [0]. getLlave ()+ " ," + a [0]. getValor ());
12
13 }
Listado 4.4: Ejemplo que muestra que un tipo parametrizado sea usado para crear
un nuevo arreglo.
4.2. Pilas
Una pila (stack ) es una coleccion de objetos que son insertados y removidos
de acuerdo al principio u ltimo en entrar, primero en salir, LIFO (last-in
first-out). Los objetos pueden ser insertados en una pila en cualquier momento,
pero solamente el mas reciente insertado, es decir, el
ultimo objeto puede ser
removido en cualquier momento. Una analoga de la pila es el dispensador de
platos que se encuentra en el mobiliario de alguna cafetera o cocina. Para este
caso, las operaciones fundamentales involucran push (empujar) platos y pop
(sacar) platos de la pila. Cuando se necesita un nuevo plato del dispensador,
se saca el plato que esta encima de la pila, y cuando se agrega un plato, se
empuja este hacia abajo en la pila para que se convierta en el nuevo plato de
la cima. Otro ejemplo son los navegadores Web de internet que guardan las
direcciones de los sitios recientemente visitados en una pila. Cada vez que un
usuario visita un nuevo sitio, esa direccion del sitio es empujada en la pila de
direcciones. El navegador, mediante el boton Volver atr as, permite al usuario
sacar los sitios previamente visitados.
Listado 4.5: Excepcion lanzada por los metodos pop() y top() de la interfaz pila
cuando son llamados con una pila vaca.
Una interfaz completa para el ADT pila esta dado en el listado 4.6. Esta
interfaz es muy general ya que especifica que elementos de cualquier clase dada,
y sus subclases, pueden ser insertados en la pila. Se obtiene esta generalidad
mediante el concepto de genericos (seccion 4.1).
Para que un ADT dado sea para cualquier uso, se necesita dar una clase
concreta que implemente los metodos de la interfaz asociada con ese ADT. Se
da una implementacion simple de la interfaz Stack en la siguiente subseccion.
1 /* *
2 * Interfaz para una pila : una colecci o n de objetos que son insertados
3 * y removidos de acuerdo al principio u ltimo en entrar , primero en salir .
4 * Esta interfaz incluye los m e todos principales de java . util . Stack .
5 *
6 * @see E m p t y S t a c k E x c e p t i o n
7 */
8
9 public interface Stack <E > {
10
11 /* *
12 * Regresa el n u mero de elementos en la pila .
13 * @return n
u mero de elementos en la pila .
14 */
15 public int size ();
16
4.2 Pilas 81
17 /* *
18 * Indica si la pila est
a vacia .
19 * @return true si la pila est
a vac
a , de otra manera false .
20 */
21 public boolean isEmpty ();
22
23 /* *
24 * Explorar el elemento en la cima de la pila .
25 * @return el elemento cima en la pila .
26 * @exception E m p t y S t a c k E x c e p t i o n si la pila est
a vac
a.
27 */
28 public E top ()
29 throws E m p t y S t a c k E x c e p t i o n ;
30
31 /* *
32 * Insertar un elemento en la cima de la pila .
33 * @param elemento a ser insertado .
34 */
35 public void push ( E elemento );
36
37 /* *
38 * Quitar el elemento cima de la pila .
39 * @return elemento removido .
40 * @exception E m p t y S t a c k E x c e p t i o n si la pila est
a vac
a.
41 */
42 public E pop ()
43 throws E m p t y S t a c k E x c e p t i o n ;
44 }
4.2.2. Implementaci
on de una pila usando un arreglo
Se puede implementar una pila guardando sus elementos en un arreglo. La
pila en esta implementacion consiste de una arreglo S de n elementos ademas
de una variable entera t que da el ndice del elemento de la cima en el arreglo
S.
En Java el ndice para los arreglos inicia con cero, por lo que t se inicializa
con -1, y se usa este valor de t para identificar una pila vaca. Asimismo,
82 Pilas y colas
Algoritmo isEmpty():
regresar (t < 0)
Algoritmo top():
si isEmpty() entonces
lanzar una EmptyStackException
regresar S[t]
Algoritmo push(e):
si size()= N entonces
lanzar una FullStackException
tt+1
S[t] e
Algoritmo pop():
si isEmpty() entonces
lanzar una EmptyStackException
e S[t]
S[t] null
tt1
regresar e
An
alisis de la implementaci
on de pila con arreglos
La correctez de los metodos en la implementacion usando arreglos se tiene
inmediatamente de la definicion de los propios metodos. Hay, sin embargo,
un punto medio interesante que involucra la implementacion del metodo pop.
Se podra haber evitado reiniciar el viejo S[t] a null y se podra tener
todava un metodo correcto. Sin embargo, hay una ventaja y desventaja en
4.2 Pilas 83
Metodo Tiempo
size O(1)
isEmpty O(1)
top O(1)
push O(1)
pop O(1)
128 Object o ;
129 ArregloStack < Integer > A = new ArregloStack < Integer >();
130 A . estado ( " new ArregloStack < Integer > A " , null );
131 A . push (7);
132 A . estado ( " A . push (7) " , null );
133 o = A . pop ();
134 A . estado ( " A . pop () " , o );
135 A . push (9);
136 A . estado ( " A . push (9) " , null );
137 o = A . pop ();
138 A . estado ( " A . pop () " , o );
139 ArregloStack < String > B = new ArregloStack < String >();
140 B . estado ( " new ArregloStack < String > B " , null );
141 B . push ( " Paco " );
142 B . estado ( " B . push (\" Paco \") " , null );
143 B . push ( " Pepe " );
144 B . estado ( " B . push (\" Pepe \") " , null );
145 o = B . pop ();
146 B . estado ( " B . pop () " , o );
147 B . push ( " Juan " );
148 B . estado ( " B . push (\" Juan \") " , null );
149 }
150 }
Salida ejemplo
Se muestra enseguida la salida del programa ArregloStack. Con el uso
de tipos genericos, se puede crear un ArregloStack A para guardar enteros
y otro ArregloStack B para guardar String.
Limitaci
on de la pila con un arreglo
4.2.3. Implementaci
on de una pila usando lista simple
En esta seccion, se explora la implementacion del ADT pila usando una
lista simple enlazada. En el diseno de tal implementacion, se requiere decidir
si la cima de la pila esta en la cabeza de la lista o en la cola. Sin embargo,
hay una mejor opcion ya que se pueden insertar elementos y borrar en tiempo
constante solamente en la cabeza. Por lo tanto, es mas eficiente tener la cima
de la pila en la cabeza de la lista. Tambien, para poder hacer la operacion
88 Pilas y colas
Listado 4.8: La clase Nodo implementa un nodo generico para una lista simple
enlazada.
Clase gen
erica NodoStack
Una implementacion con Java de una pila, por medio de un lista simple
enlazada, se da en el listado 4.9. Todos los metodos de la interfaz Stack son
ejecutados en tiempo constante. Ademas de ser eficiente en el tiempo, esta
4.2 Pilas 89
Listado 4.9: Clase NodoStack implementando la interfaz Stack usando una lista
simple enlazada con los nodos genericos del listado 4.8.
Listado 4.10: Un metodo generico que invierte los elementos en un arreglo generico
mediante una pila de la interfaz Stack<E>
92 Pilas y colas
Parentesis: ( y )
Llaves: { y }
Corchetes: [ y ]
Smbolos de la funcion piso: b y c
Smbolos de la funcion techo: d y e
[(5 + x) (y + z)].
Los siguientes ejemplos ilustran este concepto:
Correcto: ()(()){([()])}
Correcto: ((()(()){([()])}))
Incorrecto: )(()){([()])}
Incorrecto: ({[ ])}
Incorrecto: (
4.2 Pilas 93
p: parrafo
4.3. Colas
Otra estructura de datos fundamental es la cola (queue). Una cola es una
coleccion de objetos que son insertados y removidos de acuerdo al principio
primero en entrar, primero en salir, FIFO (first-in first-out). Esto es, los
96 Pilas y colas
Adicionalmente, de igual modo como con el ADT pila, el ADT cola incluye
los siguientes metodos de apoyo:
Aplicaciones de ejemplo
Hay varias aplicaciones posibles para las colas. Tiendas, teatros, centros
de reservacion, y otros servicios similares que tpicamente procesen peticiones
de clientes de acuerdo al principio FIFO. Una cola sera por lo tanto una
opcion logica para una estructura de datos que maneje el procesamiento de
transacciones para tales aplicaciones.
5 *
6 * @see E m p t y Q u e u e E x c e p t i o n
7 */
8
9 public interface Queue <E > {
10 /* *
11 * Regresa el numero de elementos en la cola .
12 * @return n
u mero de elementos en la cola .
13 */
14 public int size ();
15 /* *
16 * Indica si la cola est a vac a
17 * @return true si la cola est a vac a , false de otro modo .
18 */
19 public boolean isEmpty ();
20 /* *
21 * Regresa el elemento en el frente de la cola .
22 * @return elemento en el frente de la cola .
23 * @exception E m p t y Q u e u e E x c e p t i o n si la cola est a vac
a.
24 */
25 public E front () throws E m p t y Q u e u e E x c e p t i o n ;
26 /* *
27 * Insertar un elemento en la zaga de la cola .
28 * @param elemento nuevo elemento a ser insertado .
29 */
30 public void enqueue ( E element );
31 /* *
32 * Quitar el elemento del frente de la cola .
33 * @return elemento quitado .
34 * @exception E m p t y Q u e u e E x c e p t i o n si la cola est a vac
a.
35 */
36 public E dequeue () throws E m p t y Q u e u e E x c e p t i o n ;
37 }
4.3.3. Implementaci
on simple de la cola con un arreglo
Se presenta una realizacion simple de una cola mediante un arreglo, Q,
de capacidad fija, guardando sus elementos. Como la regla principal con el
tipo ADT cola es que se inserten y borren los objetos de acuerdo al principio
FIFO, se debe decidir como se va a llevar el frente y la parte posterior de la
cola.
Una posibilidad es adaptar la aproximacion empleada para la implementa-
cion de la pila, dejando que Q[0] sea el frente de la cola y permitiendo que la
cola crezca desde all. Sin embargo, esta no es una solucion eficiente, para esta
se requiere que se muevan todos los elementos hacia adelante una celda cada
vez que se haga la operacion dequeue. Tal implementacion podra tomar por
lo tanto tiempo O(n) para realizar el metodo dequeue, donde n es el n umero
4.3 Colas 99
Algoritmo isEmpty():
return (f = r)
Algoritmo front():
si isEmpty() entonces
lanzar una EmptyQueueException
return Q[f ]
Algoritmo enqueue(e):
si size()= N 1 entonces
lanzar una FullQueueException
Q[r] e
r (r + 1) mod N
Algoritmo dequeue():
si isEmpty() entonces
lanzar una EmptyQueueException
e Q[f ]
Q[f ] null
f (f + 1) mod N
return e
4.3 Colas 101
Metodo Tiempo
size O(1)
isEmpty O(1)
front O(1)
enqueue O(1)
dequeue O(1)
102 Pilas y colas
4.3.4. Implementaci
on de una cola con una lista enla-
zada
Se puede implementar eficientemente el ADT cola usando una lista generica
simple ligada. Por razones de eficiencia, se escoge el frente de la cola para que
este en la cabeza de la lista, y la parte posterior de la cola para que este en
la cola de la lista. De esta forma se quita de la cabeza y se inserta en el final.
Para esto se necesitan mantener referencias a los nodos cabeza y cola de la
lista. Se presenta en el listado 4.13, una implementacion del metodo queue
de la cola, y en el listado 4.14 del metodo dequeue.
1 public void enqueue ( E elem ) {
2 Nodo <E > nodo = new Nodo <E >();
3 nodo . setElemento ( elem );
4 nodo . setSig ( null ); // nodo ser
a el nuevo nodo cola
5 if ( tam == 0)
6 cabeza = nodo ; // caso especial de una cola previamente vac a
7 else
8 cola . setSig ( nodo ); // agregar nodo en la cola de la lista
9 cola = nodo ; // actualizar la referencia al nodo cola
10 tam ++;
11 }
Listado 4.13: Implementacion del metodo enqueue del ADT cola por medio de una
lista simple ligada usando nodos de la clase Nodo.
Listado 4.14: Implementacion del metodo dequeue del ADT cola por medio de una
lista simple ligada usando nodos de la clase Nodo.
Cada uno de los metodos de la implementacion del ADT cola, con una
implementacion de lista ligada simple, corren en tiempo O(1). Se evita la
necesidad de indicar un tama no maximo para la cola, como se hizo en la
implementacion de la cola basada en un arreglo, pero este beneficio viene
a expensas de incrementar la cantidad de espacio usado por elemento. A
pesar de todo, los metodos en la implementacion de la cola con la lista ligada
son mas complicados que lo que se deseara, se debe tener cuidado especial
4.3 Colas 103
de como manejar los casos especiales donde la cola esta vaca antes de un
enqueue o cuando la cola se vaca despues de un dequeue.
El problema de Josefo
En el juego de ni
nos de la Papa Caliente, un grupo de n ni nos sentados
en crculo se pasan un objeto, llamado la papa alrededor del crculo. El
juego se inicia con el nino que tenga la papa pasandola al siguiente en el
crculo, y estos contin
uan pasando la papa hasta que un coordinador suena
una campana, en donde el ni no que tenga la papa debera dejar el juego y pasar
la papa al siguiente nino en el crculo. Despues de que el ni
no seleccionado
deja el juego, los otros ni
nos estrechan el crculo. Este proceso es entonces
continuado hasta que quede solamente un ni no, que es declarado el ganador.
Si el coordinador siempre usa la estrategia de sonar la campana despues de
que la papa ha pasado k veces, para alg un valor fijo k, entonces determinar
el ganador para una lista de ni nos es conocido como el problema de Josefo.
Operacion Salida D
addFirst(3) (3)
addFirst(5) (5,3)
removeFirst() 5 (3)
addLast(7) (3,7)
removeFirst() 3 (7)
removeLast() 7 ()
removeFirst() error ()
isEmpty() true ()
106 Pilas y colas
4.4.2. Implementaci
on de una deque
Como la deque requiere insercion y borrado en ambos extremos de una lista,
usando una lista simple ligada para implementar una deque sera ineficiente.
Sin embargo, se puede usar una lista doblemente enlazada, para implementar
una deque eficientemente. La insercion y remocion de elementos en cualquier
extremo de una lista doblemente enlazada es directa para hacerse 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 nodo
p que estara antes de e y al nodo q que estara despues del nodo e. Para
insertar un nuevo elemento entre los nodos p y q, cualquiera o ambos de estos
podran ser centinelas, se crea un nuevo nodo t, haciendo que los enlaces prev
y sig de t hagan referencia a p y q respectivamente.
De igual forma, para quitar un elemento guardado en un nodo t, se puede
acceder a los nodos p y q que estan a los lados de t, y estos nodos deberan
existir, ya que se usan centinelas. Para quitar el nodo t entre los nodos 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 ser reclamado por el
colector de basura, ya que no hay nadie que este apuntando a t.
Todos los metodos del ADT deque, como se describen previamente, estan
incluidos en la clase java.util.LinkedList<E>. Por lo tanto, si se necesita
usar una deque y no se desea implementarla desde cero, se puede usar la clase
anterior.
De cualquier forma, se muestra la interfaz Deque en el listado 4.16 y una
implementacion de esta interfaz en el listado 4.17.
1 /* *
2 * Interfaz para una deque : una colecci o n de objetos que pueden ser
3 * insertados y quitados en ambos extremos ; un subconjunto de los
4 * me todos de java . util . LinkedList .
5 *
6 */
7 public interface Deque <E > {
8 /* *
9 * Regresa el n u mero de elementos en la deque .
10 */
11 public int size ();
12 /* *
13 * Determina si la deque est a vac
a.
14 */
15 public boolean isEmpty ();
16 /* *
17 * Regresa el primer elemento ; una excepci o n es lanzada si la deque
18 * est a vac
a.
19 */
4.4 Colas con doble terminaci
on 107
Listado 4.16: Interfaz Deque documentada con comentarios en estilo Javadoc. Se usa
el tipo parametrizado generico E con lo cual una deque puede contener elementos
de cualquier clase especificada.
1 /* *
2 * Implementaci
o n de la interfaz Deque mediante una lista doblemente
3 * enlazada . Esta clase usa la clase NodoDL , la cual implementa un nodo de
4 * la lista .
5 */
6 public class DequeNodo <E > implements Deque <E > {
7 protected NodoDL <E > cabeza , cola ; // nodos centinelas
8 protected int tam ; // cantidad de elementos
9
10 /* * Constructor sin par a metros para crear una deque vac
a . */
11 public DequeNodo () { // inicializar una deque vac a
12 cabeza = new NodoDL <E >();
13 cola = new NodoDL <E >();
14 cabeza . setSig ( cola ); // hacer que cabeza apunte a cola
15 cola . setPrev ( cabeza ); // hacer que cola apunte a cabeza
16 tam = 0;
17 }
18
19 /* *
20 * Regresar el tama n~ o de la deque , esto es el n u
mero de elementos que tiene .
21 * @return N u mero de elementos en la deque
22 */
23 public int size () {
24 return tam ;
25 }
26 /* *
108 Pilas y colas
Listado 4.17: Clase DequeNodo que implementa la interfaz Deque con la clase NodoDL
(que no se muestra). NodoDL es un nodo de una lista doblemente enlazada generica.
110 Pilas y colas
Captulo 5
Listas e Iteradores
Operacion Salida S
add(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)
5.1 Lista arreglo 113
5.1.2. Patr
on de dise
no adaptador
Las clases son frecuentemente escritas para dar funcionalidad similar a
otras clases. El patron de dise
no adaptador se aplica a cualquier contexto
donde se quiere modificar una clase existente para que sus metodos empaten
con los de una clase o una interfaz relacionada diferente. Una forma general
para aplicar el patron adaptador es definir la nueva clase de tal forma que esta
contenga una instancia de la clase vieja como un campo oculto, e implementar
cada metodo de la nueva clase usando metodos de esta variable de instancia
oculta. El resultado de aplicar el patron adaptador sirve para que una nueva
clase realice casi las mismas funciones que alguna clase previa, pero de una
forma mas conveniente.
Con respecto al ADT lista arreglo, se observa que esta es suficiente para
definir una clase adaptadora para el ADT deque, como se ve en la tabla 5.1:
5.1.3. Implementaci
on simple con un arreglo
Una opcion obvia para implementar el ADT lista arreglo es usar un arreglo
A, donde A[i] guarda una referencia al elemento con ndice i. Se escoge el
tama no N del arreglo A lo suficientemente grande, y se mantiene el numero
de 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]. La
implementacion de los metodos add(i, e) y remove(i) son dados a continua-
cion. Una parte importante y consumidora de tiempo de esta implementacion
114 Listas e Iteradores
Algoritmo remove(i):
e A[i] { e es una variable temporal }
para j i, i + 1, . . . , n 2 hacer
A[j] A[j + 1] {llenar el espacio del elemento quitado}
nn1
regresar e
Metodo Tiempo
size() O(1)
isEmpty() O(1)
get(i) O(1)
set(i, e) O(1)
add(i, e) O(n)
remove(i) O(n)
La clase java.util.ArrayList
Java proporciona una clase, java.util.ArrayList, que implementa todos
los metodos que se dieron previamente para el ADT lista arreglo. Esta incluye
5.1 Lista arreglo 117
todos los metodos dados en el listado 5.1 para la interfaz ListaIndice. Por
otra parte, la clase java.util.ArrayList tiene caractersticas adicionales a
las que fueron dadas en el ADT lista arreglo simplificado. Por ejemplo, la
clase java.util.ArrayList tambien incluye un metodo, clear(), el cual
remueve todos los elementos de la lista de arreglo, y un metodo, toArray(),
el cual regresa un arreglo conteniendo todos los elementos de la lista arreglo
en el mismo orden. Ademas, la clase java.util.ArrayList tambien tiene
metodos para buscar en la lista, incluyendo un metodo indexOf(e), el cual
regresa el ndice de la primera ocurrencia de un elemento igual a e en la
lista arreglo, y un metodo lastIndexOf(e), el cual regresa el ndice de la
ultima ocurrencia de un elemento igual a e en la lista arreglo. Ambos metodos
regresan el valor ndice invalido 1 si no se encuentra un elemento igual a e.
Figura 5.2: Los tres pasos para crecer un arreglo extendible: (a) crea un nuevo
arreglo B; (b) copiar los elementos de A a B; (c) reasignar referencia A al nuevo
arreglo
Implementaci
on de la interfaz ListaIndice con un arreglo extendi-
ble
Se dan porciones de una implementacion Java del ADT lista arreglo usando
un arreglo extendible en el codigo 5.2. Esta clase solo proporciona los medios
para crecer. Un ejercicio es la implementacion para poder reducir.
1 /* * Realizaci o n de una lista indizada por medio de un arreglo , el cual es
2 * doblado cuando el tama ~ n o de la lista excede la capacidad del arreglo .
3 */
4 public class ArregloListaIndice <E > implements ListaIndice <E > {
5 private E [] A ; // arreglo almacenando los elementos de la lista indizada
6 private int capacidad = 16; // tama ~ n o inicial del arreglo A
7 private int tam = 0; // n u mero de elementos guardados en la lista
8 /* * Crear la lista indizada con capacidad inicial de 16. */
9 public A r r e g l o L i s t a I n d i c e () {
10 A = ( E []) new Object [ capacidad ]; // el compilador podr a advertir ,
11 // pero est a bien
12 }
13 /* * Insertar un elemento en el ndice dado . */
14 public void add ( int r , E e )
15 throws I n d e x O u t O f B o u n d s E x c e p t i o n {
16 revisarIndice (r , size () + 1);
17 if ( tam == capacidad ) { // un sobreflujo
18 capacidad *= 2;
19 E [] B =( E []) new Object [ capacidad ]; // el compilador podr a advertir
5.1 Lista arreglo 119
An
alisis amortizado del arreglo extendible
La estrategia de reemplazo del arreglo podra parecer lento a simple vista,
ya que para realizarlo se ocupa tiempo O(n). Pero se debe observar que
despues de que se haga el reemplazo, en el nuevo arreglo se pueden agregar
hasta n nuevos elementos en la lista arreglo antes de que el arreglo deba ser
reemplazado nuevamente. Este hecho permite mostrar que realizando una
serie de operaciones en un lista arreglo vaco es actualmente muy eficiente. Se
considerara la insercion de un elemento con el ndice mas grande posible, es
decir, n. En la tabla 5.3 se muestra el tiempo que toma realizar la insercion
al final de la lista, iniciando con un arreglo de tamano 1, ademas del tamano
del arreglo.
Usando un patron de dise no algortmico llamado amortizacion, se puede
mostrar que realizando una secuencia de operaciones, como las descritas
previamente, sobre una lista arreglo implementada con un arreglo extendible
es eficiente. Para realizar un analisis amortizado, se usa una tecnica de
120 Listas e Iteradores
lista enlazada, simple o doble, entonces podra ser mas natural y eficiente
usar un nodo en vez de un ndice como medio de identificacion para acceder
a S o para actualizar. Se define en esta seccion el ADT lista nodo el cual
abstrae la estructura de datos concreta lista enlazada, secciones 2.2 y 2.3,
usando el ADT posicion relacionada, el cual abstrae la nocion de lugar en
una lista nodo.
mediante una actualizacion a los enlaces sig y prev de sus vecinos. De igual
modo, se puede insertar, en tiempo O(1), un nuevo elemento e en S con una
operacion tal como addAfter(v, e), la cual indica el nodo v despues del cual
el nodo del nuevo elemento debera ser insertado. En este caso se enlaza el
nuevo nodo.
Para abstraer y unificar las diferentes formas de guardar elementos en las
diversas implementaciones de una lista, se introduce el concepto de posici on,
la cual formaliza la nocion intuitiva de lugar de un elemento relativo a los
otros en la lista.
5.2.2. Posiciones
A fin de ampliar de forma segura el conjunto de operaciones para listas,
se abstrae la nocion de posicion la cual permite usar la eficiencia de
la implementacion de las listas simples enlazadas, o dobles, sin violar los
principios del dise
no orientado a objetos. En este marco, se ve a la lista como
una coleccion que guarda a cada elemento en una posicion y que mantiene
estas posiciones organizadas en un orden lineal. Una posicion es por s misma
un 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 de
sus vecinos. En una lista, una posicion p siempre estara despues de alguna
posicion q y antes de alguna posicion s, a menos que p sea la primera
posicion o la ultima. Una posicion p, la cual esta asociada con alg
un elemento
e en una lista S, no cambia, a un si el ndice de e cambia en S, a menos que se
quite explcitamente e, y por lo tanto, se destruya la posicion p. Por otra parte,
la posicion p no cambia incluso si se reemplaza o intercambia el elemento
e guardado en p con otro elemento. Estos hechos acerca de las posiciones
permiten definir un conjunto de metodos de lista basados en posiciones que
usan objetos posicion como parametros y tambien proporcionan objetos
posicion como valores de regreso.
M
etodos de actualizaci
on 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 lista
nodo, los cuales usan objetos posicion como parametros y regresan objetos
posicion como valores de regreso algunos de ellos.
Figura 5.4: Una lista nodo. Las posiciones en el orden actual son p, q, r, y s.
p = null
p es la u
ltima posicion de la lista y se llama next(p)
Operacion Salida S
addFirst(8) (8)
p1 first() [8] (8)
addAfter(p1 , 5) (8,5)
p2 next(p1 ) [5] (8,5)
addBefore(p2 , 3) (8,3,5)
p3 prev(p2 ) [3] (8,3,5)
addFirst(9) (9,8,3,5)
p2 last() [5] (9,8,3,5)
remove(first()) 9 (8,3,5)
set(p3 , 7) 3 (8,7,5)
addAfter(first(), 2) (8,2,7,5)
Una interfaz, con los metodos requeridos, para el ADT lista nodo, llamada
ListaPosicion, esta dada en el listado 5.4. Esta interfaz usa las siguientes
excepciones para indicar condiciones de error.
Cuadro 5.4: Realizacion de una deque por medio de una lista nodo.
5.2.4. Implementaci
on lista doblemente enlazada
Si se quiere implementar el ADT lista nodo usando una lista doblemente
enlazada, seccion 2.3, se debe hacer que los nodos de la lista enlazada im-
plementen el ADT posicion. Esto es, se tiene cada nodo implementando la
interfaz Posicion y por lo tanto definen un metodo, elemento(), el cual
regresa el elemento guardado en el nodo. As, los propios nodos act
uan como
posiciones. Son vistos internamente por la lista enlazada como nodos, pero
desde el exterior, son vistos solamente como posiciones. En la vista interna,
se puede dar a cada nodo v variables de instancia prev y next que respecti-
vamente refieran a los nodos predecesor y sucesor de v, los cuales podran ser
de hecho nodos centinelas, cabeza y cola, marcando el inicio o el final de la
lista. En lugar de usar las variables prev y next directamente, se definen los
metodos getPrev, setPrev, getNext, y setNext de un nodo para acceder y
modificar estas variables.
En el listado 5.5, se muestra la clase Java, NodoD, para los nodos de una
lista doblemente enlazada implementando el ADT posicion. Esta clase es
similar a la clase NodoD mostrada en el listado 2.9, excepto que ahora los
nodos guardan un elemento generico en lugar de una cadena de caracteres.
Observar que las variables de instancia prev y next en la clase NodoD son
referencias privadas a otros objetos NodoD.
1 public class NodoD <E > implements Posicion <E > {
2 private NodoD <E > prev , next ; // Referencia a los nodos anterior y posterior
3 private E elemento ; // Elemento guardado en esta posici on
4 /* * Constructor */
5 public NodoD ( NodoD <E > nuevoPrev , NodoD <E > nuevoNext , E elem ) {
5.2 Listas nodo 129
6 prev = nuevoPrev ;
7 next = nuevoNext ;
8 elemento = elem ;
9 }
10 // M
e todo de la interfaz Posicion
11 public E elemento () throws I n v a l i d P o s i t i o n E x c e p t i o n {
12 if (( prev == null ) && ( next == null ))
13 throw new I n v a l i d P o s i t i o n E x c e p t i o n ( "La posici
o n no est
a en una lista ! " );
14 return elemento ;
15 }
16 // M
e todos accesores
17 public NodoD <E > getNext () { return next ; }
18 public NodoD <E > getPrev () { return prev ; }
19 // M
e todos actu alizador es
20 public void setNext ( NodoD <E > nuevoNext ) { next = nuevoNext ; }
21 public void setPrev ( NodoD <E > nuevoPrev ) { prev = nuevoPrev ; }
22 public void setElemento ( E nuevoElemento ) { elemento = nuevoElemento ; }
23 }
Listado 5.5: Clase NodoD realizando un nodo de una lista doblemente enlazada
implementando la interfaz Posicion.
Figura 5.6: Quitando el objeto guardado en la posicion para PVD: (a) antes de
la remoci
on; (b) desenlazando el nodo viejo; (c) despues de la remocion.
Implementaci
on de la lista nodo en Java
14 cola = new NodoD <E >( cabeza , null , null ); // crea cola
15 cabeza . setNext ( cola ); // hacer que cabeza apunte a cola
16 }
17 /* * Regresa el n u mero de elementos en la lista ; tiempo O (1) */
18 public int size () { return tam ; }
19 /* * Valida si la lista est a vac a ; tiempo O (1) */
20 public boolean isEmpty () { return ( tam == 0); }
21 /* * Regresa la primera posici o n en la lista ; tiempo O (1) */
22 public Posicion <E > first ()
23 throws E m p t y L i s t E x c e p t i o n {
24 if ( isEmpty ())
25 throw new E m p t y L i s t E x c e p t i o n ( " La lista est a vac a " );
26 return cabeza . getNext ();
27 }
28 /* * Regresa la u ltima posici o n en la lista ; tiempo O (1) */
29 public Posicion <E > last ()
30 throws E m p t y L i s t E x c e p t i o n {
31 if ( isEmpty ())
32 throw new E m p t y L i s t E x c e p t i o n ( " La lista est a vac a " );
33 return cola . getPrev ();
34 }
35 /* * Regresa la posici o n anterior de la dada ; tiempo O (1) */
36 public Posicion <E > prev ( Posicion <E > p )
37 throws InvalidPositionException , B o u n d a r y V i o l a t i o n E x c e p t i o n {
38 NodoD <E > v = revis aPosicio n ( p );
39 NodoD <E > prev = v . getPrev ();
40 if ( prev == cabeza )
41 throw new B o u n d a r y V i o l a t i o n E x c e p t i o n
42 ( " No se puede avanzar m a s all a del inicio de la lista " );
43 return prev ;
44 }
45 /* * Regresa la posici o n despu e s de la dada ; tiempo O (1) */
46 public Posicion <E > next ( Posicion <E > p )
47 throws InvalidPositionException , B o u n d a r y V i o l a t i o n E x c e p t i o n {
48 NodoD <E > v = revis aPosicio n ( p );
49 NodoD <E > next = v . getNext ();
50 if ( next == cola )
51 throw new B o u n d a r y V i o l a t i o n E x c e p t i o n
52 ( " No se puede avanzar m a s all a del final de la lista " );
53 return next ;
54 }
55 /* * Insertar el elemento dado antes de la posici o n dada : tiempo O (1) */
56 public void addBefore ( Posicion <E > p , E elemento )
57 throws I n v a l i d P o s i t i o n E x c e p t i o n {
58 NodoD <E > v = revis aPosicio n ( p );
59 tam ++;
60 NodoD <E > nodoNuevo = new NodoD <E >( v . getPrev () , v , elemento );
61 v . getPrev (). setNext ( nodoNuevo );
62 v . setPrev ( nodoNuevo );
63 }
64 /* * Insertar el elemento dado despu e s de la posici
o n dada : tiempo O (1) */
65 public void addAfter ( Posicion <E > p , E elemento )
66 throws I n v a l i d P o s i t i o n E x c e p t i o n {
67 NodoD <E > v = revis aPosicio n ( p );
68 tam ++;
69 NodoD <E > nodoNuevo = new NodoD <E >( v , v . getNext () , elemento );
70 v . getNext (). setPrev ( nodoNuevo );
5.2 Listas nodo 133
71 v . setNext ( nodoNuevo );
72 }
73 /* * Insertar el elemento dado al inicio de la lista , regresando
74 * la nueva posici o n ; tiempo O (1) */
75 public void addFirst ( E elemento ) {
76 tam ++;
77 NodoD <E > nodoNuevo = new NodoD <E >( cabeza , cabeza . getNext () , elemento );
78 cabeza . getNext (). setPrev ( nodoNuevo );
79 cabeza . setNext ( nodoNuevo );
80 }
81 /* * Insertar el elemento dado al final de la lista , regresando
82 * la nueva posici o n ; tiempo O (1) */
83 public void addLast ( E elemento ) {
84 tam ++;
85 NodoD <E > ultAnterior = cola . getPrev ();
86 NodoD <E > nodoNuevo = new NodoD <E >( ultAnterior , cola , elemento );
87 ultAnterior . setNext ( nodoNuevo );
88 cola . setPrev ( nodoNuevo );
89 }
90 /* * Quitar la posici o n dada de la lista ; tiempo O (1) */
91 public E remove ( Posicion <E > p )
92 throws I n v a l i d P o s i t i o n E x c e p t i o n {
93 NodoD <E > v = revis aPosicio n ( p );
94 tam - -;
95 NodoD <E > vPrev = v . getPrev ();
96 NodoD <E > vNext = v . getNext ();
97 vPrev . setNext ( vNext );
98 vNext . setPrev ( vPrev );
99 E vElem = v . elemento ();
100 // desenlazar la posici o n de la lista y hacerlo inv a lido
101 v . setNext ( null );
102 v . setPrev ( null );
103 return vElem ;
104 }
105 /* * Reemplaza el elemento en la posici o n dada con el nuevo elemento
106 * y regresar el viejo elemento ; tiempo O (1) */
107 public E set ( Posicion <E > p , E elemento )
108 throws I n v a l i d P o s i t i o n E x c e p t i o n {
109 NodoD <E > v = revis aPosicio n ( p );
110 E elemVjo = v . elemento ();
111 v . setElemento ( elemento );
112 return elemVjo ;
113 }
114 /* * Regresa un iterador con todos los elementos en la lista . */
115 public Iterator <E > iterator () { return new ElementoIterador <E >( this ); }
116 /* * Regresa una colecci o n iterable de todos los nodos en la lista . */
117 public Iterable < Posicion <E > > positions () { // crea una lista de posiciones
118 ListaPosicion < Posicion <E > > P = new ListaNodoPosicion < Posicion <E > >();
119 if (! isEmpty ()) {
120 Posicion <E > p = first ();
121 while ( true ) {
122 P . addLast ( p ); // agregar posici o n p como el u lt . elemento de la lista P
123 if ( p == last ())
124 break ;
125 p = next ( p );
126 }
127 }
134 Listas e Iteradores
185 }
186 s += " ] " ;
187 return s ;
188 }
189 /* * Regresar una representaci o n textual de una lista nodo dada */
190 public static <E > String toString ( ListaPosicion <E > l ) {
191 Iterator <E > it = l . iterator ();
192 String s = " [ " ;
193 while ( it . hasNext ()) {
194 s += it . next (); // moldeo impl cito del elemento a String
195 if ( it . hasNext ())
196 s += " , " ;
197 }
198 s += " ] " ;
199 return s ;
200 }
201 /* * Regresa una representaci o n textual de la lista */
202 public String toString () {
203 return toString ( this );
204 }
205 }
5.3. Iteradores
Un calculo tpico en una lista arreglo, lista, o secuencia es recorrer sus
elementos en orden, uno a la vez, por ejemplo, para buscar un elemento
especfico.
Listado 5.8: Ejemplo de un iterador Java usado para convertir una lista nodo a
una cadena.
List<Integer> valores;
...sentencias que crean una nueva lista y la llenan con Integer.
int suma = 0;
for (Integer i : valores)
suma += i; // el moldeo implcito permite esto
5.3.3. Implementaci
on de los iteradores
Una forma de implementar un iterador para una coleccion de elementos es
hacer una foto de esta e iterar sobre esta. Esta aproximacion involucrara
guardar la coleccion en una estructura de datos separada que soporte acceso
secuencial a sus elementos. Por ejemplo, se podran insertar todos los ele-
mentos de la coleccion en una cola, en tal caso el metodo hasNext() podra
corresponder a !isEmpty() y next() podra corresponder a dequeue(). Con
5.3 Iteradores 139
esta 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
coleccion, y no en una copia.
Para implementar esta aproximacion directa, se necesita solo realizar un
seguimiento a donde el cursor del iterador apunta en la coleccion. As, crear un
nuevo iterador en este caso involucra crear un objeto iterador que represente
un cursor colocado solo antes del primer elemento de la coleccion. Igualmente,
hacer el metodo next() involucra devolver el siguiente elemento, si este existe,
y mover el cursor pasando esta posicion del elemento. En esta aproximacion,
crear un iterador cuesta tiempo O(1), al igual que hacer cada uno de los
metodos del iterador. Se muestra una clase implementando tal iterador en el
listado 5.9, y se muestra en el listado 5.10 como este iterador podra ser usado
para implementar el metodo iterator() en la clase ListaNodoPosicion.
1 public class ElementoIterador <E > implements Iterator <E > {
2 protected ListaPosicion <E > lista ; // la lista subyacente
3 protected Posicion <E > cursor ; // la siguiente posici on
4 /* * Crea un elemento iterador sobre la lista dada . */
5 public E l e m en t o I t e r a d o r ( ListaPosicion <E > L ) {
6 lista = L ;
7 cursor = ( lista . isEmpty ())? null : lista . first ();
8 }
9 /* * Valida si el iterador tiene un objeto siguiente . */
10 public boolean hasNext () { return ( cursor != null ); }
11 /* * Regresa el siguiente objeto en el iterador . */
12 public E next () throws N o S u c h E l e m e n t E x c e p t i o n {
13 if ( cursor == null )
14 throw new N o S u c h E l e m e n t E x c e p t i o n ( " No hay elemento siguiente " );
15 E aRegresar = cursor . elemento ();
16 cursor = ( cursor == lista . last ())? null : lista . next ( cursor );
17 return aRegresar ;
18 }
Iteradores posici
on
Para los ADT que soporten la nocion de posicion, tal como las ADT lista
y secuencia, se puede tambien dar el siguiente metodo:
140 Listas e Iteradores
1 public Iterable < Posicion <E > > positions () { // crea una lista de posiciones
2 ListaPosicion < Posicion <E > > P = new ListaNodoPosicion < Posicion <E > >();
3 if (! isEmpty ()) {
4 Posicion <E > p = first ();
5 while ( true ) {
6 P . addLast ( p ); // agregar posici o
n p como el
u lt . elemento de la lista P
7 if ( p == last ())
8 break ;
9 p = next ( p );
10 }
11 }
12 return P ; // regresar P como el objeto iterable
13 }
Conversi
on de listas a arreglos
Las listas son un concepto interesante y pueden ser aplicadas en una canti-
dad de contextos diferentes, pero hay algunos casos donde podra ser mas util
si pudiera usar la lista como un arreglo. La interfaz java.util.Collection
incluye los siguientes metodos u tiles para generar un arreglo que tenga los
mismos elementos que la coleccion dada:
Conversi
on de arreglos a listas
De manera similar, es u til en ocasiones poder convertir un arreglo en su
lista equivalente. La clase java.util.Arrays incluye el siguiente metodo:
5.4.2. Secuencias
Una secuencia es un ADT que soporta todos los metodos del ADT deque
(seccion 4.16), el ADT lista arreglo (seccion 5.1), y el ADT lista nodo (seccion
5.2). Entonces, este proporciona acceso explcito a los elementos en la lista ya
sea por sus ndices o por sus posiciones. Por otra parte, como se proporciona
capacidad de acceso dual, se incluye en el ADT secuencia, los siguientes dos
metodos puente que proporcionan conexion entre ndices y posiciones:
146 Listas e Iteradores
Cuadro 5.5: Correspondencia entre los metodos de los ADT lista arreglo y lis-
ta nodo (1a columna) contra las interfaces List y ListIterator del paquete
java.util. Se usa A y L como abreviaturas para las clases java.util.ArrayList
y java.util.LinkedList, las cuales implementan la interfaz java.util.List.
5.4 ADT listas y el marco de colecciones 147
Herencia m
ultiple en el ADT secuencia
La definicion del ADT secuencia incluye todos los metodos de tres ADT
diferentes siendo un ejemplo de herencia m ultiple. Esto es, el ADT secuencia
hereda metodos de tres super tipos de datos abstractos. En otras palabras,
sus metodos incluyen la union de los metodos de estos super ADT. Ver el
listado 5.13 para una especificacion Java del ADT como una interfaz Java.
1 /* *
2 * Una interfaz para una secuecia , una estructura de datos que
3 * soporta todas las operaciones de un deque , lista indizada y
4 * lista posici o
n.
5 */
6 public interface Secuencia <E >
7 extends Deque <E > , ListaIndice <E > , ListaPosicion <E > {
8 /* * Regresar la posici o n conteniendo el elemento en el ndice dado . */
9 public Posicion <E > atIndex ( int r ) throws B o u n d a r y V i o l a t i o n E x c e p t i o n ;
10 /* * Regresar el ndice del elemento guardado en la posici o n dada . */
11 public int indexOf ( Posicion <E > p ) throws I n v a l i d P o s i t i o n E x c e p t i o n ;
12 }
O(mn(i + 1, n 1))
donde n es el n
umero de elementos en la lista. El peor caso de este tipo de
b
usquedas ocurre cuando
r = bn/2c
Por lo tanto, el tiempo de ejecucion todava es O(n).
Las operaciones add(i, e) y remove(i) tambien deben hacer saltos para
ubicar el nodo que guarda el elemento con ndice i, y entonces insertar o
borrar un nodo. Los tiempos de ejecucion de estas implementaciones son
tambien
O(mn(i + 1, n 1))
lo cual es O(n). Una ventaja de esta aproximacion es que, si i = 0 o i = n 1,
como en el caso de la adaptacion del ADT lista arreglo para el ADT deque dado
en la seccion 5.1.2, entonces estos metodos se ejecutan en tiempo O(1). Pero,
en general, usar metodos lista arreglo con un objeto java.util.LinkedList
es ineficiente.
59 return T ;
60 }
61 /* * Representaci o n String de lista de favoritos */
62 public String toString () { return listaF . toString (); }
63 /* * M e
todo auxiliar que extrae el valor de la entrada en una posici o n */
64 protected E valor ( Posicion < Entrada <E > > p ) { return p . elemento (). valor (); }
65 /* * M e
todo auxiliar que extrae el contador de la entrada en una pos . */
66 protected int cuenta ( Posicion < Entrada <E > > p ) { return p . elemento (). cuenta (); }
67
68 /* * Clase interna para guardar elementos y sus cuentas de acceso . */
69 protected static class Entrada <E > {
70 private E valor ; // elemento
71 private int cuenta ; // cuenta de accesos
72 /* * Constructor */
73 Entrada ( E v ) {
74 cuenta = 1;
75 valor = v ;
76 }
77 /* * Regresar el elemento */
78 public E valor () { return valor ; }
79 public int cuenta () { return cuenta ; }
80 public int i n c r e m e n t a r C u e n t a () { return ++ cuenta ; }
81 public String toString () { return " [ " + cuenta + " ," + valor + " ] " ; }
82 }
83 } // Fin de la clase Li staFavor itos
Listado 5.14: La clase ListaFavoritos que incluye una clase anidada, Entrada, la
cual representa los elementos y su contador de accesos
...
lo cual es O(n3 ).
Por otra parte, si se emplea la heurstica mover al frente, insertando cada
elemento la primera vez, entonces
...
Por lo que el tiempo de ejecucion para realizar todos los accesos en este
caso es O(n2 ). As, la implementacion mover al frente tiene tiempos de acceso
mas rapidos para este escenario. Sin embargo este beneficio vien a un costo.
Implementaci
on de la heurstica mover al frente
En el listado 5.15 se da una implementacion de una lista de favoritos usando
la heurstica mover al frente, definiendo una nueva clase ListaFavoritosMF, la
cual extiende a la clase ListaFavoritos y entonces se anulan las definiciones
de los metodos moverArriba y top. El metodo moverArriba en este caso
quita el elemento accedido de su posicion frente en la lista ligada y la inserta
de regreso en la lista en el frente. El metodo top, por otra parte, es mas
complicado.
154 Listas e Iteradores
El listado 5.16 muestra una aplicacion que crea dos listas de favoritos, una
emplea la clase ListaFavoritos y la otra la clase ListaFavoritosMF. Las
listas de favoritos son construidas usando un arreglo de direcciones Web, y
despues se generan 20 numeros pseudoaleatorios que estan en el rango de 0 al
tama no del arreglo menos uno. Cada vez que se marca un acceso se muestra
el estado de las listas. Posteriormente se obtiene, de cada lista, el top del
tama no de la lista. Usando el sitio mas popular de una de las listas se abre
en una ventana.
5.5 Caso de estudio: la heurstica mover al frente 155
3. Se regresa la lista A.
Arboles
6.1.
Arboles generales
En este captulo, se discute una de las estructuras de datos no lineal
mas importante en computacion arboles. Las estructuras arbol son ademas
un progreso en la organizacion de datos, ya que permiten implementar un
conjunto de algoritmos mas rapidos que cuando se usan estructuras lineales
de datos, como una lista. Los arboles tambien dan una organizacion natural
para datos, y por lo tanto, se han convertido en estructuras indispensables
en sistemas de archivos, interfaces graficas de usuario, bases de datos, sitios
Web, y otros sistemas de computo.
Cuando se dice que los arboles no son lineales, se hace referencia a
una relacion organizacional que es mas rica que las relaciones simples antes
y despues entre objetos en secuencias. Las relaciones en un arbol son
jer
arquicas, con algunos objetos estando encima y algunos otros abajo.
Actualmente, la terminologa principal para estructuras de dato arbol viene
de los arboles genealogicos, con algunos terminos padre, hijo, ancestro,
y descendiente siendo las palabras comunes mas usadas para describir
relaciones.
6.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 tiene
un elemento padre y cero o mas elementos hijos. Un arbol es visualizado colo-
cando elementos dentro de ovalos o rectangulos, y dibujando las conexiones
158
Arboles
entre padres e hijos con lneas rectas, ver figura 6.1. Se llama al elemento
cima la raz del arbol, pero este es dibujado como el elemento mas alto, con
los otros elementos estando conectados abajo, justo lo opuesto de un arbol
vivo.
6.1.2. Definici
on formal de
arbol
Se define un arbol A como un conjunto de nodos guardando elementos
tal que los nodos tienen una relacion padre-hijo, que satisfacen las siguientes
propiedades:
Aristas y caminos en
arboles
Una arista del arbol A es un par de nodos (u, v) tal que u es el padre de
v, o viceversa. Un camino de A es una secuencia de nodos tal que dos nodos
consecutivos cualesquiera en la secuencia forman una arista. Por ejemplo, el
arbol de la figura 6.2 contiene el camino cs016/programs/pr2.
Ejemplo 6.2. La relacion de herencia entre clases en un programa Java
forman un arbol. La raz, java.lang.Object, es un ancestro de todas las otras
clases. Cada clase, C, es una descendiente de esta raz y es la raz de un
subarbol de las clases que extienden C. As, hay un camino de C a la raz,
java.lang.Object, en este arbol de herencia.
Arboles ordenados
Figura 6.3: Un
arbol ordenado asociado con un libro.
Estos metodos hacen la programacion con arboles mas facil y mas legible,
ya que pueden ser usados en las condiciones de las sentencias if y de los ciclos
while, en vez de usar un condicional no intuitivo.
Hay tambien un numero de metodos genericos que un arbol probablemente
podra soportar y que no estan necesariamente relacionados con la estructura
del arbol, incluyendo los siguientes:
6.1.4. Implementaci
on un
arbol
La interfaz Java mostrada en el listado 6.1 representa el ADT arbol.
Las condiciones de error son manejadas como sigue: cada metodo puede
tomar una posicion como un argumento, estos metodos podran lanzar
InvalidPositionException para indicar que la posicion es invalida. El
metodo parent lanza BoundaryViolationException si este es llamado sobre
un arbol vaco. En el caso de que se intente obtener la raz de un arbol A
vaco se lanza EmptyTreeException.
6.1 Arboles generales 163
Listado 6.1: La interfaz Java Tree representando el ADT arbol. Metodos adicionales
de actualizaci
on podran ser agregados dependiendo de la aplicacion.
Figura 6.4: La estructura enlazada para un arbol general: (a) el objeto posicion
asociado con un nodo; (b) la porcion de la estructura de dato asociada con un nodo
y sus hijos.
Operacion Tiempo
size, isEmpty O(1)
iterator, positions O(n)
replace O(1)
root, parent O(1)
children(v) O(cv )
isInternal, isExternal, isRoot O(1)
Altura
La altura de un nodo v en un arbol A esta tambien definida recursivamente:
n
umero de nodos de A, dv es la profundidad del nodo v, y la suma se
Phace solo
con el conjunto de nodos externos de A. En el peor caso, la suma v (1 + dv )
es proporcional a n2 . As, el algoritmo height1 corre en tiempo O(n2 ).
El algoritmo height2, mostrado a continuacion e implementado en Java
en el listado 6.4, calcula la altura del arbol A de una forma mas eficiente
usando la definicion recursiva de altura.
Algoritmo height2(A, v):
si v es un nodo externo de A entonces
regresar 0
sino
h0
para cada hijo w de v en A hacer
h max(h, height2(A, w))
regresar 1+h
1 public static <E > int height2 ( Tree <E > T , Posicion <E > v ) {
2 if ( T . isExternal ( v ) ) return 0;
3 int h = 0;
4 for ( Posicion <E > w : T . children ( v ) )
5 h = Math . max (h , height2 ( T , w ));
6 return 1 + h ;
7 }
de A.
Figura 6.5: Recorrido en preorden de un arbol ordenado, donde los hijos de cada
nodo est
an ordenados de izquierda a derecha.
P (A) = v.elemento().toString().
De otra forma,
P (A) = v.elemento().toString() + ( + P (A1 ) + , + + , + P (Ak ) + ),
donde v es la raz de A y A1 , A2 , . . . , Ak son los subarboles enraizados de los
hijos de v, los cuales estan dados en orden si A es un arbol ordenado.
170
Arboles
El tama
no del directorio propio.
Los tama
nos de los archivos en el directorio.
6.3.
Arboles Binarios
Un arbol binario es un arbol ordenado con las siguientes propiedades:
de v. Un arbol binario es propio si cada nodo tiene cero o dos hijos. Algunos
autores tambien se refieren a tales arboles como arboles binarios completos.
As, en un arbol binario propio, cada nodo interno tiene exactamente dos
hijos. Un arbol binario que no es propio es impropio.
Ejemplo 6.6. Una clase de arboles binarios se emplean en los contextos donde
se desea representar un n umero de diferentes salidas que pueden resultar de
contestar una serie de preguntas si o no. Cada nodo interno esta asociado con
una pregunta. Iniciando en la raz, se va con el hijo izquierdo o derecho del
nodo actual, dependiendo de si la respuesta a la pregunta fue Si o No. Con
cada decision, se sigue una arista desde un padre a un hijo, eventualmente
trazando un camino en el arbol desde la raz a un nodo externo. Tales arboles
binarios son conocidos como arboles de decision, porque cada nodo externo V
en tal arbol representa una decision de que hacer si la pregunta asociada con
los ancestros de v fueron contestadas en una forma que llevan a v. Un arbol de
decision es un arbol binario propio. La figura 6.8 muestra un arbol de decision
para ordenar en forma ascendente tres valores guardados en las variables A,
B y C, donde los nodos internos del arbol representan comparaciones y los
nodos externos representan la salida ordenada.
Figura 6.8: Un
arbol de decisi
on para ordenar tres variables en orden creciente
Ejemplo 6.7. Una expresi on aritmetica puede ser representada con un arbol
binario cuyos nodos externos estan asociados con variables o constantes, y
cuyos nodos internos estan asociados con los operadores +, , y /. Ver
figura 6.9. Cada nodo en tal arbol tiene un valor asociado con este.
constante.
Si un nodo es interno, entonces su valor esta definido aplicando la
operacion a los valores de sus hijos.
Definici
on recursiva de un
arbol binario
Se puede definir un arbol binario en una forma recursiva tal que un arbol
binario esta vaco o consiste de:
Un nodo r, llamado la raz de A y que guarda un elemento.
Un arbol binario, llamado el subarbol izquierdo de A.
Un arbol binario, llamado el subarbol derecho de A.
6.3.1. El ADT
arbol binario
Como un tipo de dato abstracto, un arbol binario es una especializacion
de un arbol que soporta cuatro metodos accesores adicionales:
6.3 Arboles Binarios 177
Listado 6.9: Interfaz Java ArbolBinario para el ADT arbol binario que extiende a
Tree (listado 6.1).
Figura 6.10: N
umero maximo de nodos en los niveles de un arbol binario.
Se observa que el n
umero maximo de nodos en los niveles del arbol binario
crece exponencialmente conforme se baja en el arbol. De esta observacion, se
pueden derivar las siguientes propiedades de relacion entre la altura de un
arbol binario A con el numero de nodos.
Proposici on 6.2: Sea A un arbol binario no vaco, y sean n, nE , nI y h el
numero de nodos, n umero de nodos externos, numero de nodos internos, y la
altura de A, respectivamente. Entonces A 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
6.3 Arboles Binarios 179
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
nE = nI + 1.
Figura 6.11: Operacion para quitar un nodo externo y su nodo padre, usado en la
demostracion
Implementaci
on Java de un nodo del
arbol binario
Se emplea la interfaz Java PosicionAB para representar un nodo del arbol
binario. Esta interfaz extiende Posicion, por lo que se hereda el metodo
elemento, y tiene metodos adicionales para poner el elemento en el nodo,
setElemento y para poner y devolver el padre, el hijo izquierdo y el hijo
derecho, como se muestra en el listado 6.10.
1 /* *
2 * Interfaz para un nodo de un a rbol binario . Esta mantiene un elemento ,
3 * un nodo padre , un nodo izquierdo , y nodo derecho .
4 *
5 */
6 public interface PosicionAB <E > extends Posicion <E > { // hereda elemento ()
7 public void setElemento ( E o );
8 public PosicionAB <E > getIzq ();
9 public void setIzq ( PosicionAB <E > v );
10 public PosicionAB <E > getDer ();
11 public void setDer ( PosicionAB <E > v );
12 public PosicionAB <E > getPadre ();
13 public void setPadre ( PosicionAB <E > v );
14 }
Listado 6.10: Interfaz Java PosicionAB para definir los metodos que permiten
acceder a los elementos de un nodo del arbol binario.
6.3 Arboles Binarios 181
Figura 6.12: Un nodo (a) y una estructura enlazada (b) para representar un arbol
binario.
182
Arboles
Listado 6.11: Clase auxiliar NodoAB para implementar los nodos del arbol binario.
Implementaci
on Java del
arbol binario enlazado
En el listado 6.12 se muestra la clase ArbolBinarioEnlazado que imple-
menta la interfaz ArbolBinario, listado 6.9, usando una estructura de datos
enlazada. Esta clase guarda el tama no del arbol y una referencia al objeto
NodoAB asociado con la raz del arbol en variables internas. Adicional a los
metodos de la interfaz ArbolBinario, la clase tiene otros metodos, incluyendo
6.3 Arboles Binarios 183
236 if ( isRoot ( v ))
237 remove ( v );
238 else {
239 Posicion <E > u = parent ( v );
240 remove ( v );
241 remove ( u );
242 }
243 }
244 // M
e todos auxiliares
245 /* * Si v es un nodo de un a rbol binario , convertir a PosicionAB ,
246 * si no lanzar una excepci o n */
247 protected PosicionAB <E > re visaPosi cion ( Posicion <E > v )
248 throws I n v a l i d P o s i t i o n E x c e p t i o n {
249 if ( v == null || !( v instanceof PosicionAB ))
250 throw new I n v a l i d P o s i t i o n E x c e p t i o n ( " La posici
o n no es v a lida " );
251 return ( PosicionAB <E >) v ;
252 }
253 /* * Crear un nuevo nodo de un a rbol binario */
254 protected PosicionAB <E > creaNodo ( E elemento , PosicionAB <E > parent ,
255 PosicionAB <E > left , PosicionAB <E > right ) {
256 return new NodoAB <E >( elemento , parent , left , right ); }
257 /* * Crear una lista que guarda los nodos en el sub a rbol de un nodo ,
258 * ordenada de acuerdo al recorrido en preordel del sub a rbol . */
259 protected void p r e o r d en P o s i c i o n e s ( Posicion <E > v , ListaPosicion < Posicion <E > > pos )
260 throws I n v a l i d P o s i t i o n E x c e p t i o n {
261 pos . addLast ( v );
262 if ( hasLeft ( v ))
263 p r e o r d e n P o s i c i o n e s ( left ( v ) , pos ); // recursividad en el hijo izquierdo
264 if ( hasRight ( v ))
265 p r e o r d e n P o s i c i o n e s ( right ( v ) , pos ); // recursividad en el hijo derecho
266 }
267 /* * Crear una lista que guarda los nodos del sub a rbol de un nodo ,
268 ordenados de acuerdo al recorrido en orden del sub a rbol . */
269 protected void p o s i c i o n e s E n o r d e n ( Posicion <E > v , ListaPosicion < Posicion <E > > pos )
270 throws I n v a l i d P o s i t i o n E x c e p t i o n {
271 if ( hasLeft ( v ))
272 p o s i c i o n e s E n o r d e n ( left ( v ) , pos ); // recursividad en el hijo izquierdo
273 pos . addLast ( v );
274 if ( hasRight ( v ))
275 p o s i c i o n e s E n o r d e n ( right ( v ) , pos ); // recursividad en el hijo derecho
276 }
277 }
Rendimiento de ArbolBinarioEnlazado
Los tiempos de ejecucion de los metodos de la clase ArbolBinarioEnlazado,
el cual usa una representacion de estructura enlazada, se muestran enseguida:
6.3.5. Representaci
on lista arreglo para el
arbol bina-
rio
Esta representacion alterna de un arbol binario A esta basado en una
forma de numerar los nodos de A. Para cada nodo v de A, sea p(v) el entero
definido como sigue:
Tal implementacion es simple y eficiente, para poder usarlo para los metodos
root, parent, left, right, hasLeft, hasRight, isInternal, isExternal,
e isRoot usando operaciones aritmeticas sobre los n umeros p(v) asociados
con cada nodo v involucrado en la operacion.
Se muestra un ejemplo de representacion lista arreglo de un arbol binario
en la figura 6.14.
Figura 6.14: Representacion de un arbol binario A por medio de una lista arreglo
S.
6.3.6. Recorrido de
arboles binarios
Construcci
on de un
arbol de expresi
on
Algoritmo construirExpresion(E):
Entrada: Una expresion aritmetica completamente parentetizada E =
e0 , e1 , . . . , en1 , siendo cada ei una variable, operador, o smbolo parentesis
Salida: Un arbol binario A representando la expresion aritmetica E
P una nueva pila inicialmente vaca
para i 0 hasta n 1 hacer
si ei es una variable o un operador entonces
A un nuevo arbol binario vaco
A.addRoot(ei )
P.push(A)
sino si ei =0 (0 entonces
Continuar el ciclo
sino { ei =0 )0 }
A2 P.pop() { el arbol que representa a E2 }
A P.pop() { el arbol que representa a }
A1 P.pop() { el arbol que representa a E1 }
A.attach(A.root(), A1 , A2 )
P.push(A)
regresar P.pop()
Recorrido en preorden de un
arbol binario
Como cualquier arbol binario puede ser visto como un arbol general, el
recorrido en preorden para arboles generales, seccion 6.2.2, puede ser aplicado
a cualquier arbol binario y simplificado como se muestra enseguida.
Algoritmo preordenBinario(A, v):
Realizar la accion visita para el nodo v
si v tiene un hijo izquierdo u en A entonces
preordenBinario(A, u) { recursivamente recorrer el subarbol izquierdo }
si v tiene un hijo derecho w en A entonces
preordenBinario(A, w) { recursivamente recorrer el subarbol derecho }
Como en el caso de los arboles generales, hay varias aplicaciones de
recorrido en preorden para arboles binarios.
Recorrido en postorden de un
arbol binario
De manera analoga, el recorrido en postorden para arboles generales,
seccion 6.2.3, puede ser especializado para arboles binarios, como se muestra
194
Arboles
enseguida.
Algoritmo postordenBinario(A, v):
si v tiene un hijo izquierdo u en A entonces
postordenBinario(A, u) { recursivamente recorrer el subarbol izquierdo }
si v tiene un hijo derecho w en A entonces
postordenBinario(A, w) { recursivamente recorrer el subarbol
derecho }
Realizar la accion visita para el nodo v
Evaluaci
on de un
arbol de expresi
on
Recorrido en orden de un
arbol binario
Un metodo de recorrido adicional para un arbol binario es el recorrido
en orden. En este recorrido, se visita un nodo entre los recorridos recursivos
de sus subarboles izquierdo y derecho. El recorrido en orden del subarbol
enraizado en un nodo v de un arbol binario A se da en el siguiente algoritmo.
Algoritmo enordenBinario(A, v):
si v tiene un hijo izquierdo u en A entonces
enordenBinario(A, u) { recursivamente recorrer el subarbol izquierdo }
Realizar la accion visita para el nodo v
si v tiene un hijo derecho w en A entonces
enordenBinario(A, w) { recursivamente recorrer el subarbol derecho }
El recorrido en orden de un arbol binario A puede ser visto informalmente
como visitar los nodos de A de izquierda a derecha. Ademas, para cada
nodo v, el recorrido en orden visita v despues de que todos los nodos en el
subarbol izquierdo de v y antes que todos los nodos en subarbol derecho de v,
ver figura 6.15.
Arboles binarios de b
usqueda
Sea S un conjunto cuyos elementos tienen una relacion de orden. Por
ejemplo, S podra ser un conjunto de enteros. Un arbol binario de b
usqueda
196
Arboles
puede ser visto como un arbol de decision, donde la pregunta hecha en cada
nodo interno es si el elemento en ese nodo es menor que, igual a, o mayor que
el elemento que se esta buscando.
El tiempo de ejecucion de la b
usqueda en un arbol binario de b usqueda A
es proporcional a la altura de A. La altura de un arbol binario propio con n
nodos puede ser tan peque no como log(n + 1) 1 o tan grande como (n 1)/2,
ver la propiedades del arbol binario, seccion 6.3.3. Por lo anterior los arboles
binarios de b
usqueda son mas eficientes cuando tienen alturas peque nas.
x(v) es el n
umero de nodos visitados antes de v en el recorrido en orden
de A.
y(v) es la profundidad de v en A.
Recorrido de Euler de un
arbol binario
Los algoritmos de recorrido para arbol que se han visto, todos ellos son
de la forma de iteradores. Cada recorrido visita los nodos de un arbol en
un cierto orden, y se garantiza visitar cada nodo exactamente una vez. Se
pueden unificar los tres algoritmos de recorrido dados en una sola estructura,
relajando los requerimientos de que cada nodo sea visitado una sola vez. La
ventaja de este recorrido es que permite tipos mas generales de algoritmos
para ser expresados sencillamente.
El recorrido de Euler de un arbol binario A puede ser informalmente
definido como una caminata alrededor de A, que se inicia yendo desde la
raz hacia su hijo izquierdo, viendo a las aristas de A como paredes que
siempre se conservan a la izquierda, ver figura 6.18. Cada nodo v de A es
encontrado tres veces por el recorrido de Euler:
6.3.7. Plantilla m
etodo patr
on
Los metodos de recorrido para arboles descritos previamente son ejemplos
de patrones de dise no de software orientado al objeto, la plantilla metodo
patron. La plantilla metodo patron describe un mecanismo generico de calculo
que puede ser especializado para una aplicacion particular redefiniendo cierto
pasos. Siguiendo la plantilla metodo patron, se dise na un algoritmo que
implementa un recorrido de Euler generico de un arbol binario. Se muestra
enseguida el algoritmo plantillaRecorridoEuler.
Algoritmo plantillaRecorridoEuler(A, v):
r nuevo objeto del tipo ResultadoRecorrido
visitarIzquierda(A, v, r)
si A.hasLeft(v) entonces
r.izq plantillaRecorridoEuler(A, A.left(v))
visitarAbajo(A, v, r)
si A.hasRight(v) entonces
r.der plantillaRecorridoEuler(A, A.right(v))
visitarDerecha(A, v, r)
regresar r.salida
6.3 Arboles Binarios 201
Regresar r.salida.
Implementaci
on Java
La clase RecorridoEuler, mostrada en el listado 6.13, implementa un
recorrido transversal de Euler usando la plantilla metodo patron. El recorrido
transversal es hecha por el metodo RecorridoEuler. Los metodos auxiliares
llamados por RecorridoEuler son lugares vacos, tienen cuerpos vacos o
solo regresan null. La clase RecorridoEuler es abstracta y por lo tanto no
puede ser instanciada. Contiene un metodo abstracto, llamado ejecutar, el
cual necesita ser especificado en las subclases concretas de RecorridoEuler.
1 /* *
2 * Plantilla para algoritmos que recorren un a rbol binario usando un
3 * recorrido euleriano . Las subclases de esta clase redefinir an
4 * algunos de los m
e todos de esta clase para crear un recorrido
202
Arboles
5 * espec fico .
6 */
7 public abstract class RecorridoEuler <E , R > {
8 protected ArbolBinario <E > arbol ;
9 /* * Ejecuci o n del recorrido . Este m e todo abstracto deber a ser
10 * especificado en las subclases concretas . */
11 public abstract R ejecutar ( ArbolBinario <E > T );
12 /* * Inicializaci o n del recorrido */
13 protected void inicializar ( ArbolBinario <E > T ) { arbol = T ; }
14 /* * M e todo plantilla */
15 protected R recorri doEuler ( Posicion <E > v ) {
16 ResultadoRecorrido <R > r = new ResultadoRecorrido <R >();
17 v i s i t a r I z q u i e rd a (v , r );
18 if ( arbol . hasLeft ( v ))
19 r . izq = r ecorrido Euler ( arbol . left ( v )); // recorrido recursivo
20 visitarAbajo (v , r );
21 if ( arbol . hasRight ( v ))
22 r . der = r ecorrido Euler ( arbol . right ( v )); // recorrido recursivo
23 visi tarDerec ha (v , r );
24 return r . salida ;
25 }
26 // M e todos auxiliares que pueden ser redefinidos por las subclases
27 /* * M e todo llamado para visitar a la izquierda */
28 protected void v i s i t a r I z q u i e r d a ( Posicion <E > v , ResultadoRecorrido <R > r ) {}
29 /* * M e todo llamado para visitar abajo */
30 protected void visitarAbajo ( Posicion <E > v , ResultadoRecorrido <R > r ) {}
31 /* * M e todo llamado para visitar a la derecha */
32 protected void vi sitarDer echa ( Posicion <E > v , ResultadoRecorrido <R > r ) {}
33
34 /* Clase interna para modelar el resultado del recorrido */
35 public class ResultadoRecorrido <R > {
36 public R izq ;
37 public R der ;
38 public R salida ;
39 }
40 }
[1] R. Lafore, Data Structures & Algorithms, Second Edition, Sams, 2003.
ndice, 91 criptografa, 16
arbol, 123 cursor, 101
arbol binario, 137
arbol binario completo, 137 definicion recursiva, 39
arbol binario de busqueda, 155 deque, 85
arbol binario propio, 137 descifrado, 17
arbol impropio, 137 double-ended queue, 85
arbol ordenado, 126 encriptamiento, 16
arboles, 123 estructura de datos, 3
arboles de decision, 138 estructura generica, 61
Abstract Data Type, 3 etiquetas HTML, 77
adaptador, 92 externo, 124
ADT, 3 FIFO, 78
algoritmos, 3 funcion cubica, 52
altura, 130 funcion constante, 49
analisis del peor caso, 55 funcion cuadratica, 51
analisis promedio, 55 funcion exponencial, 52
API, 65 funcion exponente, 53
Application Programming Interface, funcion factorial, 39
65 funcion lineal, 50
arista, 125 funcion logartmica, 49
arreglo bidimensional, 19 funciones polinomiales, 52
arreglo extendible, 96
generador de n umeros pseudoaleato-
camino, 125 rios, 16
caso base, 39
caso recursivo, 39 hermanos, 124
ciclo for-each, 110 heurstica, 121
cifrado de Cesar, 17 heurstica mover al frente, 121
cola con doble terminacion, 85 hijos, 123
INDICE ALFABETICO
209
O-grande, 56
operaciones primitivas, 54
padre, 123
parametros de tipo actual, 61
parametros de tipo formal, 61
pila, 63