Anda di halaman 1dari 281

Estructuras de Datos con Java

Hector Tejeda V

Abril, 2010
Indice general

1. Introducci on 7
1.1. Clases, tipos, y objetos . . . . . . . . . . . . . . . . . . . . . . 7
1.1.1. Tipos base . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.1.2. Objetos . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.1.3. Tipo enum . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.2. Metodos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.3. Expresiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.3.1. Literales . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.3.2. Operadores . . . . . . . . . . . . . . . . . . . . . . . . 24
1.3.3. Conversiones tipo base . . . . . . . . . . . . . . . . . . 28
1.4. Control de flujo . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1.4.1. Sentencias if y switch . . . . . . . . . . . . . . . . . . 30
1.4.2. Ciclos . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
1.4.3. Sentencias explcitas de control de flujo . . . . . . . . . 35
1.5. Arreglos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
1.5.1. Declaracion de arreglos . . . . . . . . . . . . . . . . . . 40
1.5.2. Arreglos como objetos . . . . . . . . . . . . . . . . . . 41
1.6. Entrada y salida . . . . . . . . . . . . . . . . . . . . . . . . . 42
1.7. Clases anidadas y paquetes . . . . . . . . . . . . . . . . . . . . 45

2. Diseno orientado al objeto 49


2.1. Metas, principios, y patrones . . . . . . . . . . . . . . . . . . . 49
2.1.1. Metas del diseno orientado a objetos . . . . . . . . . . 49
2.1.2. Principios del dise
no orientado al objeto . . . . . . . . 50
2.1.3. Patrones de diseno . . . . . . . . . . . . . . . . . . . . 53
2.2. Herencia y polimorfismo . . . . . . . . . . . . . . . . . . . . . 54
2.2.1. Herencia . . . . . . . . . . . . . . . . . . . . . . . . . . 54
2.2.2. Polimorfismo . . . . . . . . . . . . . . . . . . . . . . . 56
INDICE GENERAL 3

2.2.3. Uso de herencia en Java . . . . . . . . . . . . . . . . . 57


2.3. Excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
2.3.1. Lanzamiento de excepciones . . . . . . . . . . . . . . . 66
2.3.2. Atrapar excepciones . . . . . . . . . . . . . . . . . . . 68
2.4. Interfaces y clases abstractas . . . . . . . . . . . . . . . . . . . 70
2.4.1. Implementacion de interfaces . . . . . . . . . . . . . . 70
2.4.2. Herencia m ultiple en interfaces . . . . . . . . . . . . . 74
2.4.3. Clases abstractas y tipado fuerte . . . . . . . . . . . . 75
2.5. Conversiones y genericos . . . . . . . . . . . . . . . . . . . . . 76
2.5.1. Conversiones para objetos . . . . . . . . . . . . . . . . 76
2.5.2. Genericos . . . . . . . . . . . . . . . . . . . . . . . . . 81

3. Arreglos, listas enlazadas y recurrencia 85


3.1. Que son las estructuras de datos . . . . . . . . . . . . . . . . . 85
3.1.1. Generalidades de las Estructuras de Datos . . . . . . . 85
3.2. Arreglos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
3.2.1. Guardar entradas de juego en un arreglo . . . . . . . . 87
3.2.2. Ordenar un arreglo . . . . . . . . . . . . . . . . . . . . 94
3.2.3. Metodos para arreglos y n umeros aleatorios . . . . . . 97
3.2.4. Arreglos bidimensionales y juegos de posicion . . . . . 103
3.3. Listas simples enlazadas . . . . . . . . . . . . . . . . . . . . . 108
3.3.1. Insercion en una lista simple enlazada . . . . . . . . . . 110
3.3.2. Quitar un elemento de una lista simple enlazada . . . . 112
3.4. Listas doblemente enlazadas . . . . . . . . . . . . . . . . . . . 113
3.4.1. Insercion entre los extremos de una lista doble enlazada 116
3.4.2. Remover en el centro de una lista doblemente enlazada 117
3.4.3. Una implementacion de una lista doblemente enlazada 119
3.5. Listas circulares y ordenamiento . . . . . . . . . . . . . . . . . 121
3.5.1. Lista circular enlazada . . . . . . . . . . . . . . . . . . 121
3.5.2. Ordenar una lista doblemente enlazada . . . . . . . . . 126
3.6. Recurrencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
3.6.1. Recurrencia binaria . . . . . . . . . . . . . . . . . . . . 135
3.6.2. Recurrencia m ultiple . . . . . . . . . . . . . . . . . . . 138

4. Herramientas de an alisis 141


4.1. Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
4.1.1. La funcion constante . . . . . . . . . . . . . . . . . . . 141
4.1.2. La funcion logartmica . . . . . . . . . . . . . . . . . . 142
4 INDICE GENERAL

4.1.3. La funcion lineal . . . . . . . . . . . . . . . . . . . . . 143


4.1.4. La funcion N-Log-N . . . . . . . . . . . . . . . . . . . 143
4.1.5. La funcion cuadratica . . . . . . . . . . . . . . . . . . . 143
4.1.6. La funcion cubica y otras polinomiales . . . . . . . . . 144
4.1.7. La funcion exponencial . . . . . . . . . . . . . . . . . . 145
4.2. Analisis de algoritmos . . . . . . . . . . . . . . . . . . . . . . 146
4.2.1. Estudios experimentales . . . . . . . . . . . . . . . . . 147
4.2.2. Operaciones primitivas . . . . . . . . . . . . . . . . . . 148
4.2.3. Notacion asintotica . . . . . . . . . . . . . . . . . . . . 149
4.2.4. Analisis asintotico . . . . . . . . . . . . . . . . . . . . . 153

5. Pilas y colas 155


5.1. Pilas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
5.1.1. El tipo de dato abstracto pila . . . . . . . . . . . . . . 155
5.1.2. Implementacion de una pila usando un arreglo . . . . . 159
5.1.3. Implementacion de una pila usando lista simple . . . . 163
5.1.4. Invertir un arreglo con una pila . . . . . . . . . . . . . 165
5.1.5. Aparear parentesis y etiquetas HTML . . . . . . . . . . 168
5.2. Colas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
5.2.1. Tipo de dato abstracto cola . . . . . . . . . . . . . . . 172
5.2.2. Interfaz cola . . . . . . . . . . . . . . . . . . . . . . . . 173
5.2.3. Implementacion simple de la cola con un arreglo . . . . 175
5.2.4. Implementacion de una cola con una lista enlazada . . 178
5.2.5. Planificador Round Robin . . . . . . . . . . . . . . . . 180
5.3. Colas con doble terminacion . . . . . . . . . . . . . . . . . . . 181
5.3.1. Tipo de dato abstracto deque . . . . . . . . . . . . . . 182
5.3.2. Implementacion de una deque . . . . . . . . . . . . . . 183

6. Listas e Iteradores 187


6.1. Lista arreglo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
6.1.1. El tipo de dato abstracto lista arreglo . . . . . . . . . . 188
6.1.2. Patron de diseno adaptador . . . . . . . . . . . . . . . 189
6.1.3. Implementacion simple con un arreglo . . . . . . . . . 189
6.1.4. Una interfaz simple y la clase java.util.ArrayList . . . . 192
6.1.5. Lista arreglo usando un arreglo extendible . . . . . . . 193
6.2. Listas nodo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
6.2.1. Operaciones basadas en nodos . . . . . . . . . . . . . . 198
6.2.2. Posiciones . . . . . . . . . . . . . . . . . . . . . . . . . 199
INDICE GENERAL 5

6.2.3. El tipo de dato abstracto lista nodo . . . . . . . . . . . 200


6.2.4. Implementacion lista doblemente enlazada . . . . . . . 203
6.3. Iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
6.3.1. Tipos de dato abstracto Iterador e Iterable . . . . . . . 208
6.3.2. El ciclo for-each . . . . . . . . . . . . . . . . . . . . . . 211
6.3.3. Implementacion de los iteradores . . . . . . . . . . . . 212
6.3.4. Iteradores lista en Java . . . . . . . . . . . . . . . . . . 215
6.4. ADT listas y el marco de colecciones . . . . . . . . . . . . . . 216
6.4.1. Listas en el marco de colecciones Java . . . . . . . . . 216
6.4.2. Secuencias . . . . . . . . . . . . . . . . . . . . . . . . . 219
6.5. Caso de estudio: la heurstica mover al frente . . . . . . . . . . 224
6.5.1. Implementacion con una lista ordenada y una clase
anidada . . . . . . . . . . . . . . . . . . . . . . . . . . 224
6.5.2. Lista con heurstica mover al frente . . . . . . . . . . . 225

7. Arboles 231

7.1. Arboles generales . . . . . . . . . . . . . . . . . . . . . . . . . 231
7.1.1. Definiciones de arboles y propiedades . . . . . . . . . . 231
7.1.2. Definicion formal de arbol . . . . . . . . . . . . . . . . 232
7.1.3. El tipo de dato abstracto arbol . . . . . . . . . . . . . 235
7.1.4. Implementacion un arbol . . . . . . . . . . . . . . . . . 236
7.2. Algoritmos de recorrido para arbol . . . . . . . . . . . . . . . 238
7.2.1. Profundidad y altura . . . . . . . . . . . . . . . . . . . 239
7.2.2. Recorrido en preorden . . . . . . . . . . . . . . . . . . 242
7.2.3. Recorrido en postorden . . . . . . . . . . . . . . . . . . 245

7.3. Arboles Binarios . . . . . . . . . . . . . . . . . . . . . . . . . 249
7.3.1. El ADT arbol binario . . . . . . . . . . . . . . . . . . . 251
7.3.2. Una interfaz arbol binario en Java . . . . . . . . . . . . 251
7.3.3. Propiedades del arbol binario . . . . . . . . . . . . . . 251
7.3.4. Una estructura enlazada para arboles binarios . . . . . 254
7.3.5. Representacion lista arreglo para el arbol binario . . . 260
7.3.6. Recorrido de arboles binarios . . . . . . . . . . . . . . 263
7.3.7. Plantilla metodo patron . . . . . . . . . . . . . . . . . 271
6 INDICE GENERAL
Captulo 1

Introducci
on

1.1. Clases, tipos, y objetos


Para la construccion de estructuras de datos y algoritmos se necesitan
dar instrucciones precisar a la computadora, lo cual se puede lograr usando
un lenguaje de computadora de alto nivel, como Java. Se presenta en este
captulo un repaso breve de los elementos usados en Java y en el siguiente
captulo se repasan los principios del dise
no orientado al objeto. No se da
una descripcion completa del lenguaje Java porque hay algunos aspectos
importantes que no son relevantes en el dise no de estructuras de datos.
Los programas en Java manipulan objetos. Los objetos guardan datos y
dan metodos para acceder y modificar sus datos. Cada objeto es una instancia
de una clase, la cual define el tipo del objeto, y las operaciones que realiza.
Los miembros importantes de una clase en Java son los siguientes:

Datos de objetos Java son guardados en variables de instancia, o


campos. Por lo tanto, si un objeto de alguna clase es para guardar
datos, entonces la clase debe especificar variables de instancia para
guardar. Las variables de instancia pueden ser tipos base, como enteros,
n
umeros punto flotante, o booleanos, o ser referencias a objeto de otras
clases.

Las operaciones que se pueden realizar con los datos son llamados
metodos. Estos consisten de constructores, procedimientos, y funciones.
Los cuales definen el comportamiento de los objetos de esa clase.
8 Introducci
on

Declaraci
on de una clase

Un objeto es una combinacion especfica de datos y metodos que pueden


procesar y comunicar esos datos. Las clases definen los tipos para los objetos:
por lo tanto, los objetos son en ocasiones referidos como instancias de su clase
definidora, porque toman el nombre de esa clase como su tipo.
Un ejemplo de la definicion de una clase Java se muestra en el listado 1.1
con la clase Contador.

1 public class Contador {


2 private int cuenta ; // una variable entera de instancia
3 /* * El constructor por defecto para un objeto Contador */
4 Contador () { cuenta = 0; }
5 /* * Un m e
todo accesor para obtener el valor actual */
6 public int getCuenta () { return cuenta ; }
7 /* * Un m e
todo modificador para incrementar la cuenta */
8 public void i n c r e m e n t aC u e n t a () { ++ cuenta ; }
9 /* * Un m e
todo modificador para decrementar la cuenta */
10 public void d e c r e m e n t a C u e n t a () { -- cuenta ; }
11 }

Listado 1.1: Clase Contador usada para llevar una cuenta simple, la cual puede ser
accesada, incrementada y decrementada

La definicion de la clase esta delimitada por llaves, se usa { para marcar


el inicio y } para marcar el final. Cualquier conjunto de sentencias entre
llaves definen un bloque de programa.
La clase Contador es una clase p ublica, por lo tanto cualquier otra clase
puede crear y usar un objeto Contador. La clase Contador tiene una variable
entera de instancia llamada cuenta. La variable es inicializada a cero en
el metodo constructor, Contador, el cual es llamado cuando se crea un
nuevo objeto Contador. La clase tiene un metodo accesor, getCuenta(),
el cual devuelve el valor actual del contador. Tambien tiene dos metodos
actualizadores, incrementaCuenta() y decrementaCuenta(). Esta clase no
tiene metodo main() y por lo tanto no hace nada por s misma.
El nombre de una clase, metodo, o variable en Java es llamado un identifi-
cador, el cual puede ser cualquier cadena de caracteres de tama no arbitrario,
debiendo iniciar con una letra seguida de letras, n umeros, y guiones bajos,
siendo letra y numero de cualquier lenguaje escrito definido en el conjunto de
caracteres Unicode. La excepciones a la regla anterior son los identificadores
Java del cuadro 1.1.
1.1 Clases, tipos, y objetos 9

abstract default goto package this


assert do if private throws
boolean double implements protected throws
break else import public transient
byte enum instanceof return true
case extends int short try
catch false interface static void
char final long strictfp volatile
class finally native super while
const float new switch
continue for null synchronized

Cuadro 1.1: Lista de palabras reservadas en Java, por lo que no pueden ser usadas
como nombres de variables o metodos.

Modificadores de clase
Los modificadores de clase son palabras reservadas opcionales que
preceden a la palabra reservada class. Los diferentes modificadores de clase
y su significado son los siguientes:
abstract indica que una clase tiene metodos abstractos. Los metodos
abstractos son declarados con la palabra reservada abstract y estan
vacos, es decir, no tienen un bloque definiendo un cuerpo de codigo
para el metodo. Una clase abstract usualmente tiene una combinacion
de metodos abstractos y metodos concretos. Lo anterior se aborda en la
seccion 2.4.
final indica una clase que no puede tener subclases o ser extendida.
public indica una clase que puede ser instanciada por cualquiera en
el mismo paquete, o por cualquiera que importe la clase. Las clases
p
ublicas son declaradas en su propio archivo separado teniendo el mismo
nombre de la clase y terminacion .java.
Cuando no se usa el modificador del clase public, la clase se considera
amigable. Esto significa que puede ser usada e instanciada por todas
las clases en el mismo paquete. Este es el modificador de clase por
defecto.
10 Introducci
on

1.1.1. Tipos base


Los tipos de los objetos estan determinados por la clase de la cual vienen.
Para propositos de eficiencia y simplicidad, Java tambien tiene los siguientes
tipos base, o tipos primitivos, los cuales no son objetos:
Tipo base Descripci on
boolean Valor booleano: true o false
char Caracter Unicode de 2 bytes
byte Entero con signo en complemento a dos de 1 byte
short Entero con signo en complemento a dos de 2 bytes
int Entero con signo en complemento a dos de 4 bytes
long Entero con signo en complemento a dos de 8 bytes
float N
umero de punto flotante 4 bytes (IEEE 754-1985)
double N
umero de punto flotante 8 bytes (IEEE 754-1985)
Alguna variable teniendo uno de estos tipos guarda un valor de ese tipo, en
vez de una referencia a algun objeto. Las constantes enteras, como 18 o -255,
son de tipo int, a menos que vayan seguidos inmediatamente por una L o l,
siendo en tal caso del tipo long. Constantes de punto flotante, como 3.1416 o
2.78e5, son de tipo double, a menos que vayan seguidos inmediatamente por
una F o f, siendo en tal caso del tipo float. Se muestra en el listado 1.2
una clase sencilla, Base, que define tipos base como variables locales para el
metodo main().

Comentarios
Los comentarios son anotaciones para los lectores humanos y no son
procesados por el compilador Java. Java permite dos tipos de comentarios,
comentarios de bloque y comentarios en lnea, los cuales definen el texto
ignorado por el compilador. Un comentario de bloque inicia con /* y se
cierra con */. Un comentario que inicia con /** es usado por el programa
javadoc, para generar la documentacion del software.
Se usa tambien // para iniciar comentarios en lnea y se ignora todo
hasta el final de la lnea.

1.1.2. Objetos
Un nuevo objeto es creado usando el operador new. Este operador crea
un objeto nuevo de una clase especificada y devuelve una referencia a ese
1.1 Clases, tipos, y objetos 11

1 public class Base {


2 public static void main ( String [] args ) {
3 boolean bandera = true ;
4 char c = z ;
5 byte b = -127;
6 short s = 32000;
7 int i = 12345;
8 long l = 6789 L ;
9 float f = 3.1416 f ;
10 double d = 2.1232 e8 ;
11 System . out . println ( " bandera = " + bandera ); // + operador concatenaci
on
12 System . out . println ( " c = " + c );
13 System . out . println ( " b = " + b );
14 System . out . println ( " s = " + s );
15 System . out . println ( " i = " + i );
16 System . out . println ( " l = " + l );
17 System . out . println ( " f = " + f );
18 System . out . println ( " d = " + d );
19 }
20 }

Listado 1.2: La clase Base muestra ejemplos de uso de tipos base.

objeto. Enseguida inmediatamente del operador new se pone una llamada al


constructor para ese tipo de objeto. Cualquier constructor que este incluido
en la definicion de la clase se puede usar. En las lneas 4 y 5 de la de la clase
Ejemplo, listado 1.3, se muestra el uso del operador new para crear objetos y
asignar la referencia a esos objetos a una variable.

1 public class Ejemplo {


2 public static void main ( String [] args ) {
3 Contador c ; // declaraci o
n de la variable c del tipo Contador
4 Contador d = new Contador (); // declaraci { on y creaci
on
5 c = new Contador (); // se crea un objeto Contador , devolviendo una ref .
6 d = c ; // la referencia de c se asigna a d
7 }
8 }

Listado 1.3: Ejemplos de uso del operador new.

Al usar el operador new con alg


un tipo de clase, se suceden los siguientes
tres eventos:
Un nuevo objeto es asignado dinamicamente en memoria, y todas sus
variables de instancia son inicializadas a valores por defecto. El valor
por defecto para las variables objeto es null y para todos los tipos base
es cero, excepto las variables booleanas, puestas a false.
12 Introducci
on

El constructor para el nuevo objeto es llamado con los parametros


indicados.

Despues que el constructor termina, el operador new devuelve una


referencia, o direccion de memoria, para el objeto nuevo creado. Si la
expresion esta en la forma de una sentencia de asignacion, entonces su
direccion se guarda en la variable objeto, as la variable objeto se refiere
al objeto nuevo creado.

Objetos n
umero
Cuando se requiere guardar n umeros como objetos, se pueden usar las
clases envoltura de Java. A estas se les conoce como clases n umero y hay
una clase n
umero por cada tipo base.
El cuadro 1.2 muestra los tipos base numericos y su correspondiente clase
numerica. Desde Java 5, una operacion de creacion es realizada automatica-
mente en cualquier momento que se pasa un n umero base a un metodo que
espera un objeto. De igual forma, el metodo accesor correspondiente es usado
automaticamente cada vez que se quiere asignar el valor de objeto Number a
un tipo base numerico.

Tipo Nombre Ejemplo Ejemplo


base de la clase de creacion de acceso
byte Byte n=new ((byte)1); n.byteValue()
short Short n=new ((short)1); n.shortValue()
int Integer n=new (1); n.intValue()
long Long n=new (1L); n.longValue()
float Float n=new Floate(3.9F); n.floatValue()
double Double n=new Double(3.9); n.doubleValue()

Cuadro 1.2: Clases numero de Java. Cada clase esta dada con su correspondiente
tipo base y ejemplos para crear y acceder tales objetos.

Objetos String
Una cadena es una secuencia de caracteres que provienen de alg un alfa-
beto, el conjunto de todos los posibles caracteres. Cada caracter c que forma
1.1 Clases, tipos, y objetos 13

una cadena s puede ser referenciado por su ndice en la cadena, que es la


cantidad de caracteres que estan antes de c en s, as el primer caracter tiene
ndice cero. El alfabeto usado para definir cadenas es el conjunto de caracteres
internacional Unicode, una codificacion de caracteres de 2 bytes que cubre la
mayora de los lenguajes escritos. Java define una clase especial incorporada
de objetos llamados objetos String. Las cadenas literales en Java debe ser
encerradas entre comillas dobles.

Concatenaci
on
El procesamiento de cadenas requiere manejar cadenas. La operacion
primaria para combinar cadenas es llamada concatenaci on, la cual toma
una cadena p y una cadena q y las combina en una nueva cadena, indicada por
p + q, la cual consiste de todos los caracteres de p seguidos de los caracteres
de q. En Java, la operacion de concatenacion funciona como se indica en la
descripcion. As es legal en Java escribir una sentencia de asignacion como

String s = "fut" + "bol";

La sentencia define una variable s que refiere un objeto de clase String,


y le asigna la cadena "futbol". En Java se asume que cada objeto tiene un
metodo incorporado toString() que devuelve una cadena asociada con el
objeto.

Referencias a objetos
Crear un nuevo objeto involucra el uso del operador new para asignar el
espacio de memoria del objeto y usar el constructor del objeto para inicializar
este espacio. La localidad, o direcci
on, de este espacio, generalmente, es luego
asignado a una variable referencia. Por lo tanto, una variable de referencia
puede ser vista como un apuntador a alg un objeto. Es como si la variable
fuera un contenedor para un control remoto que puede ser usado para manejar
el objeto nuevo creado. La variable tiene una forma de apuntar al objeto y
pedir que haga cosas o de acceso a sus datos.

El operador punto
Cada variable referencia objeto debera referirse a alg
un objeto, a menos
que esta sea null, en tal caso no apunta a nada.
14 Introducci
on

Puede haber varias referencias al mismo objeto, y cada referencia a un


objeto especfico puede ser usada para llamar metodos en ese objeto. Si se
usa una variable referencia objeto para cambiar el estado del objeto, entonces
su estado cambia para todas las referencias a este. Este comportamiento se
da por el hecho de que todas las referencias apuntan al mismo objeto.
Uno de los usos primarios de una variable referencia objeto es para acceder
los miembros de la clase para este objeto, una instancia de su clase. Esto es,
una variable referencia objeto es util para acceder los metodos y las variables
instancias asociadas con un objeto. Este acceso es realizado con el operador
punto (.). Se llama a un metodo asociado con un objeto usando el nombre
de la variable referencia, seguida por el operador punto y luego el nombre del
metodo y sus parametros.
Si hay varios metodos definidos con el mismo nombre para el objeto,
entonces el sistema de ejecucion Java usa el que empate en la cantidad de
parametros y este mas cercano a sus tipos respectivos. Un nombre de metodo
combinado con la cantidad y los tipos de sus parametros es llamado la firma
del metodo. Cuando se hace la llamada a cierto metodo se usa la firma para
determinar el metodo actual a usar. Considerar los siguientes ejemplos:

estufa.cocinaCena();
estufa.cocinaCena(comida);
estufa.cocinaCena(comida,temporada);

Cada una de las llamadas a metodos se refiere a un metodo diferente


con el mismo nombre definido en la clase a la cual pertenece estufa. Sin
embargo, la firma de un metodo en Java no incluye el tipo que regresa el
metodo, as Java no permite dos metodos con la misma firma para regresar
tipos diferentes.

Variables de instancia
Las clases de Java pueden definir variables de instancia, las cuales son
llamadas tambien campos. Estas variables representan los datos asociados
con los objetos de una clase. Las variables de instancia deberan tener un
tipo, el cual puede ser un tipo base, tal como int, long, double, o un tipo
referencia, esto es, una clase, tal como un String, una interfaz, o un arreglo.
Dada una variable referencia v, la cual apunta a un objeto o, se puede
acceder cualquier variable de instancia para o que las reglas de acceso lo
1.1 Clases, tipos, y objetos 15

permitan. Por ejemplo, las variables de instancia public son accesibles para
todos. Usando el operador punto se puede acceder y poner el valor de tales
variables de instancia. Por ejemplo, si estudiante se refiere a un objeto
Estudiante que tiene variables de instancia p ublicas llamadas nombre y
edad, entonces las siguientes sentencias son permitidas:

estudiante.nombre = "Enrique";
estudiante.edad = 18;

Una referencia objeto no tiene que ser solamente una variable de referencia.
Tambien puede ser cualquier expresion que devuelva una referencia objeto.

Modificadores de variables
Cuando se declara una variable de instancia se puede opcionalmente definir
un modificador de variable seguido por el tipo de la variable y el identificador
que sera usado para la variable. Adicionalmente, se puede opcionalmente
asignar un valor inicial a la variable con el operador de asignacion =. Las
reglas para un nombre de variable son las misma que para cualquier otro
identificador. El parametro del tipo de la variable puede ser un tipo base,
indicando que guarda valores de ese tipo, o un nombre de clase, indicando
que guarda una referencia a un objeto de esa clase. El valor inicial opcional
que se podra asignar a una variable de instancia debera empatar el tipo de la
variable. La clase Duende tiene varias definiciones de variables de instancias,
mostrado en el listado 1.4. Las variables edad, magico, y estatura son tipos
base, la variable nombre es una referencia a una instancia de la clase String,
y la variable amigoDuende es una referencia a un objeto de la clase que se
define. Los valores constantes asociados con una clase deberan ser siempre
declarados como static y final, como lo es la variable ESTATURA MAX.
El alcance, o visibilidad, de variables de instancia puede ser cambiado
como se muestra en la siguiente tabla:

Niveles de acceso
Modificador Clase Paquete Subclase Resto
public S S S S
protected S S S N
sin modificador S S N N
private S N N N
16 Introducci
on

1 public class Duende {


2 // variables de instancia
3 public String nombre ;
4 public int edad ;
5 public Duende amigoDuende ;
6 private boolean magico = false ;
7 protected double estatura = 85;
8 public static final int ESTATURA_MAX = 100;
9 // constructores
10 Duende ( String n , int e , Duende a , double est ) { // parametrizado compl
11 nombre = n ;
12 edad = e ;
13 amigoDuende = a ;
14 estatura = est ;
15 }
16 Duende () { // constructor por defecto
17 nombre = " Puck " ;
18 edad = 128;
19 amigoDuende = null ;
20 estatura = 95;
21 }
22 // m e todos
23 public static void hacerRey ( Duende d ) {
24 d . nombre = " Rey " + d . getNombreReal ();
25 d . magico = true ;
26 }
27 public void hacerRey () {
28 nombre = " Rey " + getNombreReal ();
29 magico = true ;
30 }
31 public boolean esMagico () { return magico ; }
32 public void setEstatura ( int nvaEst ) { estatura = nvaEst ; }
33 public String getNombre () { return " No te lo dir e!"; }
34 public String getNombreReal () { return nombre ; }
35 public void renombrar ( String s ) { nombre = s ; }
36 }

Listado 1.4: La clase Duende


1.2 M
etodos 17

Ademas de los modificadores de variable de alcance, tambien estan los


siguientes modificadores:

1. static. Se usa para declarar que una variable que esta asociada con la
clase, no con instancias individuales de esa clase. Las varibles static
son usadas para guardar informacion global de la clase y existen a
un si
no se han creado instancias de la clase.

2. final. Una variable de instancia final es una a la que se le debe asignar


un valor inicial, y entonces nunca puede se puede asignar un nuevo
valor. Si esta es un tipo base, entonces es una constante. Si una variable
objeto es final, entonces siempre se referira al mismo objeto (a un si el
objeto cambia su estado interno).

1.1.3. Tipo enum


Desde Java 5 se soportan tipos enumerados, llamados enum. Estos son
tipos a los cuales se les permite tomar solo valores dados de un conjunto
especificado de nombres. Estos pueden ser declarados dentro de una clase
como sigue:

[modificador] enum nombre { nombre valor0, nombre valor1, ...};

donde el modificador es opcional, o puede ser public, protected, o private.


El nombre es cualquier identificador legal de Java. Cada uno de los valores
identificadores es el nombre de un valor posible que variables de este tipo enum
puede tomar. Cada uno de estos nombre valor puede ser cualquier identificador
Java, pero por convencion estos deberan ser palabras en may usculas.
El tipo enum definido se puede usar para definir otras variables, como si
fuera el nombre de una clase. Los tipos enum tiene unos cuantos metodos
incorporados, como el metodo valueOf() que devuelve el valor enum que
corresponde con la cadena dada. Se muestra en la clase DiaCurso, listado 1.5,
el uso de un tipo enum.

1.2. M
etodos
Los metodos en Java son conceptualmente similares a funciones y procedi-
mientos en otro lenguajes de programacion de alto nivel. Son lneas de codigo
18 Introducci
on

1 public class DiaCurso {


2 public enum Dia { LUN , MAR , MIE , JUE , VIE , SAB , DOM };
3 public static void main ( String [] args ) {
4 Dia d = Dia . LUN ;
5 System . out . println ( " Inicialmente d es " + d );
6 d = Dia . VIE ;
7 System . out . println ( " Ahora es " + d );
8 Dia o = Dia . valueOf ( " VIE " );
9 System . out . println ( " Son d y t lo mismo ? " +( d == o ));
10 }
11 }

Listado 1.5: Un ejemplo del uso del tipo enum

que son llamadas para un objeto particular de alguna clase. Los metodos
pueden aceptar parametros como argumentos y entonces el comportamiento
dependera de estos y del objeto. Cada metodo es indicado en el cuerpo de
alguna clase. La definicion de un metodo tiene dos partes: la firma, la cual
define el nombre y los parametros del metodo, y el cuerpo, el cual define lo
que el metodo hace.
Un metodo permite al programador mandar un mensaje a un objeto. La
firma del metodo indica como un mensaje debera parecer y el cuerpo del
metodo indica que hara el objeto cuando reciba tal mensaje.

Declaraci
on de m
etodos
La sintaxis para definir un metodo es:

[modificadores] tipo nombre(tipo0 parametro0, tipo1 parametro1, . . . ){


// cuerpo del m etodo ...
}

Los modificadores incluyen los mismos tipos de los modificadores de alcance


usados para variables, como public, protected, y static, con significado
similar. El tipo de la declaracion define el tipo devuelto por el metodo. El
nombre es cualquier identificador Java valido. La lista de parametros y sus
tipos declaran variables locales que corresponden a valores que son pasados
como argumentos al metodo. Cada declaracion tipo puede ser cualquier tipo
Java y cada parametro es un identificador Java. La lista de parametros y
sus tipos puede estar vaca, por lo cual no se pasan valores al metodo al ser
llamado. Las variables parametro, al igual que las variables de instancia de la
1.2 M
etodos 19

clase, pueden ser usadas dentro del cuerpo del metodo. De igual forma, otros
metodos de esta clase pueden ser llamados desde el cuerpo de un metodo.
Cuando un metodo de una clase es llamado, se invoca con una instancia
particular de esa clase y puede cambiar el estado de ese objeto, excepto para
un metodo static, el cual esta asociado con la propia clase. Al invocar el
metodo renombrar() de la clase Duende, listado 1.4, cambia su nombre.

Modificadores de m
etodo
Al igual que las variables de instancia, los modificadores de metodo pueden
restringir el alcance de un metodo:

1. public. Cualquiera puede llamar al metodo.


2. protected. Solo metodos del mismo paquete o de sus subclases pueden
llamar al metodo.
3. private. Solo metodos de la misma clase pueden llamar.
4. Si ninguno de los anteriores modificadores es usado, el metodo es ami-
gable, por lo que puede ser llamado por objetos de clases en el mismo
paquete.

Los modificadores anteriores podran ser seguidos por los modificadores


adicionales:

1. abstract. Un metodo declarado como abstracto no tiene codigo. La


firma de tal metodo esta seguida por un punto y coma sin cuerpo del
metodo. Por ejemplo:

public abstract void setEstatura( double nvaEst );

Los metodos abstractos pueden aparecer solo en clases abstractas. Se


revisa su utilidad en la seccion 2.4.
2. final. El metodo no puede ser anulado por una subclase.
3. static. El metodo esta asociado con la propia clase, y no con alguna
instancia particular de la clase. Estos metodos pueden ser usados para
cambiar el estado de variables estaticas asociadas a la clase, siempre y
cuando estas no sean final.
20 Introducci
on

Tipos devueltos

Una definicion de metodo debera indicar el tipo de valor que el metodo


regresara. Si el metodo no regresa un valor, entonces se debe usar la palabra
reservada void. Si el tipo devuelto es void, el metodo es llamado un procedi-
miento; de otra forma, es llamado una funcion. Para regresar un valor en Java,
un metodo debera usar la palabra reservada return y una expresion, la cual
debe empatar con el tipo regresado por el metodo. En el metodo esMagico()
de la clase Duende, listado 1.4 se devuelve un valor usando return. Tan
pronto como return es ejecutado, el metodo termina.
Las funciones Java pueden devolver solo un valor. Para regresar valores
multiples se deberan combinar todos los valores que se quieran devolver en un
objeto compuesto, cuyas variables de instancia incluiran todos los valores
que se quieran devolver, y luego regresar una referencia al objeto compuesto.
Ademas, se puede cambiar el estado interno de un objeto que es pasado a un
metodo como otra forma de devolver resultados m ultiples.

Par
ametros

Los parametros de un metodo estan definidos en una lista separada por


comas y encerrada entre parentesis despues del nombre del metodo. Un
parametro consiste de dos partes, el tipo y el nombre. Si un metodo no tiene
parametros, entonces solo un par de parentesis vacos es usado.
Todos los parametros en Java son pasados por valor, es decir, cuando se
pasa un parametro a un metodo, una copia de ese parametro es hecha para
ser usada dentro del cuerpo del metodo. El metodo puede cambiar la copia
pero no el original. Si se pasa una referencia objeto como un parametro a un
metodo, entonces la referencia tambien es copiada. Cambiando la referencia
interna dentro de un metodo no cambiara la referencia que fue pasada. Por
ejemplo si se pasa una referencia Duende x a un metodo que llame a este
parametro d, entonces el metodo puede cambiar la referencia d para que
apunte a un objeto diferente, pero x seguira refiriendose al mismo objeto. Por
supuesto, el metodo puede usar la referencia d para cambiar el estado interno
del objeto, y esto tambien cambiara al objeto de x, ya que ambos apuntan al
mismo objeto. Esto u ltimo se hace en el metodo de clase hacerRey(Duende)
de la clase Duende, listado 1.4.
1.2 M
etodos 21

Constructores
Un constructor es un tipo especial de metodo usado para inicializar
objetos nuevos creados. Java tiene una forma particular para declarar el
constructor y una forma especial para llamar al constructor. La sintaxis para
declarar un constructor es:

[modificadores] nombre(tipo0 parametro0, tipo1 parametro1, . . . ){


// cuerpo del constructor ...
}

La sintaxis es casi la misma que para el metodo, excepto que el nombre del
constructor debera ser el mismo nombre de la clase que construye, y ademas no
se indica que tipo devuelve, ya que su tipo devuelto es implcitamente el mismo
que su nombre. Los modificadores del constructor siguen las mismas reglas
que los metodos, excepto que abstract, static y final no se permiten.
El constructor para una clase Camaron podra ser el siguiente:

public Camaron(int p, String n) {


peso = p;
nombre = n;
}

Definici
on del constructor e invocaci
on
El cuerpo de un constructor es como el cuerpo de un metodo, con el
siguiente par de excepciones menores. Una es el encadenado del constructor y
la otra es la prohibicion del uso de la sentencia return.
Los constructores son invocados en una forma unica: deberan ser llamados
con el operador new. Cuando se invoca una nueva instancia de la clase es
automaticamente creada y entonces su constructor es llamado para inicializar
sus variables de instancia y realizar otras tareas de configuracion. Por ejemplo
considerar la siguiente llamada al constructor y declaracion de la variable
miCamaron:

Camaron miCamaron = new Camaron(50,"Popeye");

Una clase puede tener varios constructores, pero cada uno debera tener
una firma diferente, es decir, cada uno debera distinguirse por el tipo y la
cantidad de parametros que toma.
22 Introducci
on

M
etodo main()
Algunas clases en Java son dise nadas para ser usadas por otras clases,
otras son programas independientes. Las clases que definen programas inde-
pendientes deberan contener un tipo especial de metodo, el metodo main().
Cuando se quiere ejecutar una programa Java independiente, se refiere el
nombre de la clase que define el programa precedido del siguiente comando:

java Acuario

En este caso, el sistema de ejecucion Java busca una version compilada de


la clase Acuario, y luego llama al metodo especial main en esa clase. Este
metodo se debe declarar como sigue:

public static void main(string[] args) {


// cuerpo del m
etodo main
}

Los argumentos pasados como los parametros args al metodo main() son
los argumentos lnea de comandos dados cuando el programa es ejecutado.
La variable args es un arreglo de objetos String, es decir, una coleccion
de cadenas indizadas, con la primera cadena siendo args[0], la segunda
args[1], etc.

Bloques de sentencias y variables locales


El cuerpo de un metodo es un bloque de sentencias, el cual es una
secuencia de sentencias y declaraciones entre las llaves { y }. Los cuerpos
de los metodos y otros bloques de sentencias pueden tener bloques anidados
dentro de ellos. Los bloques de sentencias pueden contener declaraciones
de variables locales. Estas variables son declaradas dentro del cuerpo de
sentencias, generalmente al inicio, pero dentro de las llaves. Las variables
locales son similares a variables de instancia, pero solo existen mientras el
bloque de sentencias esta siendo ejecutado. Tan pronto como el flujo del
programa sale del bloque, todas las variables locales dentro de este no pueden
ser referenciadas mas. Una variable local puede ser un tipo base, como int,
float, o double, o una referencia a una instancia de alguna clase. Las
sentencias y declaraciones son siempre terminadas con punto y coma.
Hay dos formas de declarar variables locales:
1.3 Expresiones 23

tipo nombre;
tipo nombre = valor inicial;

La primera declaracion solo define el identificador, nombre, para ser de


tipo especificado. La segunda define el identificador, su tipo, e inicializa la
variable al valor inidicado. Enseguida estan algunos ejemplos de declaraciones
de variables locales:

{
double r;
Punto p1 = new Punto(1, 2);
Punto p2 = new Punto(5, 6);
int i = 1024;
double e = 2.71828;
}

1.3. Expresiones
Las variables y constantes son usadas en expresiones para definir nuevos
valores y para modificar variables. Las expresiones involucran el uso de literales,
variables, y operadores.

1.3.1. Literales
Una literal es cualquier valor constante que puede ser usado en una
asignacion u otra expresion. Java permite los siguientes tipos de literales:

La referencia objeto null. Es la u nica literal objeto y se permite usar


con cualquier tipo de referencia.

Las constantes booleanas true y false.

Entero. El tipo por defecto para un entero como 176, o -12 es int, el
cual es un entero de 4 bytes. Una literal entera long debe terminar con
L o l, como 176L, o -12l, y define un entero de 8 bytes.

Punto flotante. El tipo por defecto para un punto flotante, como 3.1416
y -432.1 es double. Para indicar una litera como float, esta debe
terminar con F o f. Literales punto flotante en notacion cientfica
24 Introducci
on

tambien son permitidas, tal como 3.14E5 o -0.19e8. La E o e indica


por diez elevado a la potencia, del mismo modo que una calculadora.
Caracter. Las constantes caracter son tomadas del alfabeto Unicode.
Un caracter esta definido como un smbolo individual encerrado entre
comillas simples. Por ejemplo, a y ? son caracteres constantes.
Ademas Java define las constantes caracter especial siguientes:

\n nueva lnea \t tabulador


\b retroceso \r retorno
\f avance forma \\ diagonal invertida
\ comilla simple \ comilla doble

Cadena. Una cadena literal es una secuencia de caracteres encerrados


con comillas dobles. Por ejemplo, la siguiente es una literal de cadena:

"Monarcas permanecer
a en primera divisi
on"

1.3.2. Operadores
Las expresiones Java involucran componer literales y variables con opera-
dores.

Operador de asignaci
on
El operador de asignacion es =. Es usado para asignar un valor, prin-
cipalmente, a una variable de instancia o variable local. Su sintaxis es la
siguiente:
variable = expresion;
donde variable refiere a una variable que es permitida ser referenciada por el
bloque conteniendo la expresion. El valor de una operacion es el valor de la
expresion que fue asignada. Por ejemplo, si i y j son ambas declaradas del
tipo int, es correcto tener una sentencia de asignacion como:
i = j = 25;
La asignacion anterior funciona ya que los operadores de asignacion son
evaluados de derecha a izquierda.
1.3 Expresiones 25

Operadores aritm
eticos
Los siguientes son los operadores aritmeticos binarios en Java:
+ adicion
sustraccion
multiplicacion
/ division
% modulo
El operador modulo tambien es conocido como el operador residuo,
porque es el residuo sobrante despues de realizar la division entera.
Java tambien proporciona un menos unario (), el cual puede ser colocado
antes de la expresion aritmetica para invertir su signo. Los parentesis pueden
ser usados en cualquier expresion para definir el orden de evaluacion. Java usa
reglas de precedencia de operadores para determinar el orden de evaluacion
cuando los parentesis no son usados.
Java proporciona los operadores incremento en uno (++) y decremento
en uno (). Si los operadores son usados antes de la variable, entonces uno
es agregado, o sustraido de, la variable y su valor es leido en la expresion. Si
es usado despues de una variable, entonces el valor es primero leido y luego la
variable es incrementada o decrementada en uno. Por ejemplo, el fragmento
de codigo
int i = 20;
int j = i++;
int k = ++i;
int m = i--;
int n = 9 + i++;
asigna 20 a j, 22 a k, 22 a m, 30 a n, y deja i con 22.

Operadores l
ogicos
Java permite los operadores de comparacion estandar entre n
umeros:
< menor que
<= menor que o igual a
> mayor que
>= mayor que o igual a
== igual a
!= no igual a
26 Introducci
on

Los operadores == y != tambien pueden ser usados con referencias objetos.


El tipo del resultado de una comparacion es un boolean.
Los operadores que pueden operar con valores booleanos son los siguientes:
! negacion (prefijo)
&& condicional y
|| condicional o

Los operadores logicos && y || no evaluaran el segundo operador en


su expresion si no es necesario determinar el valor de la expresion. Esta
caracterstica es u
til para construir expresiones booleanas donde primero se
prueba que una cierta condicion se cumpla, como que una referencia no sea
nula, y luega se prueba una condicion que podra haber generado de otra
forma una condicion de error si la primera prueba no hubiera sucedido.

Operadores a nivel bit


Java proporciona los siguientes operadores a nivel bit (bitwise) para
enteros:
~ complemento (operador unario prefijo)
& y bitwise
| o bitwise
^ o-exclusivo bitwise
<< desplazamiento a la izquierda
>> desplazamiento a la derecha sin signo
>>> desplazamiento a la izquierda con signo

Operadores de asignaci
on
Para expresiones del tipo

variable = variable operador expresion;

Java proporciona operadores de asignacion que tienen efectos laterales de


operacion. Estos operadores son de la forma

variable = operador expresion;

excepto si la variable contiene una expresion, como un ndice arreglo, la


expresion es evaluada una sola vez. As, el fragmento de codigo
1.3 Expresiones 27

a[5] = 20;
i = 5;
a[i++] += 3;
deja a[5] con el valor 20 e i con valor 6.

Concatenaci
on de cadenas
Las cadenas pueden ser compuestas usando el operador de concatenaci
on
(+).

Precedencia de operadores
Los operadores en Java tienen precedencia, la cual sirve para determinar
el orden en el cual las operaciones son hechas. Los operadores son evaluados
de acuerdo a la siguiente tabla si lo parentesis no son usados para determinar
el orden de evaluacion. Los operadores en la misma lnea son evaluados
de izquierda a derecha, excepto para la asignacion y operaciones prefijas,
las cuales son evaluadas de derecha a izquierda, de acuerdo a la regla de
evaluacion condicional para las operaciones logicas Y y O. Sin parentesis, los
operadores de mayor precedencia son evaluados antes que los operadores de
menor precedencia.

Tipo Smbolos
Postfijo exp + + exp
Prefijo + + exp exp +exp exp exp !exp
Conversion (tipo)exp
Mult./div. / %
add./subs. +
desplazamiento << >> >>>
comparacion < <= > >= instanceof
igualdad == ! =
y bitwise &
xor bitwise
o bitwise |
y &&
o ||
condicional ? :
asignacion = += -= /= %= >>= <<= >>>= &= = | =
28 Introducci
on

1.3.3. Conversiones tipo base


La conversion es una operacion que permite cambiar el tipo de un valor.
Se puede tomar un valor de alg un tipo y convertirlo en un valor equivalente de
otro tipo. Las conversiones son utiles para hacer ciertas operaciones n
umericas
y de entrada/salida.
La sintaxis para conversion de una expresion a un tipo deseado es:
(tipo) exp
donde tipo indica a que tipo se quiere convertir la expresion que se tiene. Hay
dos tipos fundamentales de conversiones que pueden ser hechas. Se puede
convertir respecto a tipos base numerica o con respecto a objetos. Se aborda
en este captulo la conversion numerica y de cadenas, y la de objetos en la
seccion 2.5.1.

Conversi
on ordinaria
Cuando se convierte de un tipo double a un int, se podra perder precision,
siendo el valor double truncado. Pero se puede convertir un int a un double
sin problema. Por ejemplo, considerar lo siguiente:
double d1 = 1.2;
double d2 = 3.9999;
int i1 = (int )d1; // i1 tiene valor 1
int i2 = (int )d2; // i2 tiene valor 3
double d3 = (double )i2; // d3 tiene valor 3.0

Conversi
on con operadores
Ciertas operaciones binarias, como la division, daran resultados diferentes
dependiendo de los tipos de variables usadas. Se debe asegurarse que las
operaciones realizadas con los valores de los tipos pretendidos son las deseadas.
Por ejemplo, cuando se usan enteros, la division no considera la parte fraccional,
pero si cuando se usan n
umeros punto flotante, como se muestra en el siguiente
ejemplo:
int i1 = 4;
int i2 = 8;
dResultado = (double )i1 / (double )i2; // dResultado <-- 0.5
dResultado = i1 / i2; // dResultado <-- 0.0
1.3 Expresiones 29

Cuando i1 e i2 fueron convertidos a dobles, se realizo una division para


n
umeros reales. Cuando no fueron convertidos, se realizo una division entera
y as el resultado fue cero. Java no hace una conversion implcita para asignar
un valor int al resultado double.

Conversi
on implcita y autoempacado/desempacado
Hay casos donde Java realiza una conversi on implcita, de acuerdo al
tipo de la variable de asignacion, dado que no exista perdida de precision.
Por ejemplo:

int iResultado = 3;
double dResultado = 3.2;
dResultado = i/d; // dResultado <-- 0.9375. i convertido a double
iResultado = i/d; // perdida de precisi
on -> error de compilaci
on
iResultado = (int )i/d; // iResultado <-- 0, se pierde la fracci
on

Como Java no realiza conversiones implcitas donde se pierda precision, la


conversion explcita en la u
ltima lnea es requerida.
A partir de Java 5 hay un nuevo tipo de conversion explcita, para ir entre
objetos Number, como Integer y Float, a su tipo base relacionado, como
int y float. En cualquier momento que un objeto Number es esperado como
parametro para un metodo, el correspondiente tipo base puede ser pasado. En
este caso, Java realizara una conversion implcita, llamada autoempacado,
la cual convertira el tipo base a su correspondiente objeto Number. De igual
forma, cada vez que un tipo base es esperado en una expresion que involucra
una referencia Number, el objeto Number es cambiado al correspondiente tipo
base, en uno operacion llamada desempacado. Se muestra enseguida codigo
que usa las nuevas conversiones explcitas.

Integer a = 5; // autoempacado de un tipo base


int b = a + 3; // desempacado de un objeto Number

Una advertencia, del autoempacado y desempacado, es que si un referencia


Number es null, entonces intentando desempacar generara un error, llamado
NullPointerException. Otra advertencia, es con el operador ==, ya que es
usado para probar la igualdad de valores tipo base, as como si dos referencias
Number estan apuntando al mismo objeto. As que cuando se intenta probar
la igualdad, evitar las conversiones implcitas hechas por el autoempacada
30 Introducci
on

y desempacado. Por u ltimo, las conversiones implcitas, de cualquier tipo,


toman tiempo, por lo tanto se deben tratar de minimizar para no retardar la
ejecucion del programa.
En cualquier momento que una cadena es concatenada con cualquier
objeto o tipo base, este es automaticamente convertido a cadena. Conversion
explcita de un objeto o tipo base a una cadena no esta permitida. Por lo
tanto, las siguientes sentencias son incorrectas:

String s = (String) 4.5; // incorrecto


String t = "Valor = " + (String) 67; // incorrecto
String u = 22; // incorrecto

Para realizar una conversion a cadena, se debera usar el metodo toString()


apropiado o realizar una conversion implcita usando la operacion de concate-
nacion, como se muestra en las siguientes sentencias:

String s = "" + 4.5; // correcto, pero estilo pobre


String t = "Valor = " + 67; // correcto
String u = Integer.toString(22); // correcto

1.4. Control de flujo


El control de flujo en Java es parecido al de otros lenguajes de alto nivel.
Se revisa la estructura basica y la sintaxis del control de flujo en esta seccion,
incluyendo regreso del metodo, sentencia if, sentencia switch, ciclos, y formas
restringidas de saltos, sentencia break y continue.

1.4.1. Sentencias if y switch


Los condicionales dan una forma de evaluar una decision y luego ejecutar
una sentencia o mas bloques de sentencias diferentes de acuerdo a la salida
de la decision.

Sentencia if
La sintaxis de una sentencia if simple es:
1.4 Control de flujo 31

if (expresion booleana)
sentencia true;
else
sentencia false;

donde sentencia true y sentencia false tienen una sola sentencia, o bien, un
bloque de sentencias encerradas entre llaves. La parte else y su sentencia
asociada en una sentencia if es opcional. Se puede agrupar una cantidad de
pruebas booleanas, como sigue:

if (primera expresion booleana)


sentencia true;
else if (segunda expresion booleana)
segunda sentencia true;
else
sentencia false;

Si la primera expresion booleana es falsa, la segunda expresion boolena


sera probada, y as sucesivamente. Una sentencia if puede tener una cantidad
arbitraria de partes else if. Para estar seguro cuando se definan sentencias
if complicadas, usar llaves para encerrar todas las sentencias de los cuerpos.
Por ejemplo, la siguiente es una sentencia correcta if.

if (nivelAgua < 20) {


irAClase();
regresarACasa();
}
else if (nivelAgua < 50) {
salirEnLancha();
recogerTrampas();
}
else
quedarseEnCasa();

Sentencia switch
Java proporciona para control de flujo de valores m ultiples la sentencia
switch, la cual es u
til con tipos enum. El siguiente es un ejemplo la cual usa
una variable d del tipo enum Dia del listado 1.5 de la seccion 1.1.3.
32 Introducci
on

switch (d) {
case LUN:
System.out.println("Exito");
break ;
case MAR:
System.out.println(""Triunfo);
break ;
case MIE:
System.out.println("Fe");
break ;
case JUE:
System.out.println("Paz");
break ;
case VIE:
System.out.println("Amor");
break ;
default :
System.out.println("Salud");
}
La sentencia switch eval ua un entero o enum y hace que el control de
flujo salte a la localidad del codigo etiquetado con el valor de esta expresion.
Si no hay una etiqueta que empate, entonces del flujo del programa salta a la
localidad etiquetada como default. Este es el unico salto explcito hecho por
la sentencia switch, sin embargo, el control de flujo pasa a otros casos si el
codigo para cada caso no es terminado con una sentencia break, la cual hace
que el control de flujo pase a la siguiente sentencia despues de switch.

1.4.2. Ciclos
Otro mecanismo importante del control de flujo en un lenguaje de progra-
macion es la repeticion. Java tiene tres tipos de ciclos.

Ciclos while
El tipo de ciclo mas simple es el ciclo while. Este ciclo prueba que una
cierta condicion se cumpla y realizara el cuerpo del ciclo cada vez que la
condicion sea evaluada a true. La sintaxis para probar una condicion antes
que el cuerpo del ciclo sea ejecutado es:
1.4 Control de flujo 33

while (condicion)
sentencia ciclo;

Al inicio de cada iteracion, el ciclo prueba la expresion y luego ejecuta


el cuerpo del ciclo, solo si la expresion booleana eval ua a true. La sentencia
del cuerpo del ciclo tambien puede ser un bloque de sentencias y la condicion
debera ser una expresion booleana.
Considerar por ejemplo, un duende que intenta regar todas sus zanahorias
en su huerta, lo cual el hara mientras su regadera no este vaca. Como su
regadera podra estar vaca, se podra escribir el codigo que haga esta tarea
como:

public void regarZanahorias() {


Zanahoria actual = huerta.encontrarSigZanahoria();

while (!regadera.est a()) {


aVac
regar(actual,regadera);
actual = huerta.encontrarSigZanahoria():
}
}

Ciclos for
Otro tipo de ciclo es el ciclo for. Los ciclos for, en su forma mas simple,
proporciona repeticion de codigo para una determinada cantidad de veces,
pero se pueden hacer otras tareas tambien. La funcionalidad de un ciclo for
es flexible, gracias a que esta dividido en cuatro secciones: la inicializacion, la
condicion, el incremento, y el cuerpo.
La sintaxis para un ciclo for es:

for (inicializacion; condicion; incremento)


sentencia ciclo;

donde las secciones inicializacion, condicion, e incremento pueden estar vacas.


En la seccion de inicializaci
on, se puede declarar una variable ndice que
solo existe en el alcance del ciclo for. Por ejemplo, si se quiere un ciclo que
indice en un contador, y no se tiene necesidad de la variable contador fuera
del ciclo for, entonces declarando algo como lo siguiente
34 Introducci
on

for (int contador=0; condici


on; incremento)
sentencia ciclo;

declarara una variable contador cuyo alcance es solo el cuerpo del ciclo.
En la seccion condicion, se indica la condicion para repetir el ciclo. Esta
debera ser una expresion booleana. El cuerpo del ciclo for sera ejecutado
cada vez que la condici on sea true cuando sea evaluado al inicio de una
iteracion posible. Tan pronto como la condicion eval ue a false, entonces el
cuerpo del ciclo ya no es ejecutado, y la ejecucion del programa contin ua con
la siguiente sentencia despues del ciclo for.
En la seccion incremento, se declara la sentencia de incremento para el
ciclo. Esta debera ser cualquier sentencia legal permitida para flexibilidad en
la codificacion. La sintaxis de un ciclo for es equivalente a:

inicializaci
on;
while (condicion) {
sentencia ciclo;
incremento;
}

excepto que en Java, un ciclo while no puede tener una condicion booleana
vaca, mientras en un ciclo for esta permitido. El siguiente ejemplo muestra
un ciclo for

public void comerManzanas(Manzanas manzanas) {


int cantManzanas = manzanas.getCantidad();
for (int x=0; x<cantManzanas; ++x) {
comerManzana(manzanas.getManzana(x));
tirarCoraz
on();
}
}

En el ejemplo anterior, la variable de ciclo x fue declarada en int x =


0. Antes de cada iteracion, el ciclo prueba la condicion x<cantManzanas y
ejecuta el cuerpo del ciclo solo si esto es verdadero. Al final de cada iteracion
el ciclo usa la sentencia ++x para incrementar la variable de ciclo antes de
probar nuevamente la condicion.
1.4 Control de flujo 35

Ciclos do...while

El ciclo do...while prueba una condicion despues del cuerpo del ciclo,
a diferencia de los ciclos anteriores, los cuales prueban una condicion antes
de realizar una iteracion en el cuerpo del ciclo. La sintaxis para el ciclo
do...while es:

do
sentencia ciclo;
while(expr booleana);

La sentencia del cuerpo del ciclo puede ser una sola sentencia o un bloque
de sentencias, y la condicion debe ser una expresion booleana. En este ciclo
se repite el cuerpo del ciclo tanto como la condicion sea verdadera cada vez
que es evaluada.
Por ejemplo, considerar que se quiere pedir al usuario una entrada y luego
hacer algo util con esa entrada. Una condicion posible, en este caso, para salir
del ciclo es cuando el usuario ingresa una cadena vaca. Sin embargo, a un en
este caso, se podra querer manejar la entrada e informar al usuario que se
quiere salir. El siguiente ejemplo ilustra ese caso:

public void getEntradaUsuario() {


String entrada;
do {
entrada = getEntradaString();
manejarEntrada(entrada);
} while (entrada.length()>0);
}

Observar en el ejemplo anterior que la condicion de salida es cuando la


condicion es no verdadera.

1.4.3. Sentencias explcitas de control de flujo


Java tambien proporciona sentencias que permiten explcitamente el cam-
bio en el flujo de un programa.
36 Introducci
on

Regresar de un m
etodo
Si un metodo esta declarado con un tipo de retorno void, entonces el flujo
regresa cuando alcanza la u ltima lnea de codigo en el metodo, o cuando se
encuentra una sentencia return sin argumento. Sin embargo, si un metodo
esta declarado con un tipo de retorno, el metodo es una funcion y este
debera salir regresando el valor de la funcion como un argumento a una
sentencia return. El siguiente ejemplo correcto ilustra el regreso de una
funcion:

// Revisar por un cumplea~


nos particular
public boolean revisaCumple(int fecha) {
if (fecha == Cumples.CUMPLE JUAN) {
return true ;
}
return false ;
}

Se sigue que la sentencia return debera ser la ultima sentencia ejecutada


en una funcion, ya que el resto del codigo nunca sera ejecutado.
Observar que hay una diferencia significativa entre una sentencia que es
la ultima lnea de codigo ejecutada en un metodo y la ultima lnea de codigo
en el metodo. En el ejemplo anterior, la lnea return true; no es la u ltima
lnea de codigo escrita en la funcion, pero podra ser la u
ltima lnea que es
ejecutada cuando la condicion involucrando a fecha sea true. Tal sentencia
explcitamente interrumpe el flujo en el metodo.

Sentencia break
El uso tpico de la sentencia break tiene la siguiente sintaxis simple:

break;

Se usa break para salir del cuerpo de la sentencia switch, for, while,
o do...while mas interna. Cuando es ejecutado break, el flujo pasa a la
siguiente sentencia despues del cuerpo del ciclo o switch que contiene el
break.
La sentencia break tambien puede ser usada en una forma etiquetada
para salir del ciclo mas externo o sentencia switch. La sintaxis es:
1.5 Arreglos 37

break etiqueta;

donde etiqueta es un identificador Java que es usado para etiquetar un ciclo


o sentencia switch. Tal etiqueta puede aparecer una sola vez al inicio de la
declaracion de un ciclo.
Se muestra el uso de una etiqueta con una sentencia break en el siguiente
ejemplo:

public static boolean tieneEntradaCero( int [][] a) {


boolean banderaEnc = false ;

busquedaCero:
for (int i=0; i<a.length; ++i)
for (int j=0; j<a[i].length; ++j)
if (a[i][j] == 0) {
banderaEnc = true ;
break busquedaCero;
}
return banderaEnc;
}

Sentencia continue
La sentencia continue tambien cambia explcitamente el flujo en un
programa, la cual tiene la siguiente sintaxis:

continue [etiqueta];

donde etiqueta es un identificador opcional Java que es usado para etiquetar


un ciclo. Esta sentencia solo puede ser usada adentro de ciclos. La sentencia
continue causa que la ejecucion salte sobre los pasos restantes del cuerpo del
ciclo en la iteracion actual, pero el ciclo contin
ua si su condicion es satisfecha.

1.5. Arreglos
Una tarea de programacion com un es manejar un grupo numerado de
objetos relacionados. Por ejemplo, se podra querer que un vdeo juego maneje
las diez puntuaciones mejores del juego. En vez de usar diez variables diferentes
38 Introducci
on

para esa tarea, se podra preferir usar un solo nombre para el grupo y usar
un ndice numerico para referirse a cada unna de las puntucaciones. De igual
forma, se podra querer que un sistema de informacion medico maneje los
pacientes asignados a camas en un cierto hospital. De nueva cuenta no se
tienen que introducir 200 variables solo por que el hospital tenga 200 camas.
En situaciones como las anteriores, se puede ahorrar esfuerzo de programa-
cion usando un arreglo, el cual es una coleccion numerada de variables todas
ellas con el mismo nombre. Cada variable, o celda, en un arreglo tiene un
ndice, el cual se refiere de forma u nica al valor guardado en esa celda. Las
celdas de un arreglo a estan numeradas por 0, 1, 2, etc. Se muestra enseguida
un arreglo de las diez puntuaciones mas altas para un vdeo juego.

Puntuaciones mejores
987 876 765 654 543 432 321 210 109 98
0 0 2 3 4 5 6 7 8 9
ndices

Esta organizacion es u
til, ya que permite hacer calculos interesantes. En
el siguiente ejemplo, listado 1.6, el metodo, de la clase DemoArreglos suma
todos los numeros en un arreglo de enteros:

1 /* * Agrega todos los n u meros en un arreglo de enteros */


2 public static int suma ( int [] a ) {
3 int total = 0;
4 for ( int i =0; i < a . length ; ++ i )
5 total += a [ i ];
6 return total
7 }

Listado 1.6: Metodo para sumar todos los elementos de un arreglo de enteros

En este ejemplo se usa una caracterstica importante de Java para los


arreglos, el tama no de celdas que un arreglo guarda, es decir, su tama no
(length). En Java, un arreglo es un tipo especial de objeto donde el tama no
del arreglo es guardado en una variable de instancia, length, la cual se accede
de la siguiente forma:

nombre arreglo.length

Las celdas de un arreglo a estan numeradas como 0, 1, 2, y as hasta


a.length-1.
1.5 Arreglos 39

Elementos y capacidades del arreglo


Cada objeto guardado en un arreglo es llamado un elemento del arreglo.
Elemento numero 0 es a[0], elemento n umero 1 es a[1], y as sucesivamente.
Como el tama no de un arreglo determina la cantidad maxima de cosas que
pueden ser guardadas en el arreglo, en ocasiones se refiere el tama no de un
arreglo como su capacidad. Enseguida se muestra otro uso simple de un
arreglo en el listado 1.7, clase DemoArreglos, el cual cuenta la cantidad de
veces que cierto numero aparece en un arreglo:

1 /* * Cuenta la cantidad de veces que un entero est a en el arreglo . */


2 public static int e nc u en tr aC u en ta ( int [] a , int k ) {
3 int cuenta = 0;
4 for ( int i =0; i < a . length ; ++ i )
5 if ( a [ i ]== k ) // revisar si el elemento actual es igual a k
6 ++ cuenta ;
7 return cuenta ;
8 }

Listado 1.7: Metodo para contar las veces que se encuentra k en el arreglo.

Error fuera de los lmites


Es un descuido serio intentar indizar un arreglo a con un n
umero fuera del
rango de 0 a a.length-1. Se dice que tal referencia esta fuera de los lmites.
Las referencias fuera de los lmites han sido usadas numerosas ocasiones por
los hackers usando un metodo llamado el ataque de desbordamiento del b ufer
para comprometer la seguridad del sistema de computo, escrito en lenguajes
que no son Java. Como medida de seguridad, los ndices de los arreglos son
siempre revisados en Java para que no esten fuera de los lmites. En el caso
de que un ndice se encuentre fuera de los lmites, el ambiente de ejecucion
Java indica una condicion de error, indicada por la clase ArrayIndexOutOf-
BoundsException. Mediante esta revision Java evita problemas de seguridad
donde otros lenguajes deben hacer frente.
Se pueden evitar errores fuera de los lmites asegurandose que siempre
se indiza en un arreglo, a, usando un valor entero entre cero y a.length.
Una forma breve de lograrlo es usando cuidadosamente la caracterstica de
terminacion temprana de los operadores logicos de Java. Por ejemplo, una
sentencia como la siguiente nunca generara un error fuera de los lmites:
40 Introducci
on

if ((i>=0) && (i<a.length) && (a[i]>2) )


x = a[i];

para la comparacion a[i]>2 solo sera hecha si las primeras dos comparaciones
son exitosas.

1.5.1. Declaraci
on de arreglos
Una forma para declarar e inicializar un arreglo es la siguiente:

tipo elemento[] nombre arreglo = {val 0, val 1, ...};

El tipo elemento puede ser cualquier tipo base Java o nombre de clase, y
nombre arreglo es un identificador Java valido. Los valores iniciales deberan
ser del mismo tipo que el del arreglo. Por ejemplo, la siguiente declaracion de
un arreglo que es inicializado con los primeros diez numeros primos:

int [] primos = {2,3,5,7,11,13,17,19,23,29};

Para declarar una variable arreglo sin inicializarlo, se hace como sigue:

tipo elemento[] nombre arreglo;

Una vez que se ha declarado un arreglo de la forma anterior, se puede


crear la coleccion de celdas para el arreglo usando la siguiente sintaxis:

new tipo elemento[tamano]

donde tama no es un entero positivo indicando el tama no del arreglo creado.


Usualmente esta expresion aparece en una sentencia de asignacion con el
nombre del arreglo en lado izquierdo del operador de asignacion. Los arreglos
creados son inicializados con ceros si el tipo del arreglo es un tipo base
numerico. Los arreglos de objetos son inicializados a referencias null. En el
siguiente codigo se define una variable de arreglo llamada a, y despues se le
asigna un arreglo de diez celdas, cada una de tipo double, siendo despues los
elementos inicializados a 1.0:

double [] a;
a = new double [10];
for (int k=0; k<a.length; ++k)
a[k] = 1.0;
1.5 Arreglos 41

1.5.2. Arreglos como objetos


Los arreglos en Java son un tipo especial de objeto. Por esta razon se
puede usar el operador new para crear una nueva instancia de un arreglo. El
arreglo puede ser usado como cualquier otro objeto general, pero se tiene
una sintaxis especial, usando los corchetes, para referirse a sus miembros.
Como un arreglo sea un objeto, el nombre de un arreglo es una referencia
a la direccion en memoria donde esta el arreglo guardado, de igual manera,
no hay nada especial en usar el operador punto y la variable de instancia,
length, para referirse al tama
no del arreglo.
Como los arreglos son objetos, entonces cuando se tiene una sentencia
como la siguiente, donde a y b son referencias a arreglos del mismo tipo

b = a;

significa que a y b se refieren al mismo objeto. As que cuando se tenga una


sentencia como

b[3] = 5;

entonces tambien se tiene que a[3] guarda un 5.

Clonaci
on de un arreglo
Cuando se quiere crear una copia exacta del arreglo, a, y asignar ese
arreglo al arreglo variable, b, se podra escribir

b = a.clone();

el cual copia el contenido de todas las celdas de a en un arreglo nuevo y lo


asigna b para que apunte a este. El metodo clone() es un metodo incorporado
de cada objeto Java. Si antes de la asignacion b[3] = 5; se hubiera clonado
el arreglo a en b, entonces los elementos en a y b para el ndice 3 podran ser
diferentes, ya que cada referencia apunta a su propio arreglo.
Se debe observar que las celdas de un arreglo son copiadas cuando este es
clonado. Si las celdas son un tipo base, como int, sus valores son copiados.
Pero si las celdas son referencias objetos, entonces estas referencias son
copiadas. Esto significa que hay dos formas para referirse a un objeto.
42 Introducci
on

1.6. Entrada y salida


Java tiene un conjunto de clases y metodos para la entrada y salida en
un programa. Hay clases para el dise no de interfaces graficas de usuario, con
ventanas pop-up (emergentes) y men us, al igual que metodos para mostrar
e introducir texto y n umeros. Tambien hay metodos para manejar objetos
graficos, imagenes, sonidos, paginas web, y eventos de raton. Varios de estos
metodo de entrada y salida pueden ser usados en programas autonomos o en
applets.
La entrada y salida simple se da con la ventana consola Java. Dependiendo
del ambiente Java donde se este usando, la ventana es una ventana emergente
especial que puede ser usada para mostrar e introducir texto, o una ventana
usada para introducir comandos al sistema operativo, estas ventanas son
referidas como ventanas shell, DOS o terminal.

M
etodos de salida simple
Java proporciona un objeto estatico incorporado, llamado System.out,
que realiza salida al dispositivo de la salida estandar, siendo este la ventana
consola Java. El objeto System.out es una instancia de la clase java.io.-
PrintStream. Esta clase define metodos para un flujo de salida de memoria,
donde los caracteres son puestos en una localidad temporal, llamada buffer,
el cual despues es vaciado cuando la ventana consola esta lista para imprimir
los caracteres.
En particular, la clase java.io.PrintStream tiene los siguientes metodos
para realizar salida simple (se usa tipo base para referirse a cualquiera de los
tipos base posibles):
print(Object o) Imprime el objeto o usando su metodo
toString().
print(String s) Imprime la cadena s.
print(tipo base t) Imprime el valor del tipo base t.
println(String s) Imprime la cadena s, seguida del caracter
nueva lnea.
Cuando el siguiente fragmento de codigo:

System.out.print("Valores Java: ");


System.out.print(3.1416);
System.out.print(,);
1.6 Entrada y salida 43

System.out.print(15);
System.out.print(" (double,char,int).");
es ejecutado, mostrara lo siguiente en la ventana consola Java:
Valores Java: 3.1416,15 (double,char,int).

Entrada simple con la clase java.util.Scanner


Para realizar entrada desde la ventana consola Java se emplea el objeto
especial System.in. La entrda viene del dispositivo de entrada estandar,
el cual por defecto es el teclado de la computadora replicando sus teclas
presionadas en la consola. El objeto System.in esta asociado con el dispositivo
de entrada estandar. Una forma simple de leer la entrada de este objeto es
usandolo para crear un objeto Scanner de la siguiente forma:
new Scanner(System.in)
La clase Scanner tiene un conjunto de metodos adecuados que leen desde
el flujo de entrada dado. En la clase EjemploEntrada, listado 1.8, se usa un
objeto Scanner para procesar la entrada:

1 import java . io . IOException ;


2 import java . util . Scanner ;
3 public class EjemploE ntrada {
4 public static void main ( String [] args ) throws IOException {
5 Scanner s = new Scanner ( System . in );
6 System . out . print ( " Ingresa tu edad en a ~
n os : " );
7 double edad = s . nextDouble ();
8 System . out . print ( " Ingresa la frecuencia card aca m a
xima : " );
9 double frecuencia = s . nextDouble ();
10 double qg = ( frecuencia - edad )*0.65;
11 System . out . println ( " Su meta de
ndice de quema de grasa es : " +
12 qg + " . " );
13 }
14 }

Listado 1.8: La clase EjemploEntrada.

Cuando se ejecuta, la aplicacion podra generar lo siguiente en la consola


Java:
Ingresa tu edad en a~
nos: 48
Ingresa la frecuencia card
aca m
axima: 170
Su meta de
ndice de quema de grasa es: 79.3.
44 Introducci
on

M
etodos de la clase java.util.Scanner

La clase Scanner lee el flujo de entrada y lo divide en tokens, los cuales


son cadenas de caracteres separados por delimitadores. Un delimitador es una
cadena de separacion especial, y el delimitador por defecto es el espacio blanco.
De esta forma, los tokens son separados por cadenas de espacios, tabuladores,
y nuevas lneas, por defecto. Los tokens pueden ser ledos inmediatamente
como cadenas o un objeto Scanner puede convertir un token a un tipo base,
siempre que token este con la sintaxis correcta. La clase Scanner incluye los
siguientes metodos para trabajar con tokens:

hasNext() Devuelve true si y solo si hay otro token


en el flujo de entrada.
next() Devuelve la siguiente cadena token en un
flujo de entrada; se genera un error si ya
no quedan mas tokens.
hasNextTipo () Devuelve true si y solo si hay otro token
en el flujo de entrada y este puede ser inter-
pretado como el Tipo base correspondiente,
siendo tipo Boolean, Byte, Double, Float,
Int, Long, Short.
nextTipo () Devuelve el siguiente token en el flujo de
entrada, devuelto como el correspondiente
Tipo base; se genera un error si ya no que-
dan mas tokens o si el siguiente token no
puede ser interpretado como un tipo base
correspondiente a Tipo.

Adicionalmente, los objetos Scanner pueden procesar la entrada lnea por


lnea, ignorando los delimitadores, y tambien buscar patrones dentro de lneas.
Los metodos para procesar la entrada de esta forma incluyen los siguientes:
1.7 Clases anidadas y paquetes 45

hasNextLine() Devuelve true si y solo si el flujo de entrada


tiene otra lnea de texto.
nextLine() Avanza la entrada pasando al final de la
lnea actual y regresando la entrada que fue
saltada.
findInLine(String s) Intenta encontrar una cadena que empate
el patron s en la lnea actual. Si el patron es
encontrado, este se devuelve y se avanza al
primer caracter despues del apareamiento.
Si el patron no es encontrado, se devuelve
null y no se avanza.
Algunos de los metodos mencionados se podran usar como se muestra
enseguida para lograr que el usuario ingrese un entero:

Scanner entrada = new Scanner(System.in);


System.out.print("Favor de ingresar un entero: ");
while (!entrada.hasNextInt()) {
input.nextLine();
System.out.print("No se ingreso un entero. ");
System.out.print("Por favor ingrese un entero: ");
}
int i = entrada.nextInt();

1.7. Clases anidadas y paquetes


Java tiene una forma u til y general para la organizacion de clases en
programas. Cada clase p ublica autonoma definida en Java debera estar en
archivo separado. El nombre del archivo es el nombre de la clase con la
extension .java. Se describe enseguida dos maneras que Java tiene para
organizar m ultiples clases en forma util.

Clases anidadas
Java permite que definiciones de clases sean anidadas, o puestas, dentro
de las definiciones de otras clases. El principal uso para tales clases anidadas
es para definir una clase que esta fuertemente afiliada con otra clase. Por
ejemplo, definir la clase cursor como clase anidada en la definicion de la clase
editor de texto mantiene a estas dos clases altamente relacionadas en el mismo
46 Introducci
on

archivo. Mas aun, permite que cada una de ellas acceda a los metodos no
publicos de la otra. Un punto tecnico respecto a las clases anidadas es que
la clase anidada debe ser declarada como static. Esta declaracion implica
que la clase anidada esta asociada con la clase externa, no una instancia de
la clase externa, que es, un objeto especfico.

Paquetes
Un conjunto de clases, todas definidas en un subdirectorio com
un, pueden
ser un paquete Java. Cada archivo en un paquete inician con la lnea:

package nombre paquete;

El subdirectorio conteniendo el paquete debera ser nombrado igual que el


paquete. Se puede tambien definir un paquete en solo archivo que contenga
varias definiciones de clases, pero cuando este es compilado, todas las clases
seran compiladas en archivos separados en el mismo subdirectorio.
En Java, se pueden usar clases que estan definidos en otros paquetes
prefijando los nombres de las clases con puntos que correspondan a las
estructuras de directorios de otros paquetes.

public boolean temperatura(


TA.Medidas.Termometro termometro, int temp) {
//...
}

En la firma del metodo temperatura se usa una clase Termometro como


parametro. Termometro esta definido en el paquete TA en un subpaquete
llamado Medidas. Los puntos en TA.Medidas.Termometro corresponden di-
rectamente a la estructura de directorios en el paquete TA.
Para evitar todo el tecleado extra para referirse a una clase foranea del
paquete actual, en Java se puede usar la palabra reservada import para
incluir clases externas o paquetes enteros en el archivo actual. Para importar
clases individuales de un paquete particular, se pone lo siguiente al inicio del
archivo:

import nombrePaquete.nombreClase;

Por ejemplo, se podra teclear


1.7 Clases anidadas y paquetes 47

package Proyecto;
import TA.Medidas.Termometro;
import TA.Medidas.Escala;

al inicio de una clase del paquete Proyecto, para indicar que se estan impor-
tando las clases llamadas TA.Medidas.Termometro y TA.Medidas.Escala.
Tambien se puede importar un paquete entero, con la siguiente sintaxis:

import nombrePaquete.*;

Cualquiera de las dos formas de importacion usadas permiten codigos mas


breves, as el metodo Temperatura() se podra reescribir como:

import TA.Medidas.*;
public boolean temperatura(Termometro termometro, int temp) {
//...
}

En resumen, la estructura de un programa Java, permite tener variables


de instancia y metodos dentro de una clase, y clases dentro de un paquete.
48 Introducci
on
Captulo 2

Dise
no orientado al objeto

2.1. Metas, principios, y patrones


Los actores principales en el paradigma del diseno orientado al objetos
son llamados objetos. Un objeto viene de una clase, la cual es una especifica-
cion de los campos de datos, tambien llamados variables de instancia, que el
objeto contiene, al igual los metodos u operaciones que el objeto puede ejecu-
tar. Cada clase presenta al mundo exterior una vista concisa y consistente de
los objetos que son instancias de esta clase, sin requerir ir en muchos detalles
innecesarios o dando otros accesos a las tareas internas de los objetos. Esta
vista computacional intenta llenar varias metas e incorporar varios principios
de diseno.

2.1.1. Metas del dise


no orientado a objetos
Las implementaciones de software deben lograr robustez, adaptabilidad, y
reutilizacion.

Robustez
Como buen programador se quiere desarrollar software que sea correcto,
es decir, que un programa de la salida correcta para todas las entradas
anticipadas. Ademas, se quiere que el software sea robusto, esto es, capaz de
manejar entradas no esperadas las cuales no estan explcitamente definidas
para la aplicacion. Por ejemplo, si un programa esta esperando un punto
flotante positivo, ya que representa el precio de un artculo, y en vez de esto se
50 Dise
no orientado al objeto

da un valor negativo, entonces el programa debera poderse recuperar de este


error. En aplicaciones de seguridad crtica, donde un error de software puede
llevar a una lesion o la perdida de la vida, el software que no es robusto podra
ser mortal. Un caso de este tipo se presento entre 1985 y 1987 en accidentes
que involucraron la therac-25, una maquina de radioterapia, donde al menos
seis pacientes recibieron sobredosis de radiacion, y tres de estos murieron como
consecuencia directa. Una comision investigadora concluyo que las causas
obedecieron a malas practicas de desarrollo, analisis de requerimientos y mal
diseno de software, no tanto por errores aislados en el codigo fuente.

Adaptabilidad
Las aplicaciones actuales de software, como los navegadores Web y los
motores de b usqueda de Internet, generalmente involucran programas grandes
que son usados por muchos a nos. Por lo tanto el software necesita ser capaz de
evolucionar en el tiempo en respuesta a condiciones cambiantes en su ambiente.
As, otra meta importante de calidad del software es la adaptabilidad o
evolucionabilidad. Relacionado a este concepto es la portabilidad, la cual
es la habilidad del software para ejecutarse con cambios mnimos en hardware
y plataformas de sistemas operativos diferentes. El software escrito en Java
es portable.

Reutilizaci
on
Otra capacidad deseable del software es la reutilizacion, es decir, el mismo
codigo debera ser utilizable como un componente de diferentes sistemas
para varias aplicaciones. El desarrollo de software de calidad puede ser una
actividad costosa, y su costo se puede recuperar si el software es dise nado de
manera que sea facilmente reutilizado en aplicaciones futuras. La reutilizacion
debera hacerse con cuidado, sin embargo, una de las mayores fuentes de errores
de software en la therac-25 se debio por el reuso inadecuado del software de
la therac-20, la cual no estaba orientada al objeto y no estaba dise nada para
la plataforma de hardware de la therac-25.

2.1.2. Principios del dise


no orientado al objeto
Para lograr las metas indicadas previamente se puede hacer con los princi-
pios de la aproximacion orientada al objeto, los cuales son:
2.1 Metas, principios, y patrones 51

Abstraccion

Encapsulacion

Modularidad

Abstracci
on

La nocion de abstracci on es extraer de un sistema complejo sus partes


mas fundamentales y describirlas de una forma precisa y simple. Describir
las partes de un sistema involucra nombrarlas y explicar su funcionalidad.
Aplicando el paradigma de abstraccion al dise no de estructuras de datos da
lugar a tipos de datos abstractos o ADT (abstract data type). Un ADT
es un modelo matematico de una estructura de datos que especifica el tipo
de dato guardado, las operaciones soportadas, y los tipos de parametros de
las operaciones. Un ADT especifica que puede cada operacion realizar, pero
no como se hace. En Java, un ADT puede ser expresado por una interfaz, la
cual es una lista de declaraciones de metodos, donde cada metodo tiene un
cuerpo vaco. En la seccion ?? se abordan las interfaces.
Un ADT es realizado por una estructura de datos concreta, la cual esta mo-
delada en Java por una clase. Una clase define los datos que son guardados y
las operaciones soportadas por los objetos que son instancias de la clase. Di-
ferente a las interfaces, las clases indican como las operaciones son realizadas
en el cuerpo de cada metodo. Implementar una interfaz, por una clase,
es cuando la clase incluye todos los metodos declarados en la interfaz, dando
un cuerpo para estos. Sin embargo, una clase puede tener mas metodos que
los de la interfaz.

Encapsulaci
on

El concepto de encapsulaci on es otro principio importante del dise no


orientado al objeto, el cual establece que los diferentes componentes de un
sistema de software no deberan mostrar los detalles internos de sus respectivas
implementaciones. Una de las ventajas principales de la encapsulacion es que
le da la libertad al programador en implementar los detalles de un sistema.
La u nica restriccion del programador es que mantenga la interfaz abstracta
que los de afuera ven.
52 Dise
no orientado al objeto

Modularidad

Otro principio fundamental del dise no orientado al objeto es la modu-


laridad. Los sistemas actuales de software generalmente consisten de varios
componentes diferentes que deben interactuar correctamente en orden para
que el sistema completo trabaje correctamente. Mantener estas interacciones
en orden requiere que estos componentes diferentes esten bien organizados.
En el diseno orientado al objeto, este enfoque de estructuracion del codigo
se logra con el concepto de modularidad. La modularidad se refiere a un
principio de organizacion para el codigo, en el cual diferentes componentes de
un sistema de software son divididos en unidades funcionales separadas.

Organizaci
on jer
arquica

La estructura impuesta por la modularidad ayuda a permitir la reutili-


zacion del software. Si los modulos de software son escritos en una forma
abstracta para resolver problemas generales, entonces los modulos pueden ser
reusados con instancias de estos mismos problemas generales que surgen en
otros contextos.
Por ejemplo, la definicion estructural de un muro es la misma de casa a
casa, siendo definido generalmente por cantidad de soportes, espaciamiento de
los soportes, etc. De esta forma, un constructor puede reusar su definiciones de
muro de una casa a otra. En la reutilizacion, algunas partes podran requerir
ajustes, por ejemplo un muro en un edificio comercial podra ser similar al de
una casa, pero el sistema electrico y el soporte podran ser diferentes.
Una forma natural de organizar varios componentes estructurales de un
paquete de software es en un modo jer arquico, la cual agrupa definiciones
abstractas similares de una forma nivel por nivel que va de lo especfico a
lo mas general conforme se recorre hacia arriba la jerarqua. Un uso com un
de tales jerarquas es en una grafica organizacional, donde cada enlace hacia
arriba puede leerse como es un, como en un rancho es una casa, y una casa
es un edificio. Este tipo de jerarqua es u
til en el dise
no de software, porque
agrupa funcionalidad com un al nivel mas general, y vistas especializadas de
comportamiento como una extension de la general.
2.1 Metas, principios, y patrones 53

2.1.3. Patrones de dise


no
Investigadores y profesionales de la computacion han desarrollado una
variedad de conceptos organizacionales y metodologas para dise nar software
de calidad orientado al objeto que es conciso, correcto, y reutilizable. El
concepto de patr on de dise no es importante en el material tratado en
estas notas, ya que describe una solucion para un problema de dise no de
software tpico. Un patron es un modelo general para una solucion que
puede ser aplicada en varias situaciones diferentes. Este describe los elementos
principales de una solucion en una forma abstracta que puede ser especializada
para un problema particular. El patron tiene un nombre para identificacion,
un contexto para describir los escenarios donde puede ser aplicado, un modelo
para indicar como el modelo es aplicado y un resultado que describe y analiza
lo que el patron produce.
Se revisan varios patrones de diseno en este material, y se muestra como
pueden ser aplicados consistentemente a implementaciones de estructuras de
datos y algoritmos. Los patrones de diseno caen en dos grupos, patrones para
resolver problemas de dise no de algoritmos y y para problemas de ingeniera
de software. Algunos de los patrones de dise no de algoritmos son:

Recursion.

Amortizacion.

Divide y venceras.

Recortar y buscar o decrementa y venceras.

Fuerza bruta.

Metodo voraz

Programacion dinamica

Algunos de los patrones de dise


no de ingeniera de software que seran
revisados son:

Posicion

Adaptador
54 Dise
no orientado al objeto

Iterador

Metodo modelo.

Composicion.

Comparador.

Decorador.

2.2. Herencia y polimorfismo


Para poder emplear las relaciones jerarquicas, las cuales son comunes en
proyectos de software, la aproximacion de dise
no orientada al objeto da varias
formas de reusar codigo.

2.2.1. Herencia
El paradigma orientado al objeto da una estructura organizacional modular
y jerarquica para reusar codigo, mediante la tecnica de herencia. Esta tecnica
permite el dise no de clases generales que pueden ser especializadas a clases
mas particulares, con las clases especializadas reutilizando el codigo de la clase
general. La clase general, la cual tambien es conocida como una clase base o
superclase, puede definir variables de instancia y metodos generales que son
aplicables la mayora de las veces. Una clase que especializa, o extiende o
hereda de, una superclase no necesita dar nuevas implementaciones para los
metodos generales, para eso los hereda. Solo debera definir aquellos metodos
que son especializados para la subclase particular.
Ejemplo. Considerar una clase S que define objetos con un campo, x
entero, y tres metodos, a(), b(), y c() devolviendo diferentes tipos. Suponer
que se define una clase T que extiende a S e incluye un campo adicional, y
entero, y dos metodos, d() y e() devolviendo diferentes tipos. La clase T
hereda los miembros de la clase S. Se ilustra la relacion entre la clase S y T en
un diagrama de Lenguaje Unificado de Modelado o UML (Unified Modeling
Language) en la figura 2.1. Cada rectangulo en el diagrama indica una clase,
poniendo en subrectangulos el nombre, los campos, y los metodos.
2.2 Herencia y polimorfismo 55

S
-x : int
+a : int
+b : int
+c (int n) : void
~
w
w
w
w

T
-y : int
+d : int
+e (int n) : void

Figura 2.1: Diagrama UML de la relacion entre las clases S y T.

Creaci
on y referencia de objetos
Cuando un objeto o es creado, memoria es asignada para sus campos de
datos, y estos campos son inicializados a valores especficos. Generalmente,
se asocia un objeto nuevo o con una variable, la cual sirve como un enlace
al objeto o, y se dice que referencia o. Para acceder al objeto o se puede
solicitar la ejecucion de algunos de los metodos de o, o mirar uno de los
campos de o. La forma primaria como un objeto p interact ua con otro objeto
o es mandando un mensaje a o usando alguno de los metodos de o. La
segunda forma como p puede interactuar con o es accediendo alguno de los
campos de o directamente, siempre y cuando o de los permisos para hacerlo.

Enlazado din
amico
Cuando un programa desea llamar un cierto metodo a() de alg un objeto
o, este manda un mensaje a o, el cual se denota, usando la sintaxis operador
punto, como o.a(). Cuando se ejecuta la version compilada del programa se le
indica al ambiente de ejecucion que revise la clase T de o para indicar si la clase
T soporta un metodo a(), y si es as, lo ejecute. En la revision, primero, se ve
si la clase T define un metodo a() para ejecutarlo, si no lo define, entonces se
revisa la superclase S de T. Si S lo define, entonces se ejecuta. Si S tampoco
define a(), entonces se repite la b
usqueda en la superclase de S. La b usqueda
56 Dise
no orientado al objeto

continua hacia arriba en la jerarqua de clases hasta que se encuentra el metodo


a() y lo ejecuta, o hasta que se alcanza la clase raz, la clase Object, sin un
metodo a(), lo cual genera un error en tiempo de ejecucion. El algoritmo que
procesa el mensaje o.a() para encontrar el metodo especfico invocado es
llamado el algoritmo de enlazado din amico, o envo din amico, el cual da
un mecanismo efectivo para encontar software reutilizable. Tambien permite
otra tecnica poderosa de la programacion orientada al objeto, polimorfismo.

2.2.2. Polimorfismo
Polimorfismo significa varias formas. En el contexto del dise no orientado
al objeto, se refiere a la habilidad de una variable objeto de tomar diferentes
formas. Java direcciona objetos usando variables referencia. La variable refe-
rencia o debera definir cual clase de objetos se le permite referir, en terminos
de alguna clase S. Pero esto implica que o pueda tambien referirse a cualquier
objeto perteneciente a una clase T que extienda S. Ahora suponer lo que
sucede cuando S define un metodo a() al igual que T. El algoritmo de enlazado
dinamico para la invocacion del metodo siempre inicia su b usqueda desde
la clase mas restrictiva que aplique. Cuando o refiere a un objeto de clase T,
entonces usara el metodo a de T cuando se pida por o.a(), y no el de S. Se
dice, en este caso, que T anula el metodo a() de S. Por otra parte, cuando
o se refiere a un objeto de clase S , se ejecutara el metodo a() de S para
o.a(). El polimorfismo es u til porque la llamada o.a() no necesita saber si
el objeto o se refiere a una instancia de T o S para lograr que el metodo a()
se ejecute correctamente. As, la variable objeto o puede ser polim orfica,
dependiendo de la clase especfica de los objetos que sean referenciados. Con
esta funcionalidad se permite que una clase especializada T que extiende una
clase S, herede los metodos estandar de S, y redefina otros metodos de S para
considerar las propiedad particulares de objetos de T.
Java tambien permite una tecnica util relacionada al polimorfismo, llamada
sobrecarga de metodos. La sobrecarga se da cuando una clase T tiene metodos
multiples con el mismo nombre, y cada uno con firma diferente. La firma de
un metodo es la combinacion del nombre y el tipo y la cantidad de argumentos
que le son pasados. No importa que varios metodos en una clase tengan el
mismo nombre, ya que pueden diferenciarse por el compilador, dado que
estos tienen firmas diferentes. En los lenguajes que permiten sobrecarga de
metodos, el ambiente de ejecucion determina cual metodo actual llamar para
un metodo especfico buscando hacia arriba en la jerarqua de clases para
2.2 Herencia y polimorfismo 57

encontrar el primer metodo con una firma que empate con el metodo llamado.
Por ejemplo, para una clase T, la cual define un metodo a(), extiende la clase
U, qua a su vez define un metodo a(x,y). Si un objeto o de clase T recibe
el mensaje o.a(x,y), entonces esta es la version de U del metodo a() que
es llamada. El polimorfismo verdadero aplica solo a metodos que tengan la
misma firma, pero que estan definidos en clases diferentes.
Herencia, polimorfismo, y sobrecarga de metodos soportan el desarrollo de
sofware reutilizable. Se pueden definir clases que hereden variables y metodos
de instancia y luego se pueden definir variables y metodos de instancia nuevos
que traten los aspectos especiales de objetos de la nueva clase.

2.2.3. Uso de herencia en Java


Hay dos formas basicas de usar herencia de clases en Java, la especiali-
zaci
on y la extensi
on.

Especializaci
on
Se realiza cuando se especializa una clase general a subclases particulares.
Tales subclases tienen una relacion es un(a) a su superclase. Una subclase
hereda todos los metodos de la superclase. Para cada metodo heredado, si
ese metodo funciona correctamente no importando si esta funcionando para
una especializacion, no se requiere trabajo adicional. Por otra parte, si un
metodo general de la superclase no trabaja correctamente en la subclase,
entonces se podra anular el metodo para lograr la funcionalidad correcta en
la subclase. Por ejemplo, se podra tener una clase general, Perro, la cual
tiene metodos beber() y olfatear(). Especializando esta clase a Sabueso
quizas no requiera anular el metodo beber(), pero podra requerirse anular
el metodo olfatear(), ya que un sabueso tiene mas desarrollado su olfato.
As, la clase Sabueso especializa los metodos de su superclase, Perro.

Extensi
on
En la extension se usa herencia para reutilizar el codigo escrito para
metodos de la superclase, pero luego se agregan nuevos metodos en la subclase
para extender su funcionalidad. Por ejemplo, para la clase Perro, se podra
crear una subclase, Xolo, la cual hereda los metodos de la superclase, pero
luego agregar un metodo nuevo, curiosidad(), ya que estos tienen un instinto
58 Dise
no orientado al objeto

curioso que no esta presente en los perros en general. Cuando se agrega el


metodo nuevo, se esta extendiendo la funcionalidad del perro en general.
En Java, cada clase puede extender a una sola classe. A
un si una definicion
de clase no hace uso explcito de la clausula extends, esta todava hereda
de la clase java.lang.Object. Por lo anterior, se dice que Java solo permite
herencia u nica entre clases.

Formas de anulaci
on de m
etodos
En la declaracion de una nueva clase, Java usa dos formas de anulacion
de metodos, refinamiento y reemplazo. En el reemplazo, un metodo com-
pletamente sustituye el metodo de la superclase que esta anulando. En Java,
todos los metodos regulares utilizan esta forma.
Sin embargo, en el refinamiento un metodo no reemplaza el metodo de su
superclase, pero en su lugar agrega codigo adicional al de la superclase. En
Java, todos los constructores utilizan la forma de refinamiento, un esquema
llamado, encadenamiento constructor. Es decir, un constructor inicia su
ejecucion llamando un constructor de la superclase. Esta llamada puede ser
hecha explcitamente o implcitamente. Para llamar un constructor de la
superclase explcitamente, se usa la palabra reservada super para referirse a
la superclase. Por ejemplo, super() llama el constructor de la superclase sin
argumentos. Sin embargo, si no se hace una llamada explcita en el cuerpo del
constructor, el compilador inserta automaticamente, como la primera lnea
del constructor, una llamada a super(). En resumen, los constructores usan
la forma refinamiento mientras los metodos usan reemplazo.

Palabra reservada this


En ocasiones, en una clase Java es conveniente referirse a la instancia
actual de esa clase. Java proporciona la palabra reservada, this, para tal
referencia. La referencia this es u til, por ejemplo, si se quisiera pasar el
objeto actual como parametro a un metodo. Otra aplicacion de this es para
referenciar un campo dentro del objeto actual que tenga un nombre que
colisione con el nombre del parametro de una clase, o la variable definida en
el bloque actual, como se muestra en la clase ProbadorThis, listado 2.1.
Cuando ProbadorThis es ejecutado, se muestra lo siguiente:

La variable local perro = 8


El campo perro = 1
2.2 Herencia y polimorfismo 59

1 public class ProbadorThis {


2 public int perro = 1; // variable de instancia
3 public static void main ( String args []) {
4 ProbadorThis t = new ProbadorThis ();
5 t . golpear ();
6 }
7 public void golpear () {
8 int perro = 8; // un perro diferente !
9 System . out . println ( " La variable local perro = " + perro );
10 System . out . println ( " El campo perro = " + this . perro );
11 }
12 }

Listado 2.1: Programa de ejemplo que muestra el uso de la referencia this para
resolver la ambig
uedad entre un campo del objeto actual y la variable local con el
mismo nombre

Ejemplo de herencia
Se consideran los siguientes ejemplos simples para mostrar los conceptos
previos de herencia y polimorfismo revisados previamente.
Se considera un conjunto de varias clases para generar y mostrar progresio-
nes numericas. Una progresion numerica es una secuencia de numeros, donde
cada uno depende de uno, o de mas n umeros previos. En una progresi on
aritm etica se determina el siguiente n umero por adicion y en una progre-
si
on geom etrica se determina el siguiente numero con una multiplicacion.
En cualquier caso, una progresion requiere una forma para definir su primer
valor y tambien para identificar el valor actual.
Se inicia definiendo una clase, Progresion, mostrada en el listado 2.2, la
cual define los campos estandares y los metodos de una progresion numerica.
En particular, se definen los siguientes dos campos entero largo:

primero. Primer valor de la progresion;

actual. Valor actual de la progresion;

y los siguientes tres metodos:

primerValor(). Reinicia la progresion al primer valor y devuelve este.

siguienteValor(). Avanza la progresion al siguiente valor y lo devuel-


ve.
60 Dise
no orientado al objeto

mostrarProgresion(). Reinicia la progresion e imprime los primeros


n valores de la progresion.
El metodo mostrarProgresion() no tiene salida en el sentido de que no
regresa ningun valor, mientras ambos metodos primerValor() y siguiente-
Valor() devuelven enteros largos. Entonces, primerValor() y siguiente-
Valor() son funciones, y mostrarProgresion() es un procedimiento.
La clase Progresion tambien incluye un metodo Progresion(), el cual es
un constructor. Los constructores configuran todas las variables de instancia
al momento que un objeto de esta clase es creado. El objetivo de la clase
Progresion es ser una una superclase desde la cual las clases especializadas
hereden, as el constructor esta codificado por lo que sera incluido en los
constructores de cada clase que extienda la clase Progresion.
1 /* *
2 * Una clase para progresiones num e ricas
3 */
4 public class Progresion {
5 /* * Primer valor de la progresi o n . */
6 protected long primero ;
7 /* * Valor actual de la progresi o n . */
8 protected long actual ;
9 /* * Constructor por defecto . */
10 Progresion () {
11 actual = primero = 0;
12 }
13 /* * Reinciar la progresi o n al primer valor .
14 *
15 * @return primer valor
16 */
17 protected long primerValor () {
18 actual = primero ;
19 return actual ;
20 }
21 /* * Avanzar la progresi o n al siguiente valor .
22 *
23 * @return siguiente valor de la progresi on
24 */
25 protected long si guienteV alor () {
26 return ++ actual ; // siguiente valor por defecto
27 }
28 /* * Imprimir los primeros n valores de la progresi on
29 *
30 * @param n n u mero de valores a imprimir
31 */
32 public void m o s t r a r P r o g r e s i o n ( int n ) {
33 System . out . print ( primerValor ());
34 for ( int i = 2; i <= n ; i ++)
35 System . out . print ( " " + sigui enteValo r ());
36 System . out . println (); // salto del l nea
37 }
38 }
2.2 Herencia y polimorfismo 61

Listado 2.2: Clase progresion numerica general.

Una clase de progresi


on aritm
etica
Se considera ahora el dise
no de la clase ProgresionAritmetica, la cual se
muestra en el listado 2.3. Esta clase define una progresion aritmetica, donde
el siguiente valor es determinado agregando un incremento constante, inc, al
valor previo. ProgresionAritmetica hereda los campos primero y actual y
los metodos primerValor() y mostrarProgresion() de la clase Progresion.
Ademas esta agrega un campo nuevo, inc, para guardar el incremento, y dos
constructores para configurar el incremento. ProgresionAritmetica anula
el metodo siguienteValor() para calcular el siguiente valor de acuerdo a
una progresion aritmetica.
El polimorfismo es usado en las clases senaladas previamente. Cuando una
referencia Progresion esta apuntando a un objeto ProgresionAritmetica,
entonces los constructores y siguienteValor() de ProgresionAritmetica
seran usados. El polimorfismo esta tambien disponible dentro de la ver-
sion heredada de mostrarProgresion(), ya que las llamadas a los metodos
primerValor() y siguienteValor() estan implcitas para el objeto actual,
siendo de la clase Progresion y de la clase ProgresionAritmetica respecti-
vamente.
1 /* *
2 * Progresi o n aritm e tica .
3 */
4 class P r o g r e s i o n A r i t m e t i c a extends Progresion {
5 /* * Incremento . */
6 protected long inc ;
7 // Se heredan las variables primero y actual de Progresion .
8 /* * Constructor por defecto que fija un incremento unitario . */
9 P r o g r e s i o n A r i t m e t i c a () {
10 this (1);
11 }
12 /* * Constructor parametrizado que proporciona el incremento . */
13 P r o g r e s i o n A r i t m e t i c a ( long incremento ) {
14 inc = incremento ;
15 }
16 /* * Avanza la progresi o n agregando el incremento al valor actual .
17 *
18 * @return valor siguiente de la progresi on.
19 */
20 protected long si guiente Valor () {
21 actual += inc ;
22 return actual ;
62 Dise
no orientado al objeto

23 }
24 // Se heredan los m e
todos primerValor () y m o s t r a r P r o g r e s i o n ( int ).
25 }

Listado 2.3: Clase para progresiones aritmeticas, la cual hereda de la clase


Progresion mostrada en el listado 2.2.

Ejemplo de constructores y this

En la definicion de la clase ProgresionAritmetica, se agregaron dos


constructores, uno por defecto, el cual no toma parametros, y el otro con
un parametro, el cual toma un parametro entero como el incremento de la
progresion. El constructor por defecto llama al de un parametro, usando
this y pasando un uno como el parametro incremento. Los dos constructores
muestran sobrecarga de metodo, ya que un metodo es especificado por su
nombre, la clase del objeto que llama, y los tipos de argumentos que son
pasados a este. En este caso, la sobrecarga es para los constructores, un
constructor por defecto y un constructor parametrizado.
La llamada this(1), lnea 13, listado 2.3, al constructor parametrizado
como la primera sentencia del constructor por defecto lanza una excepcion
a la regla encadenamiento constructor general revisada en la seccion 2.2.3.
Es decir, siempre que la primera sentencia de un constructor C 0 llama otro
constructor C 00 de la misma clase usando la referencia this, el constructor de la
superclase no es implcitamente llamado por C 0 . Observar que el constructor de
la superclase sera eventualmente llamado en la cadena, ya sea explcitamente
o implcitamente.

Una clase de progresi


on geom
etrica

La clase ProgresionGeometrica, listado 2.4, incrementa y muestra una


progresion geometrica, donde el siguiente valor se calcula multiplicando el
valor previo por una base fija, base. Una progresion geometrica es parecida a
una progresion general, excepto en la forma como se determina el siguiente
valor. Por lo anterior, ProgresionGeometrica es declarada como una sub-
clase de la clase Progresion. ProgresionGeometrica hereda los campos
primero y actual, y los metodos primerValor() y mostrarProgresion()
de Progresion.
2.2 Herencia y polimorfismo 63

1 /* *
2 * Progresi o n geom e trica
3 */
4 class P r o g r e s i o n G e o m e t r i c a extends Progresion {
5 /* * Base . */
6 protected long base ;
7 // Se heredan las variables primero y actual
8 /* * Constructor por defecto poniendo base 2. */
9 P r o g r e s i o n G e o m e t r i c a () {
10 this (2);
11 }
12 /* * Constructror parametrizado que proporciona la base .
13 *
14 * @param b base de la progresi on.
15 */
16 P r o g r e s i o n G e o m e t r i c a ( long b ) {
17 base = b ;
18 primero = 1;
19 actual = primero ;
20 }
21 /* * Avanza la progresi o n multiplicando la base con el valor actual .
22 *
23 * @return siguiente valor de la progresi on
24 */
25 protected long si guiente Valor () {
26 actual *= base ;
27 return actual ;
28 }
29 // Se heredan los m e todos primerValor () e i m p r i m i r P r o g r e s i o n ( int ).
30 }

Listado 2.4: Clase para progresiones geometricas.

Una clase de progresi


on Fibonacci
Se define la clase ProgresionFibonacci, listado 2.5, que representa la
progresion Fibonacci, donde el siguiente valor es la suma del valor actual
y del previo. Se usa un constructor parametrizado en la clase Progresion-
Fibonacci para dar una forma diferente de iniciar la progresion.
1 /* *
2 * Progresi o n de Fibonacci
3 */
4 class P r o g r e s i o n F i b o n a c c i extends Progresion {
5 /* * Valor previo . */
6 long prev ;
7 // Se heredan las variables primero y actual
8 /* * Constructor por defecto poniendo 0 y 1 como los primeros dos valores . */
9 P r o g r e s i o n F i b o n a c c i () {
10 this (0 , 1);
11 }
12 /* * Constructor param e trizado dando el primer valor y el segundo .
64 Dise
no orientado al objeto

13 *
14 * @param valor1 primer valor .
15 * @param valor2 segundo valor .
16 */
17 P r o g r e s i o n F i b o n a c c i ( long valor1 , long valor2 ) {
18 primero = valor1 ;
19 prev = valor2 - valor1 ; // valor ficticio precediendo al primero
20 }
21 /* * Avanza la progresi o n agregando el valor previo al valor actual .
22 *
23 * @return valor siguiente de la progresi on
24 */
25 protected long si guienteV alor () {
26 long temp = prev ;
27 prev = actual ;
28 actual += temp ;
29 return actual ;
30 }
31 // Se heredan los m e todos primerValor () e i m p r i m i r P r o g r e s i o n ( int ).
32 }

Listado 2.5: Clase para la progresione Fibonacci.


La clase ProbarProgresion, listado 2.6, realiza una prueba simple de
cada una de las tres subclases anteriores. La variable prog es polimorfica
porque se asignan referencias objeto de la subclases Progresion.
1 /* * Aplicaci o n para las clases progresion */
2 class P r o b a r P r o g r e s i o n {
3 public static void main ( String [] args ) {
4 Progresion prog ;
5 // probar P r o g r e s i o n A r i t m e t i c a
6 System . out . println ( " La progresi o n aritm e tica con el incremento por defecto : " );
7 prog = new P r o g r e s i o n A r i t m e t i c a ();
8 prog . m o s t r a r P r o g r e s i o n (10);
9 System . out . println ( " Progresi o n aritm e tica con incremento 5: " );
10 prog = new P r o g r e s i o n A r i t m e t i c a (5);
11 prog . m o s t r a r P r o g r e s i o n (10);
12 // probar P r o g r e s i o n G e o m e t r i c a
13 System . out . println ( " Progresi o n geom e
trica con base por defecto : " );
14 prog = new P r o g r e s i o n G e o m e t r i c a ();
15 prog . m o s t r a r P r o g r e s i o n (10);
16 System . out . println ( " Progresi o n geom e
trica con base 3: " );
17 prog = new P r o g r e s i o n G e o m e t r i c a (3);
18 prog . m o s t r a r P r o g r e s i o n (10);
19 // probar P r o g r e s i o n F i b o n a c c i
20 System . out . println ( " Progresi o n de Fibonacci con valores de inicio por defecto : " );
21 prog = new P r o g r e s i o n F i b o n a c c i ();
22 prog . m o s t r a r P r o g r e s i o n (10);
23 System . out . println ( " Progresion de Fibonacci iniciando con valores 4 y 6: " );
24 prog = new P r o g r e s i o n F i b o n a c c i (4 ,6);
25 prog . m o s t r a r P r o g r e s i o n (10);
26 }
27 }

Listado 2.6: Aplicacion para probar las clases progresion.


2.3 Excepciones 65

Al ejecutar la aplicacion ProbarProgresion genera la siguiente salida:

La progresi
on aritmetica con el incremento por defecto:
0 1 2 3 4 5 6 7 8 9
Progresi
on aritm
etica con incremento 5:
0 5 10 15 20 25 30 35 40 45
Progresi
on geom
etrica con base por defecto:
1 2 4 8 16 32 64 128 256 512
Progresi
on geom
etrica con base 3:
1 3 9 27 81 243 729 2187 6561 19683
Progresi
on de Fibonacci con valores de inicio por defecto:
0 1 1 2 3 5 8 13 21 34
Progresion de Fibonacci iniciando con valores 4 y 6:
4 6 10 16 26 42 68 110 178 288

La clase Progresion, sus subclases, y el programa de prueba tienen una


cantidad de fallas. Un problema se presenta con la progresion geometrica y la
de Fibonacci porque crecen rapidamente, y no se realiza algo para manejar el
sobreflujo inevitable de los enteros largos involucrados. Por ejemplo, como
340 > 263 , una progresion geometrica con base tres desbordara un entero largo
despues de 40 iteraciones. De igual modo, el 94o (nonagesimo cuarto) n umero
64
de Fibonacci es mayor que 2 ; as, la progresion de Fibonacci desbordara un
entero largo despues de 94 iteraciones. Otro problema es que no se debera
permitir iniciar una progresion Fibonacci con valores arbitrarios. Manejar
errores de entrada o de condicion que suceden en la ejecucion de un programa
Java requiere de un mecanismo que lo maneje. Este se comenta en la siguiente
seccion.

2.3. Excepciones
Las excepciones son eventos no esperados que ocurren en la ejecucion
de un programa. Una excepcion se puede deber a una condicion de error o
una entrada no anticipada. Las excepciones, en Java, pueden ser vistas como
objetos.
66 Dise
no orientado al objeto

2.3.1. Lanzamiento de excepciones


Las excepciones son objetos que son lanzados por el codigo que encuentra
algun tipo de condicion no esperada. Tambien pueden ser lanzadas por el
ambiente de ejecucion cuando se encuentra una condicion no esperada, como
ejecucion fuera de memoria. Una excepcion lanzada es atrapada por otro
codigo que maneja la excepcion de alg un modo, o el programa es terminado
inesperadamente.
Las excepciones se originan cuando una pieza de codigo Java encuentra
algun tipo de problema durante la ejecucion y lanza un objeto excepcion. Es
conveniente dar un nombre descriptivo a la clase del objeto excepcion. Por
ejemplo, si se intenta borrar el decimo elemento de una secuencia que tiene solo
cinco elementos, el codigo podra lanzar una ExcepcionViolacionLimites.
Esta accion podra hacerse usando el siguiente fragmento de codigo:

if (indice >= A.length)


throw new
ExcepcionViolacionLimites("
Indice "+indice+" inv
alido.");

Es conveniente instanciar un objeto excepcion al momento que la excepcion


tiene que ser lanzada. As, una sentencia throw se usa como sigue:

throw new tipo excepcion(param 0, param 1,. . . );

donde tipo excepci on es el tipo de la excepcion y los param i forman la


lista de parametros para un constructor de este tipo de excepcion.
Las excepciones son tambien lanzadas por el propio ambiente de ejecucion
Java. Por ejemplo, si se tiene un arreglo de seis elementos y se pide el
noveno elemento, entonces la excepcion ArrayIndexOutOfBoundsException
es lanzada por el sistema de ejecucion Java.

La cl
ausula throws
Al declarar un metodo es apropiado indicar las excepciones que podra
lanzar, debido a que tiene un proposito funcional y de cortesa. Le permite al
usuario saber que esperar y le indica al compilador las excepciones a las que
se debe preparar. Enseguida se muestra un ejemplo de tal definicion de un
metodo:
2.3 Excepciones 67

public void irCompras() throws


ExcepcionListaComprasCorta, ExcepcionSinDinero {
// cuerpo del m
etodo ...
}

Indicando todas las excepciones que podran ser lanzadas por un metodo,
se prepara a otros para que puedan manejar todos los casos excepcionales que
podran surgir por usar este metodo. Otro beneficio por declarar excepciones es
que no se necesitan atrapar estas excepciones en el metodo. En ocasiones esto
es apropiado cuando otro codigo es el responsable del origen de la excepcion.
Se muestra una excepcion que es pasada:

public void estarListoParaClases() throws


ExcepcionListaComprasCorta, ExcepcionSinDinero {
irCompras();
/* no se tiene que intentar o atrapar las excepciones
que irCompras() podr
a lanzar, ya que estarListo-
ParaClase() las pasar
a de largo.
*/
hacerGalletas();
}

Un metodo puede declarar lanzar tantas excepciones como se quiera. Tal


lista puede simplificarse si todas las excepciones lanzadas son subclases de la
misma excepcion. En tal caso, solo se tiene que declarar que un metodo lanza
la superclase apropiada.

Tipos de lanzables
Java define las clases Exception y Error como subclases de Throwable.
Throwable indica que cualquier objeto podra ser lanzado y atrapado. La
clase RuntimeException esta definida como subclase de Exception. La clase
Error es usada para condiciones anormales en el ambiente de ejecucion, como
agotamiento de memoria. Los errores pueden ser atrapados, pero quizas no se
debera, porque estos avisan de problemas que no pueden ser manejados apro-
piadamente. Un mensaje de error o una repentina terminacion de programa
esta mas alla de lo manejable. La clase Exception es la raz de la jerarqua
excepcion. Excepciones especializadas, como ExcepcionViolacionLimites,
deberan ser definidas como subclases de Exception o de RuntimeException.
68 Dise
no orientado al objeto

Las excepciones que no son subclases de RuntimeException deberan ser


declaradas en la clausula throws de cualquier metodo que pueda lanzarla.

2.3.2. Atrapar excepciones


Cuando una excepcion es lanzada, esta deber ser atrapada o el programa
terminara. En cualquier metodo particular, una excepcion puede ser pasada al
metodo llamador o puede ser atrapada en el metodo. Cuando una excepcion
es atrapada, esta puede ser analizada y manejada. La forma para manejar las
excepciones es try (intentar) ejecutar alguna porcion de codigo que podra
lanzar una excepcion. Si se lanza una excepcion, entonces la excepcion es
atrapada y el flujo de control pasa a un bloque catch predefinido con el
codigo para manejar la excepcion.
La sintaxis para un bloque try...catch es:

try
bloque principal de sentencias
catch(tipo excepcion1 variable 1)
bloque de sentencias 1
catch(tipo excepcion2 variable 2)
bloque de sentencias 2
...
finally
bloque de sentencias n

donde debe haber al menos un catch, siendo finally opcional. Cada tipo -
de sentencias i es el tipo de alguna excepcion.
El ambiente de ejecucion Java inicia ejecutando un bloque try...catch
con la ejecucion del bloque de sentencias, bloque principal de sentencias.
Si esta ejecucion no genera excepciones, entonces el flujo de control sigue
con la primera sentencia despues de la u ltima lnea del bloque completo
try...catch, a menos que se incluya la parte finally opcional.
Por otra parte, si el bloque, bloque principal de sentencias, genera
una excepcion, entonces la ejecucion en en el bloque try...catch termina en
ese punto y la ejecucion pasa al bloque catch cuyo tipo excepcion sea mas
cercana a la excepcion lanzada. La variable para esta sentencia catch refiere
al propio objeto excepcion, el cual puede ser usado en el bloque de la sentencia
catch apareado. Una vez que la ejecucion de ese bloque catch se completa,
2.3 Excepciones 69

el flujo de control es pasado al bloque finally opcional, si este existe, o a la


primera sentencia despues de la ultima lnea del bloque try...catch. De otra
forma, si no hay bloque catch que empate la excepcion lanzada, entonces el
control es pasado al bloque finally opcional, si existe, y luego la excepcion
es lanzada de regreso al metodo llamador.
Considerar el siguiente ejemplo del fragmento de codigo:

int indice = Integer.MAX VALUE; // -1+2E31


try { // El c
odigo podr
a tener un problema ...
String aComprar = listaCompras[indice];
}
catch (ArrayIndexOutOfBoundsException e) {
System.out.println("
Indice "+indice+" inv
alido.");
}

Si el codigo anterior no atrapa una excepcion lanzada, entonces el flujo


de control saldra inmediatamente del metodo y regresara al codigo que
llamo al metodo. En el metodo que hizo la llamada, el ambiente de ejecucion
buscara nuevamente por un bloque catch. Si no lo hay, el flujo de control
pasara al que hizo la llamada para ir a ese metodo, y as sucesivamente.
Eventualmente, si ning un codigo atrapa la excepcion, el ambiente de ejecucion,
el originador del flujo de control del programa, atrapara la excepcion. En
este punto, un mensaje de error y la traza de la pila es mostrada en la salida
estandar y el programa es terminado.
El siguiente es un mensaje de error en tiempo de ejecucion:

java.lang.NuIlPointerException: Returned a null locator


at java.awt.Component.handleEvent(Component.java:900)
at java.awt.Component.postEvent(Component.java:838)
at java.awt.Component.postEvent(Component.java:845)
at sun.awt.motif.MButtonPeer.action(MButtonPeer.java:39)
at java.lang.Thread.run(Thread.java)

Una vez que una excepcion es atrapada, hay varias cosas que un progra-
mador podra querer hacer. Una posibilidad es mostrar un mensaje de error y
terminar el programa. Tambien hay algunos casos interesantes en los cuales
la mejor forma para manejar una excepcion es ignorarla (usando un bloque
catch vaco).
70 Dise
no orientado al objeto

Ignorar una excepcion es hecho usualmente, por ejemplo, cuando el pro-


gramador no le importa si hay o no hay excepcion. Otra manera legtima
de manejar excepciones es creando y lanzando otra excepcion, quizas una
que indique la condicion excepcional mas precisa. El siguiente codigo es un
ejemplo de esta forma:

catch (ArrayIndexOutOfBoundsException e) {
throw new ExcepcionListaComprasCorta(
"
Indice del producto no est
a en lista de compras");
}

La mejor forma de manejar una excepcion, aunque no siempre es posible,


es encontrar el problema, arreglarlo, y continuar la ejecucion.

2.4. Interfaces y clases abstractas


Para que dos objetos puedan interactuar, estos deben conocer acerca de
los mensajes que cada uno podra aceptar, es decir, los metodos que cada objeto
soporta. Para reforzar este conocimiento, el paradigma del dise no orientado
al objeto requiere que las clases indiquen la interfaz de programaci on de
aplicaciones o API (Application Programming Interface), o interfaz, que
sus objetos presentan a otros objetos. En la aproximacion ADT usada, seccion
2.1.2, para estructuras de datos en este texto, una interfaz definiendo un ADT
es indicado como un tipo definicion y una coleccion de metodos para este tipo,
con los argumentos para cada metodo siendo de tipos especificados. En su
momento, esta especificacion es reforzada por el compilador o el sistema de
ejecucion, los cuales requieren que los tipos de los parametros que son pasados
a los metodos sean del tipo indicado en la interfaz. Este requerimiento es
conocido como tipado fuerte. El programador es obligado a tener que definir
interfaces y usar en estas tipado fuerte, pero esta carga es recompensada, por
obligar a emplear el principio de encapsulacion y porque atrapa errores de
programacion que de otra forma no se sabran.

2.4.1. Implementaci
on de interfaces
El elemento estructural principal en Java que forza una API es la interfaz.
Una interfaz es una coleccion de declaraciones de metodos sin datos y sin
cuerpos. Es decir, los metodos de una interfaz siempre estan vacos, son solo
2.4 Interfaces y clases abstractas 71

firmas de metodos. Cuando una clase implementa una interfaz, debera im-
plementar todos los metodos declarados en la interfaz. De esta forma, las
interfaces hacen cumplir los requisitos que una clase implementadora tenga
metodos con ciertas firmas especificadas.
Por ejemplo, suponer que se quiere crear un inventario de antiguedades,
categorizadas como objetos de tipos varios y con propiedades diversas. Se
podra querer identificar algunos de los objetos como vendibles, en tal caso
estos deberan implementar la interfaz Vendible, listado 2.7.

1 /* * Interfaz para objetos que pueden ser vendidos . */


2 public interface Vendible {
3
4 /* * descripci
o n del objeto */
5 public String descripcion ();
6
7 /* * precio de lista */
8 public float precioLista ();
9
10 /* * precio m
nimo de venta */
11 public float precioMinimo ();
12 }

Listado 2.7: Interfaz Vendible.

Despues se puede definir una clase concreta, Fotografia, listado 2.8,


que implementa la interfaz Vendible, para indicar que se estara dispuesto
a vender cualquiera de los objetos fotografa. Esta clase define un objeto
que implementa cada uno de los metodos de la interfaz Vendible, como se
requiere. Ademas, agrega el metodo esColor() especializado para objetos
Fotografia.
Otro tipo de antig uedades en el inventario podra ser algo que se deba
transportar. Para tales objetos, se define la interfaz Transportable, listado
2.9.
Se podra luego definir la clase ArticuloEmpacado, listado 2.10, para
antiguedades diversas que puedan ser vendidas, empacadas, y enviadas. Por
lo tanto, la clase ArticuloEmpacado implementa los metodos de la interfaz
Vendible y Transportable, ademas tambien se agregan los metodos espe-
cializados para poner un seguro y las dimensiones de un artculo empacado.
La clase ArticuloEmpacado muestra otra caractersticas de las clases
y las interfaces, una clase puede implementar varias interfaces, lo cual da
flexibilidad cuando se definen clases que deberan cumplir varias API. Mientras
72 Dise
no orientado al objeto

1 /* * Clase para fotograf


as que pueden ser vendidas . */
2 public class Fotografia implements Vendible {
3 private String descripcion ;
4 private float precio ; // precio puesto
5 private boolean color ; // true si foto es a color
6
7 // constructor
8 public Fotografia ( String d , float p , boolean c ) {
9 descripcion = d ;
10 precio = p ;
11 color = c ;
12 }
13
14 public String descripcion () { return descripcion ; }
15 public float precioLista () { return precio ; }
16 public float precioMinimo () { return precio /4; }
17 public boolean esColor () { return color ; }
18 }

Listado 2.8: Clase Fotografia implenentando la interfaz Vendible.

1 /* * Interfaz para objetos que pueden ser transportados . */


2 public interface Transportable {
3
4 /* * peso en gramos */
5 public int peso ();
6
7 /* * si el objeto es peligroso */
8 public boolean esPeligroso ();
9 }

Listado 2.9: Interfaz Transportable.


2.4 Interfaces y clases abstractas 73

1 /* * Clase para objetos que pueden ser vendidos , empacados ,


2 * y enviados . */
3 public class A r t i c u l o E m p a c a d o implements Vendible , Transportable {
4 private String descripcion ;
5 private float precio ;
6 private int peso ;
7 private boolean pelig ;
8 private int largo = 0; // largo empaque en cm
9 private int ancho = 0; // ancho empaque en cm
10 private int alto = 0; // alto empaque en cm
11 /* * Constructor */
12 public A r t i cu l o E m p a c a d o ( String d , float p , int pes , boolean pel ) {
13 descripcion = d ;
14 precio = p ;
15 peso = pes ;
16 pelig = pel ;
17 }
18 public String descripcion () { return descripcion ; }
19 public float precioLista () { return precio ; }
20 public float precioMinimo () { return precio /4; }
21 public int peso () { return peso ; }
22 public boolean esPeligroso () { return pelig ; }
23 public float v alorAseg urado () { return precio *1.5 f ; }
24 public void setCaja ( int l , int an , int al ) {
25 largo = l ;
26 ancho = an ;
27 alto = al ;
28 }
29 }

Listado 2.10: Clase ArticuloEmpacado


74 Dise
no orientado al objeto

que una clase en Java puede extender solamente una clase, puede sin embargo
implementar varias interfaces.

2.4.2. Herencia m
ultiple en interfaces
La habilidad de extender de mas de una clase se conoce como herencia
m ultiple. En Java, herencia m ultiple esta permitida para interfaces pero
no para clases. En Java no se permite la herencia m ultiple en clases, por la
posible confusion cuando una clase intente extender de dos clases que tengan
metodos con cuerpo diferente y firma igual. Esta confusion no existe en las
interfaces porque sus metodos estan vacos. Al no haber confusion, la herencia
m ultiple de interfaces es u
til.
En Java, el uso de herencia m ultiple de interfaces es para aproximar una
tecnica de herencia multiple llamada mezcla. Se mezclan los metodos de dos o
mas interfaces no relacionadas para definir una interface que combine su funcio-
nalidad, posiblemente agregando mas metodos por su cuenta. Para el ejemplo
de los objetos antig uedad, se podra definir la interfaz ArticuloAsegurable
para los artculos asegurados, como se muestra enseguida:

public interface ArticuloAsegurable


extends Transportable, Vendible {
/** Devolver el valor asegurado */
public int valorAsegurado();
}

La interfaz ArticuloAsegurable mezcla los metodos de la interfaz Trans-


portable con los metodos de la interfaz Vendibles, y agrega el metodo extra
valorAsegurado(). La interfaz ArticuloAsegurable podra permitir definir
la clase ArticuloEmpacado como:

public class ArticuloEmpacado


implements ArticuloAsegurable {
// mismo c
odigo de la clase ArticuloEmpacado
}

Las interfaces de Java que aproximan la mezcla son: java.lang.Cloneable,


la cual agrega una caracterstica de copia para una clase; java.lang.-
Comparable, la cual agrega una caracterstica de comparacion para una
2.4 Interfaces y clases abstractas 75

clase; y java.util.Observer, la cual agrega una caracterstica de actualiza-


cion a una clase que quiere ser notificada cuando ciertos objetos observables
cambien de estado.

2.4.3. Clases abstractas y tipado fuerte


Una clase abstracta es una clase que contiene declaraciones de metodos
vacos, o sea, declaraciones de metodos sin cuerpo, y posiblemente definiciones
de metodos concretos y variables de instancia. As, una clase abstracta
esta entre una interfaz y una clase concreta. Al igual que una interfaz, una
clase abstracta no puede ser instanciada, o sea, crear un objeto desde la clase
abstracta. Una subclase de una clase abstracta debera dar una implementacion
para los metodos abstractos de su superclase, a menos que esta misma sea
abstracta. Al igual que una clase concreta, una clase abstracta A puede
extender otra clase abstracta, y clases abstractas o concretas pueden tambien
extender a A. Una clase nueva que no sea abstracta y extienda superclases
abstractas debera implementar todos los metodos abstractos. Una clase
abstracta usa el estilo de especificacion de herencia, pero tambien permite la
especializacion y estilos de extension.

La clase java.lang.Number
Las clases n
umero Java, cuadro 1.2, especializan la clase abstracta java.-
lang.Number. Cada clase n umero concreta, tales como java.lang.Integer
y java.lang.Double, extienden la clase java.lang.Number y completan los
detalles para los metodos abstractos de la superclase. Por ejemplo, los metodos
intValue(), floatValue(), longValue(), y doubleValue() son todos abs-
tractos en java.lang.Number. Cada clase n umero concreta debera especificar
los detalles de estos metodos.

Tipado fuerte
Un objeto puede ser visto como siendo de varios tipos. El tipo primario de
un objeto o es la clase C indicada al momento que o fue instanciado. Ademas,
o es del tipo S para cada superclase S de C y es del tipo I para cada interfaz
I implementada por C.
Sin embargo, una variable puede ser declarada para ser de un solo tipo,
ya sea clase o interfaz, la cual determina como la variable es usada y como
76 Dise
no orientado al objeto

ciertos metodos actuaran en esta. De igual forma, un metodo tiene un u nico


tipo de regreso. En general, una expresion tiene un tipo unico.
La tecnica de tipado fuerte de Java, forzar a que todas las variables sean
tipadas y que los metodos declaren el tipo que se espera y que se regresa,
ayuda a prevenir errores. Pero por los requerimientos rgidos con tipos, es en
ocasiones necesario cambiar, o convertir, un tipo en otro. Tales conversiones
podran tener que ser especificadas por un operador de conversion explcito.
En la seccion 1.3.3 se indica como convertir a tipos base. En la siguiente
seccion se indica como se realizan las conversiones para variables referencia.

2.5. Conversiones y gen


ericos
2.5.1. Conversiones para objetos
Se comenta enseguida varios metodos para conversiones de tipo para
objetos.

Conversiones anchas
Una conversi on ancha ocurre cuando un tipo T es convertido a un tipo
U amplio. Los siguientes son casos comunes de conversiones anchas:

T y U son tipos clase y U es una superclase de T.

T y U son tipos interfaz y U es una superinterfaz de T.

T es una clase que implementa la interfaz U.

Las conversiones anchas son hechas automaticamente para guardar el


resultado de una expresion en una variable, sin la necesidad de una conversion
explcita. Por lo tanto, se puede asignar directamente el resultado de una
expresion de tipo T en una variable v de tipo U cuando la conversion de T
a U es una conversion ancha. El siguiente fragmento de codigo muestra una
expresion de tipo Integer, un objeto nuevo construido, asignandose a una
variable de tipo Number.

Integer i = new Integer(3);


Number n = i; // conversion ancha
2.5 Conversiones y gen
ericos 77

La correctez de una conversion ancha es revisada por el compilador y su


validez no requiere prueba por el ambiente de ejecucion Java al ejecutar el
programa.

Conversiones estrechas
Una conversi on estrecha ocurre cuando un tipo T es convertido a un
tipo S estrecho. Los siguientes son casos comunes de conversiones estrechas:

T y S son tipos clase y S es una subclase de T.

T y S son tipos interfaz y S es una subinterfaz de T.

T es una interfaz implementada por la clase S.

En general, una conversion estrecha de tipos referencia necesita una


conversion explcita. Tambien, la correctez de una conversion estrecha podra
no ser verificada por el compilador. Por lo tanto, su validacion debera ser
probada por el ambiente de ejecucion en la ejecucion del programa.
El siguiente fragmento de codigo ejemplo muestra como usar una conversion
para realizar una conversion estrecha desde el tipo Number al tipo Integer.

Number n = new Integer(2); // conv. ancha Integer->Number


Integer i = (Integer) n; // conv. estrecha Number->Integer

En la primera sentencia, un objeto nuevo de la clase Integer es creado y


asignado a una variable n del tipo Number. As, una conversion ancha ocurre
y no se ocupa conversion. En la segunda sentencia, se asigna n a una variable
i del tipo Integer usando una conversion. La asignacion es posible porque n
se refiere a un objeto del tipo Integer. Sin embargo, como la variable n es
del tipo Number, una conversion estrecha ocurre y la conversion es necesaria.

Excepciones en conversiones
Se puede convertir una referencia objeto o de tipo T en un tipo S, si el
objeto o referido es en realidad del tipo S. Por otra parte, si el objeto o no
es del tipo S, entonces intentar convertir o al tipo S lanzara la excepcion
ClassCastException. Lo anterior se presenta en el siguiente fragmento de
codigo:
78 Dise
no orientado al objeto

Number n;
Integer i;
n = new Integer(3);
i = (Integer) n; // v
alido
n = new Double(3.1416);
i = (Integer) n; // no v
alido

Para evitar problemas como el anterior y evitar poner en el codigo bloques


try...catch cada vez que se haga una conversion, Java proporciona una
forma de asegurarse que una conversion objeto sera correcta mediante el
operador, instanceof, el cual prueba si una variable objeto se refiere a un
objeto de una cierta clase o implementa una cierta interfaz. La sintaxis es:

referencia objeto instanceof tipo referencia

siendo referencia objeto una expresion que evalua a una referencia objeto
y tipo referencia es el nombre de alguna clase, interfaz, o enumeracon exis-
tente. Si referencia objeto es una instancia de tipo referencia entonces
el operador da true, de otra forma da false. Para evitar una excepcion
ClassCastException que lanzara el codigo previo, se debera modificar de
la siguiente forma:

Number n;
Integer i;
n = new Integer(3);
if (n instanceof Integer)
i = (Integer) n; // v
alido
n = new Double(3.1416);
if (n instanceof Integer)
i = (Integer) n; // no sera intentado

Conversiones con interfaces


Las interfaces permiten obligar a que objetos implementen ciertos meto-
dos, pero usando variables interfaz con objetos concretos en ocasiones se
requiere conversion. Suponer que se declara una interfaz Persona, listado 2.11.
Observar que el metodo igualA() de la interfaz Persona toma un parametro
de tipo Persona. De esta forma, se puede pasa un objeto de cualquier clase
que implemente la interfaz Persona a este metodo.
2.5 Conversiones y gen
ericos 79

1 public interface Persona {


2 public boolean igualA ( Persona otra ); // misma persona ?
3 public String getNombre (); // obtener nombre de la persona
4 public int getEdad (); // obtener edad de la persona
5 }

Listado 2.11: Interfaz Persona

En la clase Estudiante, listado 2.12, se implementa la interfaz Persona.


Se asume en el metodo igualA() que el argumento, declarado de tipo Persona,
es tambien del tipo Estudiante y se realiza una conversion estrecha del tipo
Persona, una interfaz, al tipo Estudiante, una clase, con una conversion
explcita. La conversion es permitida en este caso, porque es una conversion
estrecha desde una clase T a una interfaz U, en donde se tiene un objeto
tomado desde T tal que T extiende S y S implementa U.

1 public class Estudiante implements Persona {


2 String id ;
3 String nombre ;
4 int edad ;
5 public Estudiante ( String i , String n , int e ) {
6 id = i ;
7 nombre = n ;
8 edad = e ;
9 }
10 protected int horasEstudio () { return edad /3; }
11 public String getId () { return id ; } // id del estudiante
12 public String getNombre () { return nombre ; } // de interfaz
13 public int getEdad () { return edad ; } // de interfaz
14 public boolean igualA ( Persona otra ) { // de interfaz
15 Estudiante otr oEstudi ante = ( Estudiante ) otra ;
16 return ( id . equals ( otr oEstudi ante . getId ()));
17 }
18 public String toString () { // para mostrar
19 return " Estudiante ( Id : " + id +
20 " , Nombre : " + nombre +
21 " , Edad : " + edad + " ) " ;
22 }
23 }

Listado 2.12: Clase Estudiante implementando la interfaz Persona

Se requiere asegurarse que en la implementacion del metodo igualA() de


la clase Estudiante, no se intente la comparacion de objetos Estudiante con
otros tipos de objetos, o de otra forma, la conversion en el metodo igualA()
fallara.
80 Dise
no orientado al objeto

La posibilidad de realizar conversiones estrechas desde tipos interfaz a


tipos clase permite escribir tipos generales de estructuras de datos que solo
hagan suposiciones mnimas acerca de los elementos que guardan. Se bosqueja
enseguida como construir un directorio para guardar parejas de objetos que
implementan la interfaz Persona. El metodo quitar() realiza una b usqueda
en el contenido del directorio y quita el par de personas indicadas, si existe, y
al igual que el metodo encontrarOtra(), usa el metodo igualA() para la
busqueda.

public class DirectorioPareja {


// ... variables de instancia ...
public DirectorioParejaGenerico() { }
public void insertar(Persona persona, Persona otra) { }
public Persona encontrarOtra(Persona persona) { return null ; }
public void quitar (Persona persona, Persona otra) { }
}

Suponer que se ha llenado un directorio, miDirectorio, con parejas de


objetos Estudiante que representan parejas de compa neros de cuarto. Para
lograr encontrar el compa nero de cuarto de un objeto Estudiante dado,
estudiante1, se podra intentar hacer lo siguiente, lo cual esta mal:

Estudiante estudiante2 =
miDirectorio.encontrarOtra(estudiante1);

Esta sentencia provoca un error de compilacion explicit-cast-required


(conversion explcita requerida), debido a que se intenta hacer una conversion
estrecha sin una conversion explcita. Es decir, el valor devuelto por el metodo
encontrarOtra() es del tipo Persona mientras que la variable estudiante2,
a la cual es asignada, es del tipo estrecho Estudiante, una clase que imple-
menta la interfaz Persona, As, se debe usar hacer una conversion del tipo
Persona al tipo Estudiante, como se muestra enseguida:

Estudiante estudiante2 =
(Estudiante)miDirectorio.encontrarOtra(estudiante1);

Convertir el valor del tipo Persona regresado por el metodo encontrar-


Otra() al tipo Estudiante trabaja bien siempre que se asegure que la
llamada a miDirectorio.encontrarOtra() realmente devuelva un objeto
2.5 Conversiones y gen
ericos 81

Estudiante. En general, las interfaces pueden ser una herramienta valiosa


para el dise
no de estructuras de datos generales, las cuales pueden entonces
ser especializados por otros programadores usando la conversion.

2.5.2. Gen
ericos
A partir de Java 5.0 se incluye una estructura gen erica para usar
tipos abstractos con la finalidad de evitar 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 par ametros de
tipo formal, los cuales podran ser usados, por ejemplo, para abstraer los
tipos de algunas variables internas de la clase. Se usan parentesis angulares
(<>) para encerrar la lista de los parametros de tipo formal. Aunque cualquier
identificador valido puede ser usado para un parametro de tipo formal, por
convencion son usados los nombres de una sola letra may uscula. Dada una
clase que ha sido definida con tales tipos parametrizados, se instancia un
objeto de esta clase usando los par ametros de tipo actual para indicar los
tipos concretos que seran usados.
En el listado 2.13, 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).
La salida de la ejecucion de la clase Par se muestra enseguida:

[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 DirectorioParejaGenerico esta de-
finida en terminos de un parametro de tipo generico P, parcialmente especifi-
cado declarando que extiende la clase Persona.

public class DirectorioParejaGenerico<P extends Persona> {


// ... variables de instancia ...
82 Dise
no orientado al objeto

1 public class Par <L , V > { // entre < y > los par a metros de tipo formal
2 L llave ;
3 V valor ;
4 public void set ( L l , V v ) {
5 llave = l ;
6 valor = v ;
7 }
8 public L getLlave () { return llave ; }
9 public V getValor () { return valor ; }
10 public String toString () {
11 return " [ " + getLlave () + " , " + getValor () + " ] " ;
12 }
13 public static void main ( String [] args ) {
14 Par < String , Integer > par1 = new Par < String , Integer >();
15 par1 . set ( new String ( " altura " ) , new Integer (2));
16 System . out . println ( par1 );
17 Par < Estudiante , Double > par2 = new Par < Estudiante , Double >();
18 par2 . set ( new Estudiante ( " 8403725 A " ," Hector " ,14) , new Double (9.5));
19 System . out . println ( par2 );
20 }
21 }

Listado 2.13: Clase Par

public DirectorioParejaGenerico() { }
public void insertar(P persona, P otra) { }
public P encontrarOtra(P persona) { return null ; }
public void quitar (P persona, P otra) { }
}

Esta clase debera ser comparada con la clase DirectorioPareja de la


seccion 2.5.1. Dada la clase anterior, se puede declarar una variable refiriendo
a una instancia de DirectorioParejaGenerico, que guarde pares de objetos
de tipo Estudiante:

DirectorioParejaGenerico<Estudiante> miDirectorioEstudiante;

La estructura generica permite definir versiones genericas de metodos. En


este caso, se puede incluir la definicion generica entre los metodos modifi-
cadores. Por ejemplo, se muestra enseguida la definicion de un metodo que
puede comparar las llaves de cualesquiera dos objetos Par, dado que sus llaves
implementen la interfaz Comparable:

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


comparaPares(Par<K,V> p, Par<L,W> q) {
2.5 Conversiones y gen
ericos 83

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

Una advertencia importante relacionada a los tipos genericos es que


los elementos guardados en un arreglo no pueden ser de un tipo variable
o parametrizado. Java permite que un arreglo sea definido con un tipo
parametrizado, pero no permite que un tipo parametrizado sea usado para
crear un nuevo arreglo. Java permite que para un arreglo definido con un tipo
parametrizado sea inicializado con un arreglo nuevo no parametrizado, como
se muestra en la clase UsaPar, listado 2.14. A
un as, este mecanismo tardo
causa que el compilador de Java emita una advertencia, porque esto no es
tipado 100 % seguro.

1 public class UsaPar {


2 public static void main ( String [] args ) {
3 Par < String , Integer >[] a = new Par [10]; // correcto , pero da una advertencia
4 Par < String , Integer >[] b = new Par < String , Integer >[10]; // incorrecto
5 a [0] = new Par < String , Integer >(); // est a completamente bien
6 a [0]. set ( " Gato " ,10); // esta y la siguiente tambien est a n bien
7 System . out . println ( " Primer par es " + a [0]. getLlave ()+ " ," + a [0]. getValor ());
8 }
9 }

Listado 2.14: Ejemplo que muestra que un tipo parametrizado sea usado para crear
un nuevo arreglo.
84 Dise
no orientado al objeto
Captulo 3

Arreglos, listas enlazadas y


recurrencia

3.1. Que son las estructuras de datos


Una estructura de datos es un arreglo de datos en la memoria de
una computadora, y en algunas ocasiones en un disco. Las estructuras de
datos incluyen arreglos, listas enlazadas, pilas, arboles binarios, y tablas
de dispersion entre otros. Los algoritmos manipulan los datos en estas
estructuras de varias formas, para buscar un dato particular u ordenar los
datos.

3.1.1. Generalidades de las Estructuras de Datos


Una forma de ver las estructuras de datos es enfocandose en sus fortalezas
y debilidades. En el cuadro 3.1 se muestran las ventajas y desventajas de
las estructuras de datos que se revisaran, los terminos que se usan seran
abordados en otros captulos.

Las estructuras mostradas en el cuadro 3.1, excepto los arreglos, pueden


ser pensados como tipos de datos abstractos o ADT (Abstract Data
Type). Un ADT, de forma general, es una forma de ver una estructura de
datos: enfocandose en lo que esta hace e ignorando como hace su trabajo, es
decir, el ADT debera cumplir con ciertas propiedades, pero la manera como
86 Arreglos, listas enlazadas y recurrencia

Estructura Ventajas Desventajas


Arreglo Insercion rapida, ac- Busqueda y borrado
ceso muy rapido si se lento, tama
no fijo.
conoce el ndice.
Arreglo ordenado B usqueda mas rapi- Insercion y borrado
da que el arreglo des- lento, tamano fijo.
ordenado.
Pila Proporciona acceso Acceso lento a otros
ultimo en entrar, pri- elementos.
mero en salir.
Cola Proporciona primero Acceso lento a otros
en entrar, primero en elementos.
salir.
Lista enlazada Insercion y borrado B
usqueda lenta.
rapido.

Arbol binario B usqueda, insercion Complejo.


y borrado rapido si el
arbol permanece ba-
lanceado.

Arbol rojinegro B usqueda, insercion Complejo.


y borrado rapido.

Arbol siempre balan-
ceado.

Arbol 2-3-4 B usqueda, insercion Complejo.


y borrado rapido.

Arbol siempre balan-
ceado.
Tabla de disper- Acceso muy rapido si Borrado lento, acce-
sion se conoce la llave. In- so lento si no se cono-
sercion rapida. ce la llave, uso inefi-
ciente de la memoria.
Montculo Insercion, borrado y Acceso lento a otros
acceso al elemento elementos.
mas grande rapido
Grafo Modela situaciones Algunos algoritmos
del mundo real son lentos y comple-
jos.

Cuadro 3.1: Caractersticas de las Estructuras de Datos


3.2 Arreglos 87

estara implementado puede variar, a un empleando el mismo lenguaje. Por


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:

1. Insertar un nuevo dato.

2. Buscar un elemento indicado.

3. Borrar un elemento indicado.

Tambien se necesitara saber como iterar a traves de todos los elementos en


la estructura de datos, visitando cada uno en su turno ya sea para mostrarlo
o para realizar alguna accion en este.
Otro algoritmo importante es el ordenamiento. Hay varias formas para
ordenar los datos, desde ideas simples hasta otras que permiten realizarlo de
manera rapida.
El concepto de recurrencia es importante en el dise no de ciertos algoritmos.
La recurrencia involucra que un metodo se llame a si mismo.

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

3.2.1. Guardar entradas de juego en un arreglo


La primera aplicacion que sera revisada es para guardar las entradas en un
arregloen particular, las mejores marcas para un vdeo juego. Los arreglos
tambien se podran usar para guardar registros de pacientes en un hospital o
los nombres de estudiantes.
En primer lugar se debe determinar lo que se quiere incluir en el registro
de una puntuacion alta. Por supuesto, un componente que se debera incluir es
un entero representando los puntos obtenidos, al cual llamaremos puntuacion.
Otra caracterstica que debera incluirse es el nombre de la persona que obtuvo
la puntuacion, la cual denominaremos nombre. La clase EntradaJuego, listado
3.1, tiene el dise
no para manejar una puntuacion.
88 Arreglos, listas enlazadas y recurrencia

1 public class EntradaJuego {


2 protected String nombre ; // nombre del due ~ n o de la puntuaci o
n
3 protected int puntuacion ; // el valor de la puntuaci o n
4 /* * Constructor para crear una entrada del juego */
5 public EntradaJuego ( String n , int p ) {
6 nombre = n ;
7 puntuacion = p ;
8 }
9 /* * Recupera el campo nombre */
10 public String getNombre () { return nombre ; }
11 /* * Recupera el campo puntuaci o n */
12 public int getPuntuacion () { return puntuacion ; }
13 /* * Regresa una cadena de esta entrada */
14 public String toString () {
15 return " ( " + nombre + " , " + puntuacion + " ) " ;
16 }
17 }

Listado 3.1: Clase EntradaJuego.java

Clase para las mayores puntuaciones

La cantidad de puntuaciones que seran guardadas se indica mediante una


constante con nombre, MAX ENTRADAS, la cual es inicializada con el valor 10.
Se usa esta constante para el caso en el que se quiere cambiar facilmente la
cantidad de puntuaciones guardadas. Se define un arreglo, entradas, para
que sea un arreglo de tama no MAX ENTRADAS. Cuando se crea el arreglo sus
entradas son null, pero conforme se juega, se van llenando las entradas del
arreglo con referencias a objetos EntradaJuego.
Se quiere almacenar los mayores registros en un arreglo llamado entradas.
La cantidad de registros puede variar, por lo que se usara el nombre simbolico,
maxEntradas, para representar el n umero de registros que se quieren guardar.
Se debe poner esta variable con un valor especfico, y usando esta variable en el
codigo, se puede hacer facilmente un cambio mas tarde si se requiere. Se define
despues el arreglo, entradas, para que sea un arreglo de tama no maxEntradas.
Inicialmente, el arreglo guarda solamente entradas nulas, pero conforme el
usuario juega, se llenara en el arreglo con nuevos objetos EntradaJuego. Por lo
que se necesitara definir metodos para actualizar las referencias EntradaJuego
en las entradas del arreglo.
La forma como se mantiene organizado el arreglo entradas es simplese
guarda el conjunto de objetos EntradaJuego ordenado por sus valores enteros
puntuacion, de mayor a menor. Si el n umero de objetos EntradaJuego es
menor que maxEntradas, entonces se permite que al final del arreglo se
3.2 Arreglos 89

1 /* * Clase para guardar las puntuaciones altas en un arreglo


2 en orden descendente . */
3 public class Puntuaciones {
4 public static final int MAX_ENTRADAS = 10; // cantidad de puntuaciones
5 protected int numEntradas ; // cantidad . actual de entradas
6 protected EntradaJuego [] entradas ; // arreglo de entradas
7 /* * Constructor por defecto */
8 public Puntuaciones () {
9 entradas = new EntradaJuego [ MAX_ENTRADAS ];
10 numEntradas = 0;
11 }
12 /* * Regresa una cadena de las mayores puntuaciones */
13 public String toString () {
14 String s = " [ " ;
15 for ( int i = 0; i < numEntradas ; i ++) {
16 if ( i > 0) s += " , " ; // separar entradas con comas
17 s += entradas [ i ];
18 }
19 return s + " ] " ;
20 }
21 // ... m e todos para actualizar el conjunto de puntuaciones ...

Listado 3.2: Clase Puntuciones para manejar un conjunto de objetos Entrada.

guarden referencias null. Con este dise no se previene tener celdas vacas, u
hoyos, y los registros estan desde el ndice cero hasta la cantidad de juegos
jugados. Se ilustra una instancia de la estructura de datos en la figura 3.1 y
en la clase Puntuaciones, listado 3.2 parcial, se muestra el dise no.
El metodo toString() devuelve una cadena representando las mayores
puntuaciones del arreglo entradas. Puede tambien ser empleado para proposi-
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:
90 Arreglos, listas enlazadas y recurrencia

Figura 3.1: Una ilustracion de un arreglo de tama


no 10 guardando referencias a
6 objetos entradaJuego desde la celda 0 hasta la 5, y el resto siendo referencias
null.

add(e):inserta la entrada de juego e en la coleccion de las


mayores puntuaciones. Si la coleccion esta llena,
entonces e es agregado solamente si su puntuacion
es mayor que la menor puntuacion en el conjunto,
y en este caso, e reemplaza la entrada con la menor
puntuacion.

El reto principal en la implementacion de esta operacion es saber donde e


debera ir en el arreglo entradas y hacerle espacio a e.
Se supondra que las puntuaciones estan en el arreglo de izquierda a
derecha desde la mayor hasta la menor. Entonces se quiere que dada una
nueva entrada, e, se requiere saber donde estara. Se inicia buscando al final
del arreglo entradas. Si la u ltima referencia en el arreglo no es null y su
puntuacion es mayor que la puntuacion de e, entonces se detiene la b usqueda,
ya que en este caso, e no es una puntuacion alta. De otra forma, se sabe
que e debe estar en el arreglo, y tambien se sabe que el u ltimo registro en
el arreglo ya no debe estar. Enseguida, se va al pen ultima referencia, si esta
referencia es null o apunta a un objeto EntradaJuego con una puntuacion
que es menor que e, esta referencia debera ser movida una celda a la derecha
en el arreglo entrada. Se continua comparando y desplazando referencias de
entradas del juego hasta que se alcanza el inicio del arreglo o se compara la
3.2 Arreglos 91

puntuacion de e con una entrada con una puntuacion mayor. En cualquier


caso, se habra identificado el lugar al que e pertenece. Ver figura 3.2

Figura 3.2: Preparacion para agregar un nuevo objeto entradaJuego al arreglo


Entradas. Para hacer espacio a una nueva referencia, se tienen que desplazar las
referencias con los registros m
as peque
nos que la nueva una celda a la derecha.

En el listado 3.3 se muestra la implementacion para agregar la entrada


juego nuevo e al arreglo entradas. Se usa un ciclo para desplazar las referen-
cias. La cantidad de veces que se repite el ciclo depende de la cantidad de
referencias que se tengan que mover para hacer espacio para la nueva entrada.
La rapidez del metodo dependera de la cantidad de entradas en el arreglo
y del valor de la puntuacion. Cuando el arreglo esta lleno puede ser que la
referencia a la u
ltima entrada se pierda o se falle al agregar la nueva entrada
de juego, e.

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
la lista de las mayores puntuaciones. Por lo tanto se considera como debera
implementarse la siguiente operacion:
92 Arreglos, listas enlazadas y recurrencia

1 /* * Intenta agregar una nueva puntuaci o n a la coleccion si es lo


2 s uf ic ie n te me nt e alta . */
3 public void add ( EntradaJuego e ) {
4 int n ue va Pu n tu ac i on = e . getPuntuacion ();
5 // es la nueva entrada realmente un r e cord ?
6 if ( numEntradas == MAX_ENTRADAS ) { // el arreglo est a lleno
7 if ( n u ev aP u nt ua ci o n <= entradas [ numEntradas -1]. getPuntuacion () )
8 return ; // la nueva entrada , e , no es un record
9 }
10 else // el arreglo no est a lleno
11 numEntradas ++;
12 // Encontrar el lugar en donde la nueva entrada (( e )) estar a
13 int i = numEntradas -1;
14 for ( ; ( i >= 1) && ( n u ev aP u nt ua ci o n > entradas [i -1]. getPuntuacion ()); i - -)
15 entradas [ i ] = entradas [ i - 1]; // mover entrada i un lugar a la derecha
16 entradas [ i ] = e ; // agregar el nuevo r e cord a entradas
17 }

Listado 3.3: Codificaci


on en la clase Puntuciones del metodo add() para insertar
un objeto Entrada.

remove(i):quita y regresa la entrada de juego e con el ndice


i en el arreglo entradas. Si el ndice i esta fue-
ra de los lmites del arreglo entradas, entonces
el metodo debera lanzar una excepcion; de otra
forma, el arreglo entradas sera actualizado para
quitar el objeto en la posicion i y todos los ob-
jetos guardados con ndices mayores que i seran
movidos encima para llenar el espacio del dejado
por el objeto quitado.

La implementacion para remove() sera parecida al algoritmo para agregar


un objeto, pero en forma inversa. Para remover la referencia al objeto con
ndice i, se inicia en el ndice i y se mueven todas las referencias con ndices
mayores que i una celda a la izquierda. Ver figura 3.3.
Los detalles para hacer la operacion de remocion consideran que prime-
ramente se debe guardar la entrada del juego, llamada e, en el ndice i del
arreglo, en una variable temporal. Se usara esta variable para regresar e
cuando se haya terminado de borrar. Para el movimiento de las referencias
que son mayores que i una celda a la izquierda, no se hace hasta el final del
arreglose detiene en la pen ultima referencia, ya que la u ltima referencia
no tiene una referencia a su derecha. Para la u ltima referencia en el arreglo
entradas, es suficiente con anularla. Se termina regresando una referencia de
3.2 Arreglos 93

Figura 3.3: Remocion en el ndice 3 en el arreglo que guarda las referencias a objetos
entradaJuego.

la entrada removida, la cual no tiene una referencia que le apunte desde el


arreglo entradas. En el listado 3.4 se muestra la codificacion del metodo.
Los metodos para agregar y quitar objetos en el arreglo de puntuaciones
altas son simples. Pero forman las bases de las tecnicas usadas para construir
estructuras de datos mas complejas.

1 /* * Quitar el r e cord del ndice i y devolverlo . */


2 public EntradaJuego remove ( int i ) 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 {
3 if (( i < 0) || ( i >= numEntradas ))
4 throw new 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 ( "
I ndice no v
a lido : " + i );
5 // guardar temporalmente el objecto a ser quitado
6 EntradaJuego temp = entradas [ i ];
7 for ( int j = i ; j < numEntradas - 1; j ++) // contar hacia adelante desde i
8 entradas [ j ] = entradas [ j +1]; // mover una celda a la izquierda
9 entradas [ numEntradas - 1 ] = null ; // anular el registro menor
10 numEntradas - -;
11 return temp ; // devolver objeto eliminado
12 }

Listado 3.4: Codificacion en la clase Puntuciones del metodo remove() de acuerdo


a un ndice pasado.
94 Arreglos, listas enlazadas y recurrencia

3.2.2. Ordenar un arreglo


En la seccion previa, se mostro como se pueden agregar objetos o quitar
estos en un cierto ndice i en un arreglo mientras se conserva el ordenamiento
previo de los objetos intacto. En esta seccion, se revisa una forma de iniciar
con un arreglo de objetos que no estan ordenadados y dejarlos en orden. Esto
se conoce como el problema de ordenamiento.

Un algoritmo simple de inserci


on ordenada
Se revisaran varios algoritmos de ordenamiento en este material. Como
introduccion, se describe en esta seccion, un algoritmo simple de ordenamiento,
llamado inserci on ordenada. En este caso, se describe una version especfica
del algoritmo donde la entrada es un arreglo de elementos comparables. Se
consideran tipos mas generales de algoritmos de ordenamiento mas adelante.
El algoritmo de insercion ordenada trabaja de la siguiente forma. Se
inicia con el primer elemento en el arreglo y este elemento por s mismo ya
esta ordenado. Entonces se considera el siguiente elemento en el arreglo. Si
este es menor que el primero, se intercambian. Enseguida se considera el
tercer elemento en el arreglo. Se intercambia hacia la izquierda hasta que
este en su posicion correcta respecto a los primeros dos elementos. De esta
manera se considera el resto de los caracteres, hasta que el arreglo completo
este ordenado. Mezclando la descripcion informal anterior con construcciones
de programacion, se puede expresar el algoritmo de insercion ordenada como
se muestra enseguida.
Algoritmo Insercio nOrdenada(A):
Entrada: Un arreglo A de n elementos comparables
Salida: El arreglo A con elementos reacomodados en orden creciente
para i 1 Hasta n 1 hacer
Insertar A[i] en su posicion correcta en A[0], A[1], . . . , A[i 1].
Esta es una descripcion de alto nivel de insercion ordenada y muestra
porque este algoritmo recibe tal nombreporque cada iteracion inserta el
siguiente elemento en la parte ordenada del arreglo que esta antes de este.
Antes de codificar la descripcion se requiere trabajar mas en los detalles de
como hacer la tarea de insercion.
Se reescribira la descripcion de tal forma que se tenga un ciclo anidado
dentro de otro. El ciclo externo considerara cada elemento del arreglo en turno
y el ciclo interno movera ese elemento a su posicion correcta en el subarreglo
3.2 Arreglos 95

ordenado de caracteres que estan a su izquierda.

Inserci
on ordenada refinada

Se describe a continuacion una descripcion en nivel intermedio del algo-


ritmo de insercion ordenada. Esta descripcion esta mas cercana al codigo
actual, ya que hace una mejor descripcion para insertar el elemento A[i] en el
subarreglo que esta antes de este. Todava emplea una descripcion informal
para el movimiento de los elementos si no estan todava ordenados.
Algoritmo Insercio nOrdenada(A):
Entrada: Un arreglo A de n elementos comparables
Salida: El arreglo A con elementos recodados en orden creciente
para i 1 Hasta n 1 hacer
{Insertar A[i] en su sitio correcto en A[0], A[1], . . . , A[i 1].}
actual A[i]
j i1
Mientras j 0 Y A[j] > actual hacer
A[j + 1] A[j]
j j1
A[j + 1] actual {actual esta ahora en el lugar correcto}
Se ilustra un ejemplo de la ejecucion del algoritmo de ordenamiento en la
figura 3.4. La parte ordenada del arreglo esta en blanco, y el siguiente elemento
a ser insertado esta en azul. Este se encuentra guardado en la variable cur.
Cada renglon corresponde a una iteracion del ciclo externo, y cada copia del
arreglo en un renglon corresponde a una iteracion del ciclo interno. Cada
comparacion se muestra con un arco. Tambien se indica si la comparacion
dio un movimiento o no.

Descripci
on de inserci
on ordenada con Java

La clase OrdenaArreglo tiene el metodo estatico insercionOrdenada()


el cual codifica el algoritmo de insercion ordenada. El codigo proporcionado
es para el caso especial cuando A es un arreglo de caracteres, referenciado
por a.
96 Arreglos, listas enlazadas y recurrencia

Figura 3.4: Ejecuci


on del algoritmo de ordenamiento de un arreglo de 8 caracteres.

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 2 car a cter en a
5 char actual = a [ i ]; // car a cter actual a ser insertado
6 int j = i -1; // se inicia con la celda izquierda de i
7 while (j >=0 && a [ j ] > actual ) // mientras a [ j ] no est e ordenado con actual
8 a [ j +1 ] = a [ j - - ]; // mover a [ j ] a la derecha y decrementar j
9 a [ j +1 ] = actual ; // este es el lugar correcto para actual
10 }
11 }

Listado 3.5: Metodo Java para ordenar un arreglo de caracteres usando el algoritmo
inserci
on ordenada
3.2 Arreglos 97

3.2.3. M
etodos para arreglos y n
umeros aleatorios
Como los arreglos son muy importantes, Java proporciona un determinado
n
umero de metodos incorporados para realizar tareas comunes en arreglos.
Estos metodos son estaticos en la clase java.util.Arrays. Esto es, ellos
estan asociados con la propia clase, y no con alguna instancia particular de la
clase.

M
etodos u
tiles de Arrays
Se listan a continuacion algunos metodos simples de la clase java.util.-
Arrays que no requieren explicacion adicional:

equals(A, B):regresa verdadero si y solo si el arreglo A y el


arreglo B son iguales. Se considera que dos arreglos
son iguales si estos tienen el mismo n umero de
elementos y cada par correspondiente de elementos
en los dos arreglos son iguales. Esto es, A y B
tienen los mismos elementos en el mismo orden.
fill(A, x):guarda el elemento x en cada celda del arreglo A.
sort(A):ordena el arreglo A usando el ordenamiento natural
de sus elementos. Para ordenar usa el algoritmo de
ordenamiento rapido, el cual es mucho mas rapido
que el de insercion ordenada.
toString(A):devuelve un String representante del arreglo A.

Generaci
on de n
umeros pseudoaleatorios
Otra capacidad incorporada en Java es la habilidad para generar n umeros
pseudoaleatorios, los cuales son utiles cuando se prueban programas que usan
arreglos. Estos numeros son estadsticamente aleatorios, pero no necesaria-
mente aleatorios verdaderos. En particular, Java tiene la clase incorporada,
java.util.Random, cuyas instancias son generadores de n umeros pseu-
doaleatorios. Las secuencias no son actualmente aleatorias porque es posible
predecir el siguiente numero en la secuencia dada la lista de n umeros an-
teriores. Un generador de n umeros pseudoaletorios popular para generar el
siguiente n
umero, sig, a parte del n umero actual, actual, es:

sig = (a*actual + b) % n;
98 Arreglos, listas enlazadas y recurrencia

donde a, b, y n son enteros escogidos apropiadamente. La secuencia generada


se puede mostrar que es uniforme estadsticamente, lo cual esta bien para la
mayora de las aplicaciones que requieran numeros aleatorios, como los juegos.
Para aplicaciones donde se requieran secuencias aleatorias no predecibles,
como en configuraciones de seguridad de computadoras, una formula como la
anterior no debe ser usada. Se debe muestrear de una fuente que sea aleatoria,
como la estatica de radio proveniente del espacio exterior.
Como el siguiente n umero en un generador pseudoaleatorio es determinado
por el numero previo, entonces se requiere que el generador tenga un valor
para iniciar, el cual es llamado su semilla. La secuencia de n
umeros generados
por una semilla dada siempre sera la misma. La semilla para una instancia
de la clase java.util.Random puede ser puesta en su constructor o con el
metodo setSeed().
Un truco para obtener una secuencia diferente cada vez que una aplicacion
es ejecutada es usar una semilla que sera diferente para cada ejecucion. Por
ejemplo, se podra usar alg un tiempo ingresado por el usuario, o se podra
poner la semilla al tiempo actual en milisegundos desde el 1 de enero de
1970, dado por el metodo System.currentTimeMillis().
Algunos de los metodos incluidos en la clase java.util.Random son los
siguientes:

nextBoolean():regresa el siguiente valor boolean pseudoaletario.


nextFloat():regresa el siguiente valor float pseudoaletario,
entre 0.0 y 1.0.
nextInt():regresa el siguiente valor int pseudoaletario.
nextInt(n):regresa el siguiente valor int pseudoaletario en el
rango [0, n).
setSeed(s):pone la semilla de este generador de n
umeros pseu-
doaleatorios con el entero largo s.

Ejemplo con n
umeros pseudoaleatorios
Se proporciona la aplicacion PruebaArreglo, listado 3.6, para mostrar el
uso de las clase Arrays y Random del paquete java.util
Una posible salida de esta aplicacion es:

arreglos iguales antes de ordenar: true


arreglos iguales despu
es de ordenar: false
ant = [49, 41, 8, 10, 48, 87, 52, 2, 97, 81]
3.2 Arreglos 99

1 import java . util . Arrays ;


2 import java . util . Random ;
3 /* * Programa que muestra algunos usos de los arreglos */
4 public class PruebaArreglo {
5 public static void main ( String [] args ) {
6 int num [] = new int [10];
7 Random rand = new Random (); // un generador de n u meros p s e u d o a l e a t o r i o s
8 rand . setSeed ( System . c u r r e n t T i m e M i l l i s ()); // tiempo actual como semilla
9 // llenar el arreglo num con n u meros p s e u d o a l e a t o ri o s entre 0 y 99 , incl .
10 for ( int i = 0; i < num . length ; i ++)
11 num [ i ] = rand . nextInt (100); // siguiente numero p se ud oa l ea to ri o
12 int [] ant = ( int []) num . clone (); // clonar arreglo num
13 System . out . println ( " arreglos iguales antes de ordenar : " +
14 Arrays . equals ( ant , num ));
15 Arrays . sort ( num ); // ordenar arreglo num ( ant esta sin cambios )
16 System . out . println ( " arreglos iguales despu e s de ordenar : " +
17 Arrays . equals ( ant , num ));
18 System . out . println ( " ant = " + Arrays . toString ( ant ));
19 System . out . println ( " num = " + Arrays . toString ( num ));
20 }
21 }

Listado 3.6: Aplicacion PruebaArreglo usando varios metodos incorporados en


Java

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

Por cierto, hay una leve posibilidad de que los arreglos ant y num per-
manezcan igual, a un despues de que num sea ordenado, es decir, si num ya
esta ordenado antes de que sea clonado. Pero la probabilidad de que esto
ocurra es menor que uno en cuatro millones, as que es improbable que suceda
en unos cuantos miles de pruebas, per.

Criptografa simple con cadenas y arreglos de caracteres


Una de las aplicaciones primarias de los arreglos es la representacion de
String. Los objetos cadena son usualmente representados internamente como
un arreglo de caracteres. A un si las cadenas pudieran estar representadas de
otra forma, hay una relacion natural entre cadenas y arreglos de caracteres
ambos usan ndices para referirse a sus caracteres. Gracias a esta relacion,
Java hace facil la creacion de objetos String desde arreglos de caracteres y
viceversa. Para crear un objeto String de un arreglo de caracteres A, se usa
la expresion,

new String(A)
100 Arreglos, listas enlazadas y recurrencia

Esto es, uno de los constructores de la clase String toma un arreglo de


caracteres como su argumento y regresa una cadena teniendo los mismos
caracteres en el mismo orden como en el arreglo. De igual forma, dado un
String S, se puede crear un arreglo de caracteres de la representacion de S
usando la expresion,

S.toCharArray()

es decir, la clase String tiene un metodo, toCharArray(), el cual regresa


un arreglo, del tipo char[] con los mismos caracteres de S.

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 texto cifrado. Asimismo,
la criptografa tambien estudia las formas correspondientes de realizar el
descifrado, el cual toma un texto cifrado y lo convierte de regreso en el texto
plano original.
Es discutible que el primer esquema de encriptamiento es el cifrador de
Cesar, el cual es nombrado despues de que Julio Cesar, uso 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.

Usar caracteres como ndices de arreglos


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

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

donde mod es el operador m odulo, el cual regresa el residuo despues de


realizar una division entera. El operador se denota con % en Java, y es el
operador que se requiere para dar la vuelta en el final del alfabeto. Para el 26
el modulo es 0, para el 27 es 1, y el 28 es 2. El algoritmo de desciframiento
para el cifrador de Cesar es exactamente lo opuestose reemplaza cada letra
con una que esta tres lugares antes que esta, con regreso para la A, B y C.
Se puede capturar esta regla de reemplazo usando arreglos para encriptar
y descifrar. Porque cada caracter en Java esta actualmente guardado como un
numerosu valor Unicodese pueden usar letras como ndices de arreglos.
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 mayusculas. 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, descrifrar, puede representar la
regla de desciframiento, por lo que descifrar[i] es la letra que reemplaza
la letra n
umero i.
Arreglo encriptar
D E F G H I J K L M N ... X Y Z A B C
0 1 2 3 4 5 6 7 8 9 10 ... 20 21 22 23 24 25

El arreglo de encriptamiento para el cifrador Cesar quedara como se


muestra previamente. Por ejemplo, para codificar el caracter J, este es
usado como ndice, haciendo J - A, que es calculado por Java como 74
- 65 = 9, (los valores son sus codigos Unicode). Luego al acceder el arreglo
con ese ndice se obtiene el caracter M.
La aplicacion Cesar, listado 3.7, tiene un cifrador de Cesar simple y
completo usando la aproximacion anterior. Ademas realiza las conversiones
entre cadenas y arreglos de caracteres.
Cuando se ejecuta este programa, se obtiene la siguiente salida:

Orden de Encriptamiento = DEFGHIJKLMNOPQRSTUVWXYZABC


Orden de Descifrado = XYZABCDEFGHIJKLMNOPQRSTUVW
HVWUXFWXUDV GH GDWRV; DUUHJORV B OLVWDV OLJDGDV.
ESTRUCTURAS DE DATOS; ARREGLOS Y LISTAS LIGADAS.
102 Arreglos, listas enlazadas y recurrencia

1 /* * Clase para encriptar y descifrar con el cifrador de C e sar . */


2 public class Cesar {
3 public static final char [] abc = { A , B , C , D , E , F , G , H , I ,
4 J , K , L , M , N , O , P , Q , R , S , T , U , V , W , X , Y , Z };
5 protected char [] encriptar = new char [ abc . length ]; // Arreglo encrip tamiento
6 protected char [] descifrar = new char [ abc . length ]; // Arreglo descif ramiento
7 /* * Constructor para inicializar los arreglos de encri ptamien to y
8 * descif ramiento */
9 public Cesar () {
10 for ( int i =0; i < abc . length ; i ++)
11 encriptar [ i ] = abc [( i + 3) % abc . length ]; // rotar alfabeto 3 lugares
12 for ( int i =0; i < abc . length ; i ++)
13 descifrar [ encriptar [ i ] - A ] = abc [ i ]; // descifrar inverso a encriptar
14 }
15 /* * M e todo de encri ptamien to */
16 public String encriptar ( String secreto ) {
17 char [] mensj = secreto . toCharArray (); // arreglo mensaje
18 for ( int i =0; i < mensj . length ; i ++) // ciclo de e ncripta miento
19 if ( Character . isUpperCase ( mensj [ i ])) // se tiene que cambiar letra
20 mensj [ i ] = encriptar [ mensj [ i ] - A ]; // usar letra como ndice
21 return new String ( mensj );
22 }
23 /* * M e todo de desci framien to */
24 public String descifrar ( String secreto ) {
25 char [] mensj = secreto . toCharArray (); // arreglo mensaje
26 for ( int i =0; i < mensj . length ; i ++) // ciclo desc iframien to
27 if ( Character . isUpperCase ( mensj [ i ])) // se tiene letra a cambiar
28 mensj [ i ] = descifrar [ mensj [ i ] - A ]; // usar letra como ndice
29 return new String ( mensj );
30 }
31 /* * Metodo main para probar el cifrador de C e sar */
32 public static void main ( String [] args ) {
33 Cesar cifrador = new Cesar (); // Crear un objeto cifrado de C e sar
34 System . out . println ( " Orden de Encri ptamien to = " +
35 new String ( cifrador . encriptar ));
36 System . out . println ( " Orden de Descifrado = " +
37 new String ( cifrador . descifrar ));
38 String secreto = " ESTRUCTURAS DE DATOS ; ARREGLOS Y LISTAS LIGADAS . " ;
39 secreto = cifrador . encriptar ( secreto );
40 System . out . println ( secreto ); // el texto cifrado
41 secreto = cifrador . descifrar ( secreto );
42 System . out . println ( secreto ); // deber a ser texto plano
43 }
44 }

Listado 3.7: Una clase simple y completa de Java para el cifrador de Cesar
3.2 Arreglos 103

1 2 3 4 5 6 7
0 1 2 3 4 5 6 7
1 2 3 4 5 6 7 8
2 3 4 5 6 7 8 9
3 4 5 6 7 8 9 0
4 5 6 7 8 9 0 1

Cuadro 3.2: Representacion del arreglo de enteros bidimensional, Y, con 5 renglones


y 7 columnas. El valor de Y[2][4] es 8.

3.2.4. Arreglos bidimensionales y juegos de posici


on
Varios juegos de computadora, sean juegos de estrategia, de simulacion,
o de conflicto de primera persona, usan un tablero bidimensional. Los
programas que manejan juegos de posicion requieren una forma de representar
objetos en un espacio bidimensional. Una forma natural de hacerlo es con un
arreglo bidimensional, donde se usan dos ndices, por ejemplo i y j, para
referirse a las celdas en el arreglo. El primer ndice se refiere al n
umero del
renglon y el segundo al n umero de la columna. Dado tal arreglo se pueden
mantener tableros de juego bidimensionales, as como realizar otros tipos de
calculos involucrando datos que estan guardados en renglones y columnas.
Los arreglos en Java son unidimensionales; se puede usar un solo ndice
para acceder cada celda de un arreglo. Sin embargo, hay una forma como se
pueden definir arreglos bidimensionales en Javase puede crear un arreglo
bidimensional como un arreglo de arreglos. Esto es, se puede definir un arreglo
de dos dimensiones para que sea un arreglo con cada una de sus celdas siendo
otro arreglo. Tal arreglo de dos dimensiones es a veces llamado una matriz.
En Java, se declara un arreglo de dos dimensiones como se muestra en el
siguiente ejemplo:

int[][] Y = new int[5][7];

La sentencia crea una arreglo de arreglos bidimensional, Y, el cual es


5 7, teniendo cinco renglones y siete columnas. Es decir, Y es un arreglo
de longitud cinco tal que cada elemento de Y es un arreglo de longitud de
siete enteros. Ver cuadro 3.2. Lo siguiente podra entonces ser usos validos
del arreglo Y y siendo i y j variables tipo int:
104 Arreglos, listas enlazadas y recurrencia

Y[i][i+1] = Y[i][i] + 3;
i = a.length; // i es 5
j = Y[4].length; // j es 7

Se revisa enseguida una aplicacion de un arreglo bidimensional implemen-


tando un juego simple posicional, para revisar algunos detalles del uso de
arreglos bidimensionales.

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 alg un jugador 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 de 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, un cero indica celda
vaca, un uno indica una X, y un menos uno indica O. Esta codificacion
permite tener una forma simple de probar si en una configuracion del tablero
es una victoria para X o para O, sabiendo si los valores de un renglon, columna
o diagonal suman -3 o 3. Se ilustra lo anterior en la figura 3.5.

Figura 3.5: Una ilustracion del juego del gato a la izquierda, y su representacion
empleando el arreglo tablero.
3.2 Arreglos 105

La clase Gato, listado 3.8, modela el tablero del gato para dos jugadores.
El codigo solo mantiene el tablero del gato y registra 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 protected static final int X = 1 , O = -1; // jugadores
4 protected static final int VACIA = 0; // celda vac
a
5 protected int tablero [][] = new int [3][3]; // tablero
6 protected int jugador ; // jugador actual
7 /* * Constructor */
8 public Gato () { li mpiarTa blero (); }
9 /* * Limpiar el tablero */
10 public void li mpiarTa blero () {
11 for ( int i = 0; i < 3; i ++)
12 for ( int j = 0; j < 3; j ++)
13 tablero [ i ][ j ] = VACIA ; // cada celda deber a estar vac
a
14 jugador = X ; // el primer jugador es X
15 }
16 /* * Poner una marca X u O en la posici o n i , j */
17 public void ponerMarca ( int i , int j ) throws I l l e g a l A r g u m e n t E x c e p t i o n {
18 if ( ( i < 0) || ( i > 2) || ( j < 0) || ( j > 2) )
19 throw new I l l e g a l A r g u m e n t E x c e p t i o n ( " Posici
o n invalida en el tablero " );
20 if ( tablero [ i ][ j ] != VACIA )
21 throw new I l l e g a l A r g u m e n t E x c e p t i o n ( " Posici
o n del tablero ocupada " );
22 tablero [ i ][ j ] = jugador ; // colocar la marca para el jugador actual
23 jugador = - jugador ; // intercambiar jugadores , se usa O es -X
24 }
25 /* * Revisar si la configuraci o n del tablero es ganadora para un jugador dado */
26 public boolean esGanador ( int marca ) {
27 return (( tablero [0][0] + tablero [0][1] + tablero [0][2] == marca *3) // ren . 0
28 || ( tablero [1][0] + tablero [1][1] + tablero [1][2] == marca *3) // ren . 1
29 || ( tablero [2][0] + tablero [2][1] + tablero [2][2] == marca *3) // ren . 2
30 || ( tablero [0][0] + tablero [1][0] + tablero [2][0] == marca *3) // col . 0
31 || ( tablero [0][1] + tablero [1][1] + tablero [2][1] == marca *3) // col . 1
32 || ( tablero [0][2] + tablero [1][2] + tablero [2][2] == marca *3) // col . 2
33 || ( tablero [0][0] + tablero [1][1] + tablero [2][2] == marca *3) // diag .
34 || ( tablero [2][0] + tablero [1][1] + tablero [0][2] == marca *3)); // diag .
35 }
36 /* * Regresar el jugador ganador o 0 para indicar empate */
37 public int ganador () {
38 if ( esGanador ( X ))
39 return ( X );
40 else if ( esGanador ( O ))
41 return ( O );
42 else
43 return (0);
44 }
45 /* * Regresar una cadena simple mostrando el tablero actual */
46 public String toString () {
47 String s = " " ;
48 for ( int i =0; i <3; i ++) {
49 for ( int j =0; j <3; j ++) {
50 switch ( tablero [ i ][ j ]) {
51 case X : s += " X " ; break ;
52 case O : s += " O " ; break ;
106 Arreglos, listas enlazadas y recurrencia

53 case VACIA : s += " " ; break ;


54 }
55 if ( j < 2) s += " | " ; // l
mite de columna
56 }
57 if ( i < 2) s += " \n - - - - -\ n " ; // l
mite de renglon
58 }
59 return s ;
60 }
61 /* * Ejecuci o n de prueba de un juego simple */
62 public static void main ( String [] args ) {
63 Gato juego = new Gato ();
64 /* mueve X : */ /* mueve O : */
65 juego . ponerMarca (1 ,1); juego . ponerMarca (0 ,2);
66 juego . ponerMarca (2 ,2); juego . ponerMarca (0 ,0);
67 juego . ponerMarca (0 ,1); juego . ponerMarca (2 ,1);
68 juego . ponerMarca (1 ,2); juego . ponerMarca (1 ,0);
69 juego . ponerMarca (2 ,0);
70 System . out . println ( juego . toString () );
71 int jugador Ganador = juego . ganador ();
72 if ( jugadorG anador != 0)
73 System . out . println ( jug adorGana dor + " gana " );
74 else
75 System . out . println ( " Empate " );
76 }
77 }

Listado 3.8: Clase Gato para simular el tablero del juego del gato.

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

O|X|O
-----
O|X|X
-----
X|O|X
Empate

Figura 3.6: Salida del ejemplo del juego del gato

M
etodos para arreglos de arreglos
Se haba se
nalado en la seccion 1.5, que una referencia arreglo apunta a
un objeto arreglo. Si se tienen dos arreglos bidimensionales, A y B, con ambos
3.2 Arreglos 107

teniendo las mismas entradas, se podra decir que el arreglo A es igual a B.


Pero los arreglos unidimensionales que forman los renglones de A y B estan
guardados en localidades de memoria diferentes, a pesar de que tengan el
mismo contenido interno. Por lo tanto, una llamada al metodo java.util.-
Arrays.equals(A,B) devolvera false en este caso. La razon de lo anterior
es porque el metodo equals() realiza una igualdad superficial, es decir,
prueba solo si los elementos correspondientes en A y B son iguales entre ellos
usando solo una nocion simple de igualdad. La regla igualdad simple dice
que dos variables tipo base son iguales si estas tienen el mismo valor y dos
referencias objetos son iguales si ambas se refieren al mismo objeto. Cuando
se quiere realizar una igualdad profunda para arreglos de objetos, como los
arreglos bidimensionales, la clase java.util.Arrays proporciona el siguiente
metodo:
deepEquals(A,B) Devuelve si A y B son iguales profundamen-
te. Seran profundamente iguales A y B si
estos tienen la misma cantidad de elemen-
tos y los dos elementos A[i] y B[i] en cada
pareja de elementos correspondientes son
por s mismos iguales en el sentido simple,
son arreglos del mismo tipo primitivo tal
que Arrays.equals(A[i],B[i]) podran
devolver true, o son arreglos de referen-
cia objeto tal que A[i] y B[i] son iguales
profundamente.
Ademas de la igualdad profunda, se quiere un metodo profundo para
convertir un arreglo bidimensional o un arreglo de referencias objeto en una
cadena. Tal metodo es proporcionado tambien por la clase java.util.Arrays
deepToString(A) Devuelve una cadena representando los
contenidos de A. Para convertir un ele-
mento A[i] a una cadena, si A[i] es
una referencia arreglo, se llama Arrays.-
deepToString(A[i]) es llamado, de otro
modo el metodo String.valueOf(A[i])
es usado.
No esta disponible un metodo para hacer una copia identica de un arreglo
bidimensional, por lo tanto, si se quiere hacer lo anterior, se necesita hacer una
llamada a A[i].clone() o java.util.Arrays.copyOf(A[i],A[i].length)
para cada renglon, A[i], de A.
108 Arreglos, listas enlazadas y recurrencia

3.3. Listas simples enlazadas


Los arreglos son elegantes y simples para guardar cosas en un determinado
orden, pero tienen el inconveniente de no ser muy adaptables, ya que se tiene
que fijar el tamano 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 3.7.

Figura 3.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 .

Podra parecer extra


no tener un nodo que referencia a otro nodo, pero tal
esquema trabaja facilmente. La referencia sig dentro de un nodo puede ser
vista como un enlace o apuntador a otro nodo. De igual nodo, moverse de un
nodo a otro siguiendo una referencia a sig es conocida como salto de enlace
o salto de apuntador. El primer nodo y el u ltimo son usualmente llamados la
cabeza y la cola de la lista, respectivamente. As, se puede saltar a traves de
la lista iniciando en la cabeza y terminando en la cola. Se puede identificar la
cola como el nodo que tenga la referencia sig como null, la cual indica el
final de la lista. Una lista enlazada definida de esta forma es conocida como
una lista simple enlazada.
Como en un arreglo, una lista simple enlazada guarda sus elementos en
un cierto orden. Este orden esta determinado por la cadenas de enlaces sig
yendo desde cada nodo a su sucesor en la lista. A diferencia de un arreglo, una
lista simple enlazada no tiene un tama no fijo predeterminado, y usa espacio
proporcional al n umero de sus elementos. Asimismo, no se emplean n umeros
ndices para los nodos en una lista enlazada. Por lo tanto, no se puede decir
solo por examinar un nodo si este es el segundo, quinto u otro nodo en la
lista.
3.3 Listas simples enlazadas 109

Implementaci
on de una lista simple enlazada
Para implementar una lista simple enlazada, se define la clase Nodo, listado
3.9, 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, listado 3.10, para definir
la lista enlazada actual. Esta clase guarda una referencia al nodo cabeza y
una variable guarda el total de nodos de la lista.

1 /* * Nodo de una lista simple enlazada de cadenas . */


2 public class Nodo {
3 private String elemento ; // Los elementos son cadenas de caracteres
4 private Nodo sig ;
5 /* * Se crea un nodo con el elemento dado y el nodo sig . */
6 public Nodo ( String e , Nodo n ) {
7 elemento = e ;
8 sig = n ;
9 }
10 /* * Regresa el elemento de este nodo . */
11 public String getElemento () { return elemento ; }
12 /* * Regresa el nodo siguiente de este nodo . */
13 public Nodo getSig () { return sig ; }
14 // m
e todos modificadores
15 /* * Pone el elemento de este nodo . */
16 public void setElemento ( String nvoElem ) { elemento = nvoElem ; }
17 /* * Pone el nodo sig de este nodo . */
18 public void setSig ( Nodo nvoSig ) { sig = nvoSig ; }
19 }

Listado 3.9: Implementaci


on de un nodo de una lista simple enlazada

1 /* * Lista simple enlazada . */


2 public class ListaSimple {
3 protected Nodo cabeza ; // nodo cabeza de la lista
4 protected long tam ; // cantidad de nodos en la lista
5 /* * constructor por defecto para crear una lista vac a */
6 public ListaSimple () {
7 cabeza = null ;
8 tam = 0;
9 }
10 // ... m e
todos de actualizacion y b
u squeda deber
an ir aqu

11 }

Listado 3.10: Implementaci


on parcial de una lista simple enlazada
110 Arreglos, listas enlazadas y recurrencia

3.3.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 3.8.

Figura 3.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.

La idea principal es crear un nodo nuevo, poner su enlace sig para que se
refiera al mismo objeto que la cabeza, y luego poner la cabeza apuntando al
nuevo nodo.
Se muestra enseguida el algoritmo para insertar un nodo nuevo v al inicio
de una lista simple enlazada. Observar que este metodo trabaja aun si la lista
esta vaca. Observar que se ha puesto el apuntador sig 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 }
3.3 Listas simples enlazadas 111

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

Figura 3.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).

En este caso, se crea un nuevo nodo, se asigna su referencia sig para que
apunte al objeto null, se pone la referencia sig de la cola para 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 sig 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 }
112 Arreglos, listas enlazadas y recurrencia

3.3.2. Quitar un elemento de una lista simple enlazada


La operacion contraria de insertar un nuevo elemento en la cabeza de una
lista enlazada es quitar un elemento en la cabeza. Esta operacion es ilustrada
en la figura 3.10.

Figura 3.10: Remocion de un elemento en la cabeza de una lista simple enlazada:


(a) antes de la remocion; (b) desligando el viejo nodo; (c) despues de la remoci
on.

En el algoritmo removerPrimero se muestra la forma de realizar la


remocion del elemento en la cabeza de la lista.
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 borrar facilmente el nodo cola de una
lista simple enlazada. A
un si se tiene una referencia cola directamente al
3.4 Listas doblemente enlazadas 113

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. Listas doblemente enlazadas


Como se comentaba en la seccion previa, quitar un elemento de la cola de
una lista simple enlazada no es facil. Ademas, el quitar cualquier otro nodo de
la lista diferente de la cabeza de una lista simple enlazada es consumidor de
tiempo, ya que no se tiene una forma rapida de acceder al nodo que se desea
quitar. Por otra parte, hay varias aplicaciones donde no se tiene un acceso
rapido tal como el acceso al nodo predecesor. Para tales aplicaciones, sera
bueno contar con una forma de ir en ambas direcciones en una lista enlazada.
Hay un tipo de lista enlazada que permite ir en ambas direccioneshacia
adelante y hacia atrasen una lista enlazada. Esta es la lista doblemente
enlazada. Tal lista permite una gran variedad de operaciones rapidas de
actualizacion, incluyendo la insercion y el borrado en ambos extremos, y en el
centro. Un nodo en una lista doblemente enlazada guarda dos referenciasun
enlace sig, el cual apunta al siguiente nodo en la lista, y un enlace prev, el
cual apunta al nodo previo en la lista.
Una implementacion de un nodo de una lista doblemente enlazada se
muestra en la clase NodoD, listado 3.11, donde se asume que los elementos son
cadenas de caracteres.

Centinelas cabeza y cola


Para simplificar la programacion, es conveniente agregar nodos especiales
en los extremos de lista doblemente enlazada: un nodo cabeza (header )
justo antes de la cabeza de la lista, y un nodo cola (trailer ) justo despues
de la cola de la lista. Estos nodos falsos o centinelas no guardan ning un
elemento. La cabeza tiene una referencia sig valida pero una referencia prev
nula, mientras cola tiene una referencia prev valida pero una referencia sig
nula. Una lista doblemente enlazada con estos centinelas se muestra en la
figura 3.11.
114 Arreglos, listas enlazadas y recurrencia

1 /* * Nodo de una lista doblemente enlazada de una lista de cadenas */


2 public class NodoD {
3 protected String elemento ; // Nodo guarda un elemento String
4 protected NodoD sig , prev ; // Apuntadores a nodos siguiente y previo
5 /* * Constructor que crea un nodo con los campos dados */
6 public NodoD ( String e , NodoD p , NodoD s ) {
7 elemento = e ;
8 prev = p ;
9 sig = s ;
10 }
11 /* * Regresa el elemento de este nodo */
12 public String getElemento () { return elemento ; }
13 /* * Regresa el nodo previo de este nodo */
14 public NodoD getPrev () { return prev ; }
15 /* * Regresa el nodo siguiente de este nodo */
16 public NodoD getSig () { return sig ; }
17 /* * Pone el elemento de este nodo */
18 public void setElemento ( String nvoElem ) { elemento = nvoElem ; }
19 /* * Pone el nodo previo de este nodo */
20 public void setPrev ( NodoD nvoPrev ) { prev = nvoPrev ; }
21 /* * Pone el nodo siguiente de este nodo */
22 public void setSig ( NodoD nvoSig ) { sig = nvoSig ; }
23 }

Listado 3.11: Clase NodoD representando un nodo de una lista doblemente enlazada
que guarda una cadena de caracteres.

Figura 3.11: Una lista doblemente enlazada con centinelas, cabeza y cola, marcando
el final de la lista.
3.4 Listas doblemente enlazadas 115

Un objeto lista enlazada podra necesitar guardar referencias a estos dos


centinelas y un contador tam que guarde el n umero de elementos, sin contar
los centinelas, en la lista.
Insertar o remover elementos en cualquier extremo de la lista doblemente
enlazada se hace directo. Ademas, el enlace prev elimina la necesidad de
recorrer la lista para obtener el nodo que esta antes de la cola. Se muestran
los detalles de la operacion remover en la cola de la lista doblemente enlazada
en la figura 3.12.

Figura 3.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.

Los detalles de la remocion en la cola de la lista doblemente enlazada se


muestran en el algoritmo removerUltimo.

Algoritmo removerUltimo():
si tam = 0 entonces
Indicar un error: la lista esta vaca.
v cola.getP rev() { ultimo nodo }
u v.getP rev() { nodo anterior al ultimo nodo }
cola.setP rev(u)
u.setSig(cola)
v.setP rev(null)
v.setSig(null)
tam tam 1 { decrementar la cuenta de nodos }
De igual modo, se puede facilmente hacer una insercion de un nuevo
116 Arreglos, listas enlazadas y recurrencia

elemento al inicio de una lista doblemente enlazada, como se muestra en la


figura 3.13.

Figura 3.13: Inserci


on de un elemento en el frente: (a) previo; (b) final.

La implementacion de la insercion de un nuevo nodo v al inicio, de acuerdo


al algoritmo agregarInicio, donde la variable tam guarda la cantidad de
elementos en la lista. Este algoritmo tambien trabaja con una lista vaca.
Algoritmo agregarInicio(v):
w cabeza.getSig() { primer nodo }
v.setSig(w)
v.setP rev(cabeza)
w.setP rev(v)
cabeza.setSig(v)
tam tam + 1

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

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


3.4 Listas doblemente enlazadas 117

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

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

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

Este metodo esta dado en detalle en el algoritmo agregarDespue s y es


ilustrado en la figura 3.14. Recordando el uso de los centinelas cabeza y cola,
este algoritmo trabaja a un si v es el nodo cola, el nodo que esta justo antes
del centinela cola.

Figura 3.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.

Algoritmo agregarDespues(v, z):


w v.getSig() { nodo despues de v }
z.setP rev(v) { enlazar z a su predecesor, v }
z.setSig(w) { enlazar z a su sucesor, w }
w.setP rev(z) { enlazar w a su nuevo predecesor, z }
v.setSig(z) { enlazar v a su nuevo sucesor, z }
tam tam + 1

3.4.2. Remover en el centro de una lista doblemente


enlazada
De igual modo, es facil remover un nodo v intermedio de una lista do-
blemente enlazada. Se acceden los nodos u y w a ambos lados de v usando
118 Arreglos, listas enlazadas y recurrencia

sus metodos getPrev y getSig, estos nodos deberan existir, ya que se estan
usando centinelas. Para quitar el nodo v, se tiene que hacer que u y w se
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 3.15.

Figura 3.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 }
3.4 Listas doblemente enlazadas 119

3.4.3. Una implementaci


on de una lista doblemente en-
lazada
La clase ListaDoble, listado 3.12, muestra una implementacion de una
lista doblemente enlazada con nodos que guardan un String como elementos.
Enseguida se dan un conjunto de observaciones de la clase ListaDoble.

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.

Se puede usar la clase ListaDoble para una lista doblemente enlazada


de objetos String solamente. Para construir una lista de otros tipos, se
puede usar una declaracion generica.

Los metodos getPrimero() y getUltimo() dan acceso directo al primer


nodo y el u
ltimo en la lista.

Los metodos getPrev() y getSig() permiten recorrer la lista.

Los metodos tienePrev() y tieneSig() detectan los lmites de la lista.

Los metodos agregarInicio() y agregarFinal() agregan un nuevo


nodo al inicio y al final de la lista, respectivamente.

Los metodos agregarAntes() y agregarDespues() agregan un nodo


antes y despues de un nodo existente, respectivamente.

Teniendo solamente un solo metodo para borrar, remover(), no es ac-


tualmente una restriccion, ya que se puede remover al inicio o al final de
una lista doblemente enlazada L ejecutando L.remover(L.getPrimero())
o L.remover(L.getUltimo()), respectivamente.

El metodo toString() que convierte la lista entera en una cadena es


u
til para propositos de prueba y depuracion.

1 /* * Lista doblemente enlazada de nodos tipo NodoD para cadenas . */


2 public class ListaDoble {
3 protected int tam ; // cantidad de elementos
4 protected NodoD cabeza , cola ; // centinelas
5 /* * Constructor que crea una lista vac
a */
6 public ListaDoble () {
7 tam = 0;
120 Arreglos, listas enlazadas y recurrencia

8 cabeza = new NodoD ( null , null , null ); // crer cabeza


9 cola = new NodoD ( null , cabeza , null ); // crear terminaci o
n
10 cabeza . setSig ( cola ); // cabeza apunta a terminaci o
n
11 }
12 /* * Regresa la cantidad de elementos en la lista */
13 public int tam () { return tam ; }
14 /* * Indica si la lista esta vac a */
15 public boolean estaVacia () { return ( tam == 0); }
16 /* * Regresa el primer nodo de la lista */
17 public NodoD getPrimero () throws I l l e g a l S t a t e E x c e p t i o n {
18 if ( estaVacia ()) throw new I l l e g a l S t a t e E x c e p t i o n ( " La lista esta vac a " );
19 return cabeza . getSig ();
20 }
21 /* * Regresa el u ltimo nodo de la lista */
22 public NodoD getUltimo () throws I l l e g a l S t a t e E x c e p t i o n {
23 if ( estaVacia ()) throw new I l l e g a l S t a t e E x c e p t i o n ( " La lista esta vac a " );
24 return cola . getPrev ();
25 }
26 /* * Regresa el nodo anterior al nodo v dado . Un error ocurre si v
27 * es la cabeza */
28 public NodoD getPrev ( NodoD v ) throws I l l e g a l A r g u m e n t E x c e p t i o n {
29 if ( v == cabeza ) throw new I l l e g a l A r g u m e n t E x c e p t i o n
30 ( " No se puede mover hacia atr a s de la cabeza de la lista " );
31 return v . getPrev ();
32 }
33 /* * Regresa el nodo siguiente al nodo v dado . Un error ocurre si v
34 * es la cola */
35 public NodoD getSig ( NodoD v ) throws I l l e g a l A r g u m e n t E x c e p t i o n {
36 if ( v == cola ) throw new I l l e g a l A r g u m e n t E x c e p t i o n
37 ( " No se puede mover hacia adelante de la terminaci o n de la lista " );
38 return v . getSig ();
39 }
40 /* * Inserta el nodo z dado antes del nodo v dado . Un error
41 * ocurre si v es la cabeza */
42 public void agregarAntes ( NodoD v , NodoD z ) throws I l l e g a l A r g u m e n t E x c e p t i o n {
43 NodoD u = getPrev ( v ); // podr a lanzar un I l l e g a l A r g u m e n t E x c e p t i o n
44 z . setPrev ( u );
45 z . setSig ( v );
46 v . setPrev ( z );
47 u . setSig ( z );
48 tam ++;
49 }
50 /* * Inserta el nodo z dado despues del nodo v dado . Un error
51 * ocurre si v es la cabeza */
52 public void ag regarDes pues ( NodoD v , NodoD z ) {
53 NodoD w = getSig ( v ); // podr a lanzar un I l l e g a l A r g u m e n t E x c e p t i o n
54 z . setPrev ( v );
55 z . setSig ( w );
56 w . setPrev ( z );
57 v . setSig ( z );
58 tam ++;
59 }
60 /* * Inserta el nodo v dado en la cabeza de la lista */
61 public void agregarInicio ( NodoD v ) {
62 agre garDespu es ( cabeza , v );
63 }
64 /* * Inserta el nodo v dado en la cola de la lista */
3.5 Listas circulares y ordenamiento 121

65 public void agregarFinal ( NodoD v ) {


66 agregarAntes ( cola , v );
67 }
68 /* * Quitar el nodo v dado de la lista . Un error ocurre si v es
69 * la cabeza o la cola */
70 public void remover ( NodoD v ) {
71 NodoD u = getPrev ( v ); // podr
a lanzar I l l e g a l A r g u m e n t E x c e p t i o n
72 NodoD w = getSig ( v ); // podr a lanzar I l l e g a l A r g u m e n t E x c e p t i o n
73 // desenlazar el nodo v de la lista
74 w . setPrev ( u );
75 u . setSig ( w );
76 v . setPrev ( null );
77 v . setSig ( null );
78 tam - -;
79 }
80 /* * Regresa si un nodo v dado tiene un nodo previo */
81 public boolean tienePrev ( NodoD v ) { return v != cabeza ; }
82 /* * Regresa si un nodo v dado tiene un nodo siguiente */
83 public boolean tieneSig ( NodoD v ) { return v != cola ; }
84 /* * Regresa una cadena representando la lista */
85 public String toString () {
86 String s = " [ " ;
87 NodoD v = cabeza . getSig ();
88 while ( v != cola ) {
89 s += v . getElemento ();
90 v = v . getSig ();
91 if ( v != cola )
92 s += " ," ;
93 }
94 s += " ] " ;
95 return s ;
96 }
97 }

Listado 3.12: Una clase de lista doblemente enlazada.

3.5. Listas circulares y ordenamiento


En esta seccion se revisan algunas aplicaciones y extensiones de listas
enlazadas.

3.5.1. Lista circular enlazada


El juego de ninos, Pato, Pato, Ganso, es jugado en varias culturas. Una
variacion de la lista simple enlazada, llamada la lista circular enlazada, es
usada para una variedad de aplicaciones involucrando juegos circulares, como
el juego que se comenta. Enseguida se muestra la lista circular y su aplicacion
en un juego circular.
122 Arreglos, listas enlazadas y recurrencia

Una lista circular enlazada tiene el mismo tipo de nodos que una lista
simple enlazada. Esto es, cada nodo en una lista circular enlazada tiene un
apuntador siguiente y una referencia a un elemento. Pero no hay una cabeza
o cola en la lista circular enlazada. En vez de hacer que el apuntador del
u
ltimo nodo sea null, se hace que apunte de regreso al primer nodo. Por lo
tanto, no hay un primer nodo o u ltimo. Si se recorren los nodos de una lista
circular enlazada desde cualquier nodo usando los apuntadores sig, entonces
se hara un ciclo a traves de los nodos.
Aun cuando una lista circular enlazada no tiene inicio o terminacion, no
obstante se necesita que alg un nodo este marcado como especial, el cual
sera llamado cursor. El nodo cursor permite tener un lugar para iniciar si se
requiere recorrer una lista circular enlazada. Y si se recuerda esta posicion
inicial, entonces tambien se puede saber cuando se haya terminado con un
recorrido en la lista circular enlazada, porque es cuando se regresa al nodo
que fue el nodo cursor cuando se inicio.
Se pueden entonces definir algunos metodos simples de actualizacion para
una lista circular enlazada:

agregar(v):inserta un nuevo nodo v inmediatamente despues


del cursor; si la lista esta vaca, entonces v se
convierte en el cursor y su apuntador sig apunta
a el mismo.
remover():borra y regresa el nodo v inmediatamente despues
del cursor (no el propio cursor, a menos que este
sea el unico nodo); si la lista queda vaca, el cursor
es puesto a null.
avanzar():pasa el cursor al siguiente nodo en la lista.

La clase ListaCircular, listado 3.13, muestra una implementacion Java


de una lista circular enlazada, la cual usa la clase Nodo, listado 3.9. Tambien
incluye un metodo toString() para generar una representacion de cadena
de la lista.

Algunas observaciones acerca de la clase ListaCircular


Es un programa simple que puede dar suficiente funcionalidad para simular
juegos circulares, como Pato, Pato, Ganso, como se ve enseguida. No es un
3.5 Listas circulares y ordenamiento 123

1 /* * Lista circularmente enlazada con nodos de tipo Nodo guardando cadenas . */


2 public class ListaCircular {
3 protected Nodo cursor ; // cursor actual
4 protected int tam ; // cantidad de nodos en la lista
5 /* * Constructor que crea una lista vacia */
6 public ListaCircular () { cursor = null ; tam = 0; }
7 /* * Regresa cantidad de nodos */
8 public int tam () { return tam ; }
9 /* * Regresa cursor */
10 public Nodo getCursor () { return cursor ; }
11 /* * Mueve cursor hacia adelante */
12 public void avanzar () { cursor = cursor . getSig (); }
13 /* * Agrega nodo despu e s del cursor */
14 public void agregar ( Nodo nodoNuevo ) {
15 if ( cursor == null ) { // lista vacia ?
16 nodoNuevo . setSig ( nodoNuevo );
17 cursor = nodoNuevo ;
18 }
19 else {
20 nodoNuevo . setSig ( cursor . getSig ());
21 cursor . setSig ( nodoNuevo );
22 }
23 tam ++;
24 }
25 /* * Quita nodo despu e s del cursor */
26 public Nodo remover () {
27 Nodo nodo = cursor . getSig (); // nodo que ser a removido
28 if ( nodo == cursor )
29 cursor = null ; // la lista quedar a vac
a
30 else {
31 cursor . setSig ( nodo . getSig () ); // desenlazar el nodo
32 nodo . setSig ( null );
33 }
34 tam - -;
35 return nodo ;
36 /* * Devuelve una representaci o n String de la lista ,
37 * iniciando desde el cursor */
38 public String toString () {
39 if ( cursor == null ) return " [] " ;
40 String s = " [... " + cursor . getElemento ();
41 Nodo cursorOrig = cursor ;
42 for ( avanzar (); cursorOrig != cursor ; avanzar () )
43 s += " , " + cursor . getElemento ();
44 return s + " ...] " ;
45 }
46 }

Listado 3.13: Una clase de lista circular enlazada con nodos simples.
124 Arreglos, listas enlazadas y recurrencia

programa robusto, sin embargo. En particular, si una lista circular esta vaca,
entonces llamar avanzar o remover en esa lista causara una excepcion.

Pato, Pato, Ganso


En el juego de ninos Pato, Pato, Ganso, un grupo de ni nos se sienta en
crculo. Uno de ellos es ellos es elegido para quedarse parado y este camina
alrededor del crculo por fuera. El ni
no elegido toca a cada ni no en la cabeza,
diciendo Pato o Ganso. Cuando el elegido decide llamar a alguien el
Ganso, el Ganso se levanta y persigue al otro ni no alrededor del crculo.
Si el Ganso no logra tocar al elegido, entonces a el le toca quedarse para la
proxima ronda, sino el elegido lo seguira siendo en la siguiente ronda.
Simular este juego es una aplicacion ideal de una lista circular enlazada.
Los nodos pueden representar a los ni nos que estan sentados en crculo. El
nino elegido puede ser identificado como el ni no que esta sentado despues
del cursor, y puede ser removido del crculo para simular el recorrido alrededor.
Se puede avanzar el cursor con cada Pato que el elegido identifique, lo
cual se puede simular con una decision aleatoria. Una vez que un Ganso
es identificado, se puede remover este nodo de la lista, hacer una seleccion
aleatoria para simular si el Ganso alcanzo al elegido, e insertar el ganador
en la lista. Se puede entonces avanzar el cursor e insertar al ganador y repetir
el proceso, o terminar si esta es la ultima partida del juego.

Lista circular enlazada para simular Pato, Pato, Ganso


La clase arreglos/PatoPatoGanso, listado 3.14, muestra el codigo para
simular el juego Pato, Pato, Ganso.
Se muestra un ejemplo de la salida de la ejecucion del programa Pato,
Pato, Ganso a continuacion:

Los jugadores son: [...Pedro, Alex, Eli, Paco, Gabi, Pepe, Luis, To~
no...]
Alex es el 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 el elegido.
Jugando Pato, Pato, Ganso con: [...Paco, Gabi, Pepe, Luis, To~
no, Pedro, Eli...]
Gabi es un pato.
Pepe es el ganso!
3.5 Listas circulares y ordenamiento 125

1 import java . util . Random ;


2 public class PatoPatoGanso {
3 /* * Simulaci o n de Pato , Pato , Ganso con una lista circular enlazada . */
4 public static void main ( String [] args ) {
5 ListaCircular C = new ListaCircular ();
6 int N = 3; // veces que se jugar a el juego
7 Nodo eleg ; // jugador elegido
8 Nodo ganso ; // el ganso
9 Random rand = new Random ();
10 rand . setSeed ( System . c u r r e n t T i m e M i l l i s () ); // tiempo actual como semilla
11 // Los jugadores ...
12 String [] noms = { " Alex " ," Eli " ," Paco " ," Gabi " ," Pepe " ," Luis " ," To ~ n o " ," Pedro " };
13 for ( String nombre : noms ) {
14 C . agregar ( new Nodo ( nombre , null ) );
15 C . avanzar ();
16 }
17 System . out . println ( " Los jugadores son : " + C . toString ());
18 for ( int i = 0; i < N ; i ++) { // jugar Pato , Pato , Ganso N veces
19 eleg = C . remover ();
20 System . out . println ( eleg . getElemento () + " es el elegido . " );
21 System . out . println ( " Jugando Pato , Pato , Ganso con : " + C . toString ());
22 // marchar alrededor del circulo
23 while ( rand . nextBoolean () || rand . nextBoolean ()) {
24 C . avanzar (); // avanzar con probabilidad 3/4
25 System . out . println ( C . getCursor (). getElemento () + " es un pato . " );
26 }
27 ganso = C . remover ();
28 System . out . println ( " " + ganso . getElemento () + " es el ganso ! " );
29 if ( rand . nextBoolean () ) {
30 System . out . println ( " El ganso gan o ! " );
31 C . agregar ( ganso ); // meter ganso al c rculo
32 C . avanzar (); // cursor est a sobre el ganso
33 C . agregar ( eleg ); // elegido seguir a si e ndolo en la pr
o xima ronda
34 }
35 else {
36 System . out . println ( " El ganso perdi o ! " );
37 C . agregar ( eleg ); // poner elegido en lugar del ganso
38 C . avanzar (); // cursor est a sobre persona elegida
39 C . agregar ( ganso ); // ganso es el elegido en la siguiente ronda
40 }
41 }
42 System . out . println ( " El circulo final es " + C . toString ());
43 }
44 }

Listado 3.14: El metodo main() de una aplicaci


on que usa una lista circular enlazada
para simular el juego de ninos Pato, Pato, Ganso.
126 Arreglos, listas enlazadas y recurrencia

El ganso perdio!
Pepe es el 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.

3.5.2. Ordenar una lista doblemente enlazada


Para ordenar una lista doblemente enlazada se puede emplear el algoritmo
InsercionOrdenada.
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()
Una implementacion en Java del algoritmo de insercion ordenada para una
lista doblemente enlazada representada por la clase ListaDoble se muestra
en el listado 3.15.
3.6 Recurrencia 127

1 /* * Inserci o n ordenada para una lista doblemente enlazada


2 de la clase ListaDoble . */
3 public static void ordenar ( ListaDoble L ) {
4 if ( L . tam () <= 1) return ; // L ya est a ordenada en este caso
5 NodoD pivote ; // nodo pivote
6 NodoD ins ; // punto de inserci on
7 NodoD fin = L . getPrimero (); // fin de la corrida
8 while ( fin != L . getUltimo ()) {
9 pivote = fin . getSig (); // obtener el siguiente nodo de pivote
10 L . remover ( pivote ); // quitar pivote de L
11 ins = fin ; // iniciar busqueda desde fin de la corrida ordenada
12 while ( L . tienePrev ( ins ) &&
13 ins . getElemento (). compareTo ( pivote . getElemento ()) > 0)
14 ins = ins . getPrev (); // mover a la izquierda
15 // agregar el pivote de regreso , despues del punto de inserci on
16 L . agregarD espues ( ins , pivote );
17 if ( ins == fin ) // se ha agregado pivote despu e s de fin ( se quedo
18 fin = fin . getSig (); // en el mismo lugar ) -> incrementar marcador fin
19 }
20 }

Listado 3.15: Ordenamiento por insercion para una lista doblemente enlazada
representada por la clase ListaDoble.

3.6. 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
128 Arreglos, listas enlazadas y recurrencia

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 definici
on recursiva.

1 si n = 0
factorial(n) = (3.1)
n factorial(n 1) si n 1
Esta definicion es tpica de varias definiciones recursivas. Primero, esta
contiene uno, o mas caso base, los cuales no estan definidos recursivamente,
si no en terminos de cantidades fijas. En este caso, n = 0 es el caso base.
Esta definicion tambien contiene uno o mas casos recursivos, los cuales
estan definidos empleando la definicion de la funcion que esta siendo definida.
Observar que no hay circularidad en esta definicion, porque cada vez que la
funcion es invocada, su argumento es mas peque no en uno.

Implementaci
on recursiva de la funci
on factorial
Se considera una implementacion en Java, listado 3.16, de la funcion facto-
rial que esta dada por la formula 3.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 }

Listado 3.16: Una implementacion recursiva de la funcion factorial.

Cual es la ventaja de usar recurrencia? La implementacion recursiva de


la funcion factorial es un poco mas simple que la version iterativa, en este
caso no hay razon urgente para preferir la recurrencia sobre la iteracion. Para
algunos problemas, sin embargo, una implementacion recursiva puede ser
3.6 Recurrencia 129

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


- - -
-- -- --
- - -
--- --- --- 1
- - -
-- -- --
- - -
---- 1 ---- --- 2
- - -
-- -- --
- - -
--- --- --- 3
- -
-- --
- -
---- 2 ----- 1

(a) (b) (c)

Figura 3.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.

significativamente mas simple y facil de entender que una implementacion


iterativa como en los ejemplos siguientes.

Dibujar una regla inglesa

Como un caso mas complejo del uso de recurrencia, considerar como dibujar
las marcas de una regla tpica inglesa. Una regla esta dividida en intervalos
de una pulgada, y cada intervalo consiste de un conjunto de marcas colocadas
en intervalos de media pulgada, un cuarto de pulgada, y as sucesivamente.
Como el tama no del intervalo decrece por la mitad, la longitud de la marca
decrece en uno, ver figura 3.16.
Cada m ultiplo de una pulgada tambien tiene una etiqueta numerica. La
longitud de la marca mas grande es llamada la longitud de la marca principal.
No se consideran las distancias actuales entre marcas, sin embargo, se imprime
130 Arreglos, listas enlazadas y recurrencia

una marca por lnea.

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 3.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:

Un intervalo con una longitud de marca central L 1


Un solo interalo de longitud L
Un intervalo con una longitud de marca central L 1

Con cada llamada recursiva, la longitud decrece por uno. Cuando la


longitud llega a cero, se debe regresar. Como resultado, este proceso recursivo
siempre terminara. Esto sugiere un proceso recursivo, en el cual el primer
paso y ultimo son hechos llamando a dibujaMarcas(L-1) recursivamente. El
paso central es realizado llamando a la funcion dibujaUnaMarca(L). Esta
formulacion recursiva es mostrada en el listado 3.17. Como en el ejemplo del
factorial, el codigo tiene un caso base, cuando L = 0. En este ejemplo se
hacen dos llamadas recursivas a la funcion.

Sumar elementos de un arreglo recursivamente


Supongase que se da un arreglo, A, de n enteros que se desean sumar
juntos. Se puede resolver el problema de la suma usando recurrencia lineal
3.6 Recurrencia 131

1 // dibuja regla conteniendo nPulgadas con marca de tama n ~ o longPrincipal


2 public static void dibujaRegla ( int nPulgadas , int longPrincipal ) {
3 dibu jaUnaMar ca ( longPrincipal , 0); // dibujar marca 0 y su etiqueta
4 for ( int i = 1; i <= nPulgadas ; i ++) {
5 dibujaMarcas ( longPrincipal -1); // dibujar marcas para esta pulgada
6 dibu jaUnaMar ca ( longPrincipal , i ); // dibujar marca i y su etiqueta
7 }
8 }
9 // dibuja marcas de longitud dada
10 public static void dibujaMarcas ( int longMarca ) {
11 if ( longMarca > 0) { // parar cuando la longitud llegue a 0
12 dibujaMarcas ( longMarca -1); // recu rsivamen te dibujar marca superior
13 dibu jaUnaMar ca ( longMarca ); // dibujar marca principal
14 dibujaMarcas ( longMarca -1); // recu rsivamen te dibujar marca inferior
15 }
16 }
17 // dibuja una marca sin etiqueta
18 public static void dib ujaUnaMa rca ( int longMarca ) {
19 dibu jaUnaMar ca ( longMarca , -1);
20 }
21 // dibuja una marca
22 public static void dib ujaUnaMa rca ( int longMarca , int etiqMarca ) {
23 for ( int i = 0; i < longMarca ; i ++)
24 System . out . print ( " -" );
25 if ( etiqMarca >= 0) System . out . print ( " " + etiqMarca );
26 System . out . print ( " \ n " );
27 }

Listado 3.17: Implementacion recursiva del metodo dibujaMarcas() para mostrar


marcas de una regla.
132 Arreglos, listas enlazadas y recurrencia

observando que la suma de todos los n enteros de A es igual a A[0], si n es 1,


o la suma de los primeros n menos 1 enteros mas la suma del ultimo elemento
en A. En particular, se puede resolver esta suma usando el algoritmo recursivo
siguiente.
Algoritmo SumaLineal(A, n):
Entrada: Un arreglo A de enteros y un entero n 1 tal que A tiene
al menos n elementos.
Salida: La suma de los primeros n enteros en A.
si n = 1 entonces
regresar A[0]
si no
regresar SumaLineal(A, n 1) + A[n]
Este ejemplo, tambien ilustra una propiedad importante que un metodo
recursivo debera siempre tenerque el metodo termina. Se asegura esto
escribiendo una sentencia no recursiva para el caso n igual a 1. Ademas,
siempre se hace una llamada recursiva sobre un valor mas peque no del
parametro n menos 1 que el que fue dado, n, por lo tanto, en alg un punto
(en el punto mas bajo de la recurrencia), se hara la parte no recursiva del
calculo (regresando A[0]. En general, un algoritmo que usa recurrencia lineal
tpicamente tiene la siguiente forma:

Prueba para los casos base. Se inicia probando un conjunto de casos


base, debera haber al menos uno. Estos casos basos deberan estar
definidos, por lo que cada posible encadenamiento de llamadas recursivas
eventualmente alcanzara un caso base, y el manejo de cada caso base
no debera usar recurrencia.

Recurrencia. Despues de probar los casos base, entonces se realizan


llamadas recursivas simples. Este paso recursivo podra involucrar una
prueba que decida cual de varias posibles llamadas recursivas hacer, pero
finalmente debera escoger hacer solo una de estas posibles llamadas
cada vez que se haga este paso. Mas aun, se debera definir cada posible
llamada recursiva para que esta progrese hacia el caso base.

Invertir un arreglo por recurrencia


Se considera ahora el problema de invertir los n elementos de un arreglo, A,
para que el primer elemento sea el ultimo, el segundo elemento sea el pen
ultimo,
3.6 Recurrencia 133

y as sucesivamente. Se puede resolver este problema usando recurrencia lineal,


observando que la inversion de un arreglo se puede lograr intercambiando
el primer elemento y el u ltimo, y entonces invertir recursivamente el resto
de los elementos en el arreglo. Se describen a continuacion los detalles de
este algoritmo, usando la convencion de que la primera vez que se llama el
algoritmo se hace como invertirArreglo(A,0,n-1).
Algoritmo InvertirArreglo(A, i, j):
Entrada: Un arreglo A e ndices enteros no negativos i y j.
Salida: La inversion de los elementos en A iniciando en el ndice i
y terminando en j.
si i < j entonces
Intercambiar A[i] y A[j]
InvertirArreglo(A, i + 1, j 1)
regresar
En este algoritmo se tienen dos casos base implcitos, a saber, cuando
i = j y cuando i > j. Sin embargo, en cualquier caso, se termina el algoritmo,
ya que una secuencia con cero elementos o un elemento es trivialmente igual a
su inversion. Ademas, en el paso recursivo se esta garantizando ir progresando
hacia uno de los dos casos base. Si n es par, eventualmente se alcanzara el
caso i = j, y si n es impar, eventualmente se alcanzara el caso i > j. El
argumento anterior implica que el algoritmo recursivo esta garantizado para
terminar.

Definir problemas para facilitar la recurrencia

Para dise
nar un algoritmo recursivo para un problema dado, es util pensar
en las diferentes formas en que se puede subdividir el problema para definir
problemas que tengan la misma estructura general que el problema original.
Este proceso en ocasiones significa que se necesita redefinir el problema
original para facilitar la b
usqueda de problemas similares. Por ejemplo, con el
algoritmo InvertirArreglo, se agregaron los parametros i y j por lo que una
llamada recursiva para invertir la parte interna del arreglo A podran tener la
misma estructura y la misma sintaxis, como la llamada para invertir cualquier
otro par de valores del arreglo A. Entonces, en vez de inicialmente llamar
al algoritmo como InvertirArreglo(A), se llama a este inicialmente como
InvertirArreglo(A, 0, n1). En general, si se tiene dificultad para encontrar
la estructura repetitiva requerida para dise nar un algoritmo recursivo, en
134 Arreglos, listas enlazadas y recurrencia

algunos casos es u
til trabajar el problema con algunos ejemplos concretos
peque
nos para ver como los subproblemas deberan estar definidos.

Recurrencia de cola

Usar la recurrencia puede ser con frecuencia una herramienta u til para
disenar 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 algo-
ritmo recurrente en uno no recurrente, pero hay algunos casos cuando se
puede hacer esta conversion mas facil y eficiente. Especficamente, se puede
facilmente convertir algoritmos que usan recurrencia de cola. Un algorit-
mo usa recurrencia de cola si este usa recursion lineal y el algoritmo hace
una llamada recursiva como su u ltima operacion. Por ejemplo, el algoritmo
InvertirArreglo usa recurrencia de cola.
Sin embargo, no es suficiente que la ultima sentencia en el metodo definicion
incluya una llamada recurrente. Para que un metodo use recurrencia de cola, la
llamada recursiva debera ser absolutamente la ultima parte que el metodo haga,
a menos que se este en el caso base. Por ejemplo, el algoritmo SumaLineal no
usa recurrencia de cola, a un cuando su u ltima sentencia incluye una llamada
recursiva. Esta llamada recursiva no es actualmente la u ltima tarea que
el metodo realiza. Despues de esta recibe el valor regresado de la llamada
recursiva, le agrega el valor de A[n 1] y regresa su suma. Esto es, la ultima
tarea que hace el algoritmo es una suma, y no una llamada recursiva.
Cuando un algoritmo emplea recurrencia de cola, se puede convertir el
algoritmo recurrente en uno no recurrente, iterando a traves de las llamadas
recurrentes en vez de llamarlas a ellas explcitamente. Se ilustra este tipo de
conversion revisitando el problema de invertir los elementos de un arreglo.
El siguiente algoritmo no recursivo realiza la tarea de invertir los elementos
iterando a traves de llamadas recurrentes del algoritmo InvertirArreglo.
Inicialmente se llama a este algoritmo como InvertirArregloIterativo
(A, 0, n 1).
3.6 Recurrencia 135

Algoritmo InvertirArregloIterativo(A, i, j):


Entrada: Un arreglo A e ndices enteros no negativos i y j.
Salida: La inversion de los elementos en A iniciando en el ndice i
y terminando en j.
mientras i < j hacer
Intercambiar A[i] y A[j]
ii+1
j j1
regresar

3.6.1. Recurrencia binaria


Cuando un algoritmo hace dos llamadas recurrentes, se dice que usa
recurrencia binaria. Estas llamadas pueden, por ejemplo, ser usadas para
resolver dos hojas similares de alg
un problema, como se hizo en la seccion 3.6
para dibujar una regla inglesa. Como otra aplicacion de recurrencia binaria,
se revisitara el problema de sumar los n elementos de un arreglo de enteros
A. En este caso, se pueden sumar los elementos de A: (i) recurrentemente
los elementos en la primera mitad de A; (ii) recurrentemente los elementos
en la segunda mitad de A; y (iii) agregando estos dos valores juntos. Se dan
los detalles en el siguiente algoritmo, el cual es inicialmente llamado como
SumaBinaria(A, 0, n).
Algoritmo SumaBinaria(A, i, n):
Entrada: Un arreglo A de enteros y enteros i y n.
Salida: La suma de los primeros n enteros en A iniciando en el ndice i.
si n = 1 entonces
regresar A[i]
regresar SumaBinaria(A, i, dn/2e)+SumaBinaria(A, i + dn/2e, bn/2c)
Para analizar el algoritmo SumaBinaria, se considera, por simplicidad,
el caso cuando n es potencia de 2. La figura 3.17 muestra el trazado de la
recursion de una ejecucion del metodo SumaBinaria(0,8). Se etiqueta cada
recuadro con los valores de los parametros i y n, los cuales representan el
ndice inicial y el tama
no de la secuencia de los elementos que seran invertidos,
respectivamente. Observar que las flechas en el trazado van de una recuadro
etiquetado (i, n) a otro recuadro etiquetado (i, n/2) o (i + n/2, n/2). Esto
es, el valor del parametro n es dividido en dos en cada llamada recursiva.
Por lo tanto, la profundidad de la recursion, esto es, el n umero maximo
136 Arreglos, listas enlazadas y recurrencia

de instancias del metodo que estan activas al mismo tiempo, es 1 + log2 n.


Entoces el algoritmo usa una cantidad de espacio adicional aproximadamente
proporcional a su valor. El tiempo de ejecucion del algoritmo es todava
proporcional a n, ya que cada recuadro es visitado en tiempo constante y hay
2n 1 recuadros.

Figura 3.17: Trazado de la recursion de SumaBinaria(0,8).

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
si no
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
n
umero de llamadas exponencial para calcular el k-esimo n umero de Fibonacci
3.6 Recurrencia 137

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,
nk > 2k/2 , lo que significa que F ibBinario(k) hace un n umero de llamadas
que son exponenciales en k. En otras palabras, empleando recurrencia binaria
para calcular numeros de Fibonacci es muy ineficiente.

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 numeros Fibonacci (Fk1 , Fk ).
Entonces se puede usar el siguiente algoritmo de recurrencia lineal mostrado
a continuacion.
138 Arreglos, listas enlazadas y recurrencia

Algoritmo FibLineal(k):
Entrada: Un entero k no negativo.
Salida: El par de numeros Fibonacci (Fk , Fk1 ).
si k 1 entonces
regresar (0, k)
si no
(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 lla-
madas adicionales. Esto es, calcular el k-esimo numero 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 recurren-
cia 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
es una parte central de una tecnica llamada programacion din amica, la cual
esta relacionada con la recursion.

3.6.2. Recurrencia m
ultiple
La generalizacion de la recurrencia binaria lleva a la recurrencia multi-
ple, que es cuando un metodo podra hacer varias llamadas recursivas, siendo
ese numero potencialmente mayor que dos. Una de las aplicaciones mas co-
munes 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

Para resolver el acertijo, se necesita asignar un dgito u nico, esto es,


0,1,. . . ,9, a cada letra en la ecuacion, para poder hacer la ecuacion verdadera.
Tpicamente, se resuelve el acertijo usando observaciones humanas del acertijo
3.6 Recurrencia 139

particular que se esta intentando resolver para eliminar configuraciones hasta


que se pueda trabajar con las configuraciones restantes, probando la correctez
de cada una.

Sin embargo, si el n umero de configuraciones posibles no es muy grande, se


puede usar una computadora para enumerar todas las posibilidades y probar
cada una, sin emplear observaciones humanas. Ademas, tal algoritmo puede
usar recurrencia m ultiple para trabajar a traves de todas las configuraciones
en una forma sistematica. Se proporciona enseguida pseudocodigo para tal
algoritmo. Para conservar la descripcion general suficiente para ser usado con
otros acertijos, el algoritmo enumera y prueba todas las secuencias de longitud
k sin repeticiones de los elementos de un conjunto dado U . Se construyen las
secuencias de k elementos mediante los siguientes pasos:

1. Generar recursivamente las secuencias de k 1 elementos.

2. Agregar a cada una de tales secuencias un elemento que no este contenido


dentro de esta.

En toda la ejecucion del algoritmo, se usa el conjunto U para saber los


elementos no contenidos en la secuencia actual, por lo que un elemento e no
ha sido usado a
un s y solo s e esta en U .

Otra forma de ver el algoritmo siguiente es que este enumera cada sub-
conjunto ordenado posible de 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.
140 Arreglos, listas enlazadas y recurrencia

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
si no
ResolverAcertijo(k 1, S, U )
Agregar e de regreso a U { e esta ahora sin uso }
Remover e del final de S
Captulo 4

Herramientas de an
alisis

4.1. Funciones
Se comenta brevemente, en esta seccion, las siete funciones mas importantes
usadas en el analisis de algoritmos.

4.1.1. La funci
on constante
La funcion mas simple que se emplea es la funci
on 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 n
umero 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.
142 Herramientas de an
alisis

4.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 4.1: reglas de logaritmos dados numeros 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 .
4.1 Funciones 143

4.1.3. La funci
on lineal
Otra funcion simple e importante es la funci
on 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 hacer
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 tama no n reque-
rira 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.

4.1.4. La funcion N-Log-N


La funcion
f (n) = n log n
es la que asigna a una entrada n el valor de n veces el logaritmo de base dos
de n. Esta funcion crece un poco mas rapido que la funcion lineal, pero es
mas rapida que la funcion cuadratica. Por lo tanto, si se logra mejorar el
tiempo de ejecucion de alg
un problema desde un tiempo cuadratico a n-log-n,
se tendra un algoritmo que corra mas rapido en general.

4.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.
144 Herramientas de an
alisis

Ciclos anidados y la funci


on cuadr
atica
La funcion cuadratica puede tambien surgir en los ciclos anidados donde la
primera iteracion de un ciclo usa una operacion, la segunda usa dos operaciones,
la tercera usa tres operaciones, y as sucesivamente, es decir, el n umero de
operaciones es
1 + 2 + 3 + + (n 2) + (n 1) + n.
En otras palabras, este es el n
umero total de operaciones que seran hechas
por el ciclo anidado, si el n
umero de operaciones realizadas dentro del ciclo
se incrementa por uno con cada iteracion del ciclo exterior.
Se cree que Carl Gauss uso la siguiente identidad para resolver el problema
que le haban dejado de encontrar la suma de enteros desde el 1 hasta el 100.
Proposici on 4.2: para cualquier entero n 1, se tiene

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
operaciones es n2 /2 + n/2, en este caso, lo cual es un poco mas que un factor
constante (1/2) veces la funcion cuadratica n2 .

4.1.6. La funci
on c
ubica y otras polinomiales
De las funciones que son potencias de la entrada, se considera la funci
on
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 funciones
polinomiales.
4.1 Funciones 145

Una funcion polinomial es una funcion de la forma,

f (n) = a0 + a1 n + a2 n2 + a3 n3 + + ad nd ,

donde a0 , a1 , . . . , ad son constantes, llamados los coeficientes de la polinomial,


y ad 6= 0. El entero d, el cual indica la potencia mas grande en el polinomio,
es llamado el grado del polinomio.

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

donde a y b son enteros y a b.


Usando una sumatoria se puede escribir un polinomio f (n) de grado d
con coeficientes a0 , . . . , ad como
d
X
f (n) = ai n i .
i=0

4.1.7. La funci
on exponencial
Otra funcion usada en el analisis de algoritmos es la funci
on exponencial

f (n) = bn ,

donde b es una constante positiva, llamada la base, y el argumento n es el


exponente, es decir, la funcion f (n) asigna al argumento de entrada n el
valor obtenido de multiplicar la base b por s misma n veces. En analisis de
algoritmos, la base mas com un para la funcion exponencial es b = 2. Por
ejemplo, si se tiene un ciclo que inicia realizando una operacion y entonces
dobla el n
umero de operaciones hechas con cada iteracion, entonces el numero
n
de operaciones realizadas en la n-esima iteracion es 2 . Ademas, una palabra
entera conteniendo n bits puede representar todos los enteros no negativos
menores que 2n . La funcion exponencial tambien sera referida como funcion
exponente.
146 Herramientas de an
alisis

En ocasiones se tienen otros exponentes ademas de n, por lo que es u


til
conocer unas cuantas reglas para trabajar con exponentes.
Proposicion 4.3: reglas de exponentes dados enteros positivos a, b, y c,
se tiene

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

recordando que a0 = 1 si a > 0. Esta suma es igual a

an+1 1
.
a1

4.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
4.2 An
alisis de algoritmos 147

software (como se refleja en el sistema operativo, lenguaje de programacion,


compilador, interprete, etc.) en el cual el algoritmo esta implementado, compi-
lado y ejecutado. Considerando las posibles variaciones que pueden darse por
los diferentes factores ambientales, se intenta enfocarse en la relacion entre el
tiempo de ejecucion de un algoritmo y el tama no de su entrada.
Se esta interesado en caracterizar el tiempo de ejecucion de un algoritmo
como una funcion del tama no de la entrada. Entonces se requiere saber cual
es una forma apropiada de medirla.

4.2.1. Estudios experimentales


Si un algoritmo ha sido implementado, se puede estudiar su tiempo de
ejecucion usando varias pruebas de entrada y guardando el tiempo usado
en cada ejecucion. Para hacer estas mediciones de forma precisa se pueden
hacer llamadas al sistema que estan construidas en el lenguaje o el sistema
operativo, por ejemplo, usando metodo System.currentTimeMillis(). Se
esta interesado en determinar la dependencia general del tiempo de ejecucion
de acuerdo al tama no de la entrada, para lo cual se deben hacer varios
experimentos con muchas entradas de prueba diferentes de varios tama nos.
Entonces se pueden visualizar los resultados de tales experimentos graficando
el rendimiento de cada ejecucion del algoritmo como un punto con coordenada
x igual al tama no de la entrada, n, y la coordenada y igual al tiempo de
ejecucion, t. Este analisis requiere que se escojan buenas entradas de muestra.
Las limitaciones que tienen los estudios experimentales son:

Los experimentos solo pueden hacerse en un conjunto limitado de


pruebas de entrada, por lo que quedaran entradas no incluidas que
podran ser importantes.

Es difcil comparar los tiempos experimentales de ejecucion de dos


algoritmos a menos que los experimentos sean hechos en el mismo
hardware y software.

Se tiene que implementar de forma completa y ejecutar un algoritmo


para poder estudiar sus tiempos de ejecucion experimentalmente.
148 Herramientas de an
alisis

4.2.2. Operaciones primitivas


Si se desea analizar un algoritmo particular, se puede realizar un analisis
directo en el pseudo-codigo de alto nivel en vez de realizar experimentos del
tiempo de ejecucion. Se define un conjunto de operaciones primitivas tales
como las siguientes:

Asignar un valor a una variable.

Llamar un metodo.

Realizar una operacion aritmetica.

Comparar dos n
umeros.

Indexar en un arreglo.

Seguir la referencia a un objeto.

Regresar desde un metodo.

Conteo de operaciones primitivas


Una operacion primitiva corresponde a una instruccion de bajo nivel con
un tiempo de ejecucion que es constante. Se cuentan cuantas operaciones
primitivas son ejecutadas, y se usa este n umero t como una medida del tiempo
de ejecucion del algoritmo.
El conteo de las operaciones esta correlacionado a un tiempo de ejecu-
cion actual en una computadora particular, para cada operacion primitiva
corresponde a una instruccion constante en el tiempo, y solo hay n umero
fijo de operaciones primitivas. El n umero, t, de operaciones primitivas que
un algoritmo realiza sera proporcional al tiempo de ejecucion actual de ese
algoritmo.
Un algoritmo podra correr mas rapido con algunas entradas que con
otras del mismo tama no. Por lo tanto, se podra desear expresar el tiempo
de ejecucion de un algoritmo como la funcion del tama no de la entrada
obtenida de tomar el promedio de todas las posibles entradas del mismo
tama no. Desafortunadamente, el an alisis promedio suele ser muy difcil,
ya que requiere definir una distribucion de probabilidad sobre el conjunto de
entradas, lo cual es una tarea difcil.
4.2 An
alisis de algoritmos 149

Enfoque en el peor caso


El analisis del peor caso es mucho mas facil que el caso promedio, ya
que solo requiere la habilidad de identificar el peor caso de entrada, el cual
es frecuentemente simple. Con esta aproximacion tambien se llega a mejores
algoritmos, ya que logrando que el algoritmo se comporte bien para el peor
caso, entonces para cualquier entrada tambien lo hara.

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

f (n) cg(n), para n n0 .

Esta definicion es frecuentemente referida como la notacion O-grande,


por eso en ocasiones se dice f (n) es O-grande de g(n). Alternativamente,
se puede tambien decir f (n) es orden de g(n). La definicion se ilustra en la
figura 4.1
Ejemplo 4.1. La funcion 8n 2 es O(n)
150 Herramientas de an
alisis

Figura 4.1: Ilustracion de la notacion O-grande. La funcion f (n) es O(g(n)) ya


que f (n) c g(n) cuando n n0

Justificaci on. Por la definicion de O-grande, se necesita encontrar una


constante real c > 0 y una constante entera n0 1 tal que 8n 2 cn para
cada entero n n0 . Una posible opcion es c = 8 y n0 = 1 de la infinidad
de opciones disponibles ya que cualquier n umero real mayor que o igual a 8
funciona para c, y cualquier entero mayor que o igual a 1 trabajara para n0 .
La notacion O-grande nos permite decir que una funcion f (n) es menor
que o igual a otra funcion g(n) hasta un factor constante y en el sentido
asintotico conforme n tiene a infinito. Esto se debe del hecho de que la
definicion usar para comparar f (n) con g(n) veces una constante, c, para
los casos asintoticos cuando n n0 .

Propiedades de la notaci
on O-Grande

La notacion O-grande permite ignorar los factores constantes y los terminos


de orden menor y enfocarse en los componentes principales de una funcion
que afectan su crecimiento.
Ejemplo 4.2. 5n4 + 3n3 + 2n2 + 4n + 1 es O(n4 )
Justificacion: Observar que 5n4 +3n3 +2n2 +4n+1 (5+3+2+4+1)n4 =
cn4 , para c = 15, cuando n n0 = 1
Se puede caracterizar la relacion de crecimiento de cualquier funcion
polinomial.
Proposici on 4.5: Si f (n) es un polinomio de grado n, esto es
4.2 An
alisis de algoritmos 151

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 .

As, se puede mostrar que f (n) es O(nd ) definiendo c = a0 + a1 + + ad y


n0 = 1.
As, el termino de mayor grado en una funcion polinomial es el termino
que determina la razon de crecimiento asintotico del polinomio. Se muestran a
continuacion ejemplos adicionales, en donde se combinan las funciones vistas
al inicio del captulo.
Ejemplo 4.3. 5n2 + 3n log n + 2n + 5 es O(n2 ).
Justificaci on: 5n2 + 3n log n + 2n + 5 (5 + 3 + 2 + 5)n2 = cn2 . para c =
15 cuando n n0 = 2. Observar que n log n es cero para n = 1.
Ejemplo 4.4. 20n3 + 10n log n + 5 es O(n3 ).
Justificaci on: 20n3 + 10n log n + 5 35n3 , para n 1.
Ejemplo 4.5. 3 log n + 2 es O(log n).
Justificaci on: 3 log n + 2 5 log n, para n 2. La razon por la que se
usa n 2 = n0 es porque log n es cero para n = 1.
Ejemplo 4.6. 2n+2 es O(2n ).
Justificaci on: 2n+2 = 2n 22 = 4 2n ; por lo tanto, se puede tomar c = 4 y
n0 = 1 en este caso.
Ejemplo 4.7. 2n + 100 log n es O(n).
Justificaci on: 2n + 100 log n 102n para n 2 = n0 ; por lo tanto, se
puede tomar a c = 102 en este caso.

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.
152 Herramientas de an
alisis

Las funciones descritas en la seccion 4 son las funciones mas comunes


usadas en conjuncion con la notacion O-grande para caracterizar los tiempos
de ejecucion y el espacio usado de los algoritmos. Ademas, se usan los nombres
de estas funciones para referirse a los tiempos de ejecucion de los algoritmos
que estan caracterizando. Por ejemplo, se podra decir que un algoritmo que
se ejecuta con tiempo 4n2 + n log n en el peor caso como un algoritmo tiempo
cuadratico, ya que corre en tiempo O(n2 ). De igual forma, un algoritmo
ejecutandose en tiempo a lo mas 5n + 20 log n + 4 podra ser llamado un
algoritmo lineal.

-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 notaci on 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 4.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 4.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.
4.2 An
alisis de algoritmos 153

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

Se muestra enseguida la tasa de crecimiento de algunas funciones impor-


tantes a continuacion:

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

La siguiente tabla explora el tamano maximo permitido para una instancia


de entrada que es procesada por un algoritmo en 1 segundo, 1 minuto, y 1
hora. Se muestra la importancia del diseno de un buen algoritmo, porque un
algoritmo lento asintoticamente es derrotado en una ejecucion grande por
un algoritmo mas rapido asintoticamente, aun si el factor constante para el
algoritmo mas rapido es malo.
154 Herramientas de an
alisis

Tiempo Tama no maximo del problema (n)


Ejecucion (s) 1 segundo 1 minuto 1 hora
400n 2,500 150,000 9,000,000
2n2 707 5,477 42,426
2n 19 25 31

La importancia del dise no de un buen algoritmo va mas alla de que puede


ser resuelto eficientemente en una computadora dada. Como se muestra en
la siguiente tabla aun si se logra una aceleracion dramatica en el hardware,
todava no se puede superar la desventaja de un algoritmo asintoticamente
lento. La tabla muestra el tama no maximo del nuevo problema que se calcula
para una cantidad fija de tiempo, suponiendo que los algoritmos con los
tiempos de ejecucion dados son ejecutados ahora en una computadora 256
veces mas rapida que la previa.

Tiempo de ejecucion Tama


no maximo del nuevo problema
400n 256m
2
2n 16m
n
2 m+8
Captulo 5

Pilas y colas

5.1. 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 atras, permite al usuario
sacar los sitios previamente visitados.

5.1.1. El tipo de dato abstracto pila


Las pilas son las estructuras de datos mas simples, no obstante estas
tambien estan entre las mas importantes, ya que son usadas en un sistema
de diferentes aplicaciones que incluyen estructuras de datos mucho mas
sofisticadas. Formalmente, una pila es un tipo de dato abstracto que soporta
los siguientes dos metodos:
156 Pilas y colas

push(e):inserta el elemento e, para que sea la cima de la


pila.
pop():quita el elemento de la cima de la pila y lo regresa;
un error ocurre si la pila esta vaca.
Adicionalmente, tambien se podran definir los siguientes metodos:
size():regresa el numero de elementos en la pila.
isEmpty():regresa un booleano indicando si la pila esta vaca.
top():regresa el elemento de la cima de la pila, sin remo-
verlo; un error ocurre si la pila esta vaca.
La siguiente tabla muestra una serie de operaciones en la pila y su efecto
en una pila de enteros inicialmente vaca, observar que los elementos son
metidos y sacados por el mismo lado, el lado derecho.
Operacion Salida Contenido
push(5) (5)
push(3) (5,3)
pop() 3 (5)
push(7) (5,7)
pop() 7 (5)
top() 5 (5)
pop() 5 ()
pop() error ()
isEmpty() true ()
push(9) (9)
push(7) (9,7)
push(3) (9,7,3)
push(5) (9,7,3,5)
size() 4 (9,7,3,5)
pop() 5 (9,7,3)
push(8) (9,7,3,8)
pop() 8 (9,7,3)
pop() 3 (9,7)

Una interfaz pila en Java


Debido a su importancia, la estructura de datos pila esta incluida co-
mo una clase en el paquete java.util. La clase java.util.Stack es una
5.1 Pilas 157

estructura que guarda objetos genericos Java e incluye, entre otros, los
metodos push(), pop(), peek() (equivalente a top()), size(), y empty()
(equivalente a isEmpty()). Los metodos pop() y peek() lanzan la excep-
cion EmptyStackException del paquete java.util si son llamados con pilas
vacas. Mientras sea conveniente solo usar la clase incluida java.util.Stack,
es instructivo aprender como dise nar e implementar una pila desde cero.
Implementar un tipo de dato abstracto en Java involucra dos pasos. El
primer paso es la definicion de una interfaz de programaci on de aplica-
ciones o API, o interfaz, la cual describe los nombres de los metodos que
la estructura abstracta de datos soporta y como tienen que ser declarados y
usados.
Ademas, se deben definir excepciones para cualquier cualquier condicion
de error que pueda originarse. Por ejemplo, la condicion de error que ocurre
cuando se llama al metodo pop() o top() en una cola vaca es se nalado
lanzando una excepcion de tipo EmptyStackException, la cual esta definida
en el listado 5.1

1 /* *
2 * Excepci
o n Runtime lanzada cuando se intenta hacer una operaci
o n top
3 * o pop en una cola vac a.
4 */
5
6 public class E m p t y S t a c k E x c e p t i o n extends R u n t i m e E x c e p t io n {
7 public E m p t y S t a c k E x c e p t i o n ( String err ) {
8 super ( err );
9 }
10 }

Listado 5.1: 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 5.2. 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 2.5.2).
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.
158 Pilas y colas

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
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 }

Listado 5.2: Interfaz Stack documentada con comentarios en estilo Javadoc. Se


usa el tipo parametrizado generico E as la pila puede contener elementos de
cualquier clase especificada.
5.1 Pilas 159

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

Figura 5.1: Implementacion de una pila con un arreglo S. El elemento de la cima


esta guardado en la celda S[t].

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,
se pueda usar t para determinar el n umero de elementos (t+1). Se agrega
tambien una nueva excepcion, llamada FullStackException, para se nalar
el error que surge si se intenta insertar un nuevo elemento en una pila llena.
La excepcion FullStackException es particular a esta implementacion y no
esta definida en la ADT pila. Se dan los detalles de la implementacion de la
pila usando un arreglo en los siguientes algoritmos.
160 Pilas y colas

Algoritmo size():
regresar t + 1

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
evitar esta asignacion que podra pensarse al implementar los algoritmos en
Java. El equilibrio involucra el mecanismo colector de basura de Java que
busca en la memoria objetos que ya no son mas referenciados por objetos
activos, y recupera este espacio para su uso futuro. Sea e = S[t] el elemento
de la cima antes de que el metodo pop sea llamado. Haciendo a S[t] una
referencia nula, se indica que la pila no necesita mantener mas una referencia
5.1 Pilas 161

al objeto e. Ademas, si no hay otras referencias activas a e, entonces el espacio


de memoria tomado por e sera recuperado por el colector de basura.
En la siguiente tabla se muestran los tiempos de ejecucion para los metodos
en la ejecucion de una pila con un arreglo. Cada uno de los metodos de la
pila en el arreglo ejecuta un n umero constante de sentencias involucrando
operaciones aritmeticas, comparaciones, y asignaciones. Ademas, pop tambien
llama a isEmpty, el cual tambien se ejecuta en tiempo constante. Por lo tanto,
en esta implementacion del ADT pila, cada metodo se ejecuta en tiempo
constante, esto es, cada uno corre en tiempo O(1).

Metodo Tiempo
size O(1)
isEmpty O(1)
top O(1)
push O(1)
pop O(1)

Una implementacion concreta de Java de los algoritmos para la imple-


mentacion de la pila se da en el listado 5.3, con la implementacion Java de
la clase ArregloStack implementando la interfaz Stack. Se usa un nombre
simbolico, CAPACIDAD, para indicar el tama no del arreglo. Este valor permite
indicar la capacidad del arreglo en un solo lugar en el codigo y que el valor se
refleje en todo el codigo.

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.

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


resultado: num. elems. = 0, esta vacio = true, pila: []
------> A.push(7), regresa null
resultado: num. elems. = 1, esta vacio = false, pila: [7]
------> A.pop(), regresa 7
resultado: num. elems. = 0, esta vacio = true, pila: []
------> A.push(9), regresa null
resultado: num. elems. = 1, esta vacio = false, pila: [9]
------> A.pop(), regresa 9
162 Pilas y colas

1 /* *
2 * Implementaci o n de la ADT pila usando un arreglo de longitud fija .
3 * Una excepci o n es lanzada si la operaci o n push es intentada cuando
4 * el tama n ~ o de la pila es igual a la longitud del arreglo . Esta
5 * clase incluye los m e todos principales de la clase agregada
6 * java . util . Stack .
7 *
8 * @see F u l l S t a c k E x c e p t i o n
9 */
10 public class ArregloStack <E > implements Stack <E > {
11 /* *
12 * Capacidad del arreglo usado para implementar la pila .
13 */
14 protected int capacidad ;
15 /* *
16 * Capacidad por defecto del arreglo .
17 */
18 public static final int CAPACIDAD = 1000;
19 /* *
20 * Arreglo gen e rico usado para implementar la pila .
21 */
22 protected E [] S ;
23 /* *
24 * I ndice del elemento cima de la pila en el arreglo
25 */
26 protected int cima = -1;
27 /* *
28 * Inicializa la pila para usar un arreglo de longitud por defecto .
29 */
30 public ArregloStack () {
31 this ( CAPACIDAD );
32 }
33 /* *
34 * Inicializa la pila para usar un arreglo de longitud dada .
35 * @param cap longitud del arreglo .
36 */
37 public ArregloStack ( int cap ) {
38 capacidad = cap ;
39 S = ( E []) new Object [ capacidad ]; // el compilador podr a dar advertencia
40 // pero est a bien
41 }
42 /* *
43 * Regresa el n u mero de elementos en la pila .
44 * Este m e todo se ejecuta en tiempo O (1)
45 * @return n u mero de elementos en la pila .
46 */
47 public int size () {
48 return ( cima + 1);
49 }
50 /* *
51 * Prueba si la pila est a vac
a.
52 * Este m e todo se ejecuta en tiempo O (1)
53 * @return true si la pila est a vacia , false de otra forma .
54 */
55 public boolean isEmpty () {
56 return ( cima < 0);
57 }
58 /* *
59 * Inserta un elemento en la cima de la pila .
60 * Este m e todo se ejecuta en tiempo O (1)
61 * @return elemento insertado .
62 * @param elemento elemento a ser insertado .
63 * @exception F u l l S t a c k E x c e p t i o n si el arreglo de los elementos est a lleno .
64 */
65 public void push ( E elemento ) throws F u l l S t a c k E x c e p t i o n {
66 if ( size () == capacidad )
67 throw new F u l l S t a c k E x c e p t i o n ( " La pila est a
llena . " );
68 S [++ cima ] = elemento ;
69 }
70 /* *
5.1 Pilas 163

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


------> new ArregloStack<String> B, regresa null
resultado: num. elems. = 0, esta vacio = true, pila: []
------> B.push("Paco"), regresa null
resultado: num. elems. = 1, esta vacio = false, pila: [Paco]
------> B.push("Pepe"), regresa null
resultado: num. elems. = 2, esta vacio = false, pila: [Paco, Pepe]
------> B.pop(), regresa Pepe
resultado: num. elems. = 1, esta vacio = false, pila: [Paco]
------> B.push("Juan"), regresa null
resultado: num. elems. = 2, esta vacio = false, pila: [Paco, Juan]

Limitaci
on de la pila con un arreglo
La implementacion con un arreglo de una pila es simple y eficiente. Sin
embargo, esta implementacion tiene un aspecto negativoesta debe asumir
un lmite superior fijo, CAPACIDAD, en el tama no de la pila. Para el ejemplo
mostrado en el listado 5.3 se escogio el valor de la capacidad igual a 1,000 o un
valor arbitrario menor o mayor. Una aplicacion podra actualmente necesitar
mucho menos espacio que esto, por lo que se tendra un desperdicio de
memoria. Alternativamente, una aplicacion podra necesitar mas espacio que
esto, lo cual causara que la implementacion de la pila genere una excepcion
tan pronto como un programa cliente intente rebasar la capacidad de la
pila, por defecto 1,000 objetos. A pesar de la simplicidad y eficiencia, la pila
implementada con arreglo no es precisamente ideal.
Afortunadamente, hay otra implementacion, la cual se revisa a continua-
cion, que no tiene una limitacion del tama no y usa espacio proporcional al
numero actual de elementos guardados en la pila. A un, en casos donde se
tiene una buena estimacion en el n umero de elementos que se guardaran en
la pila, la implementacion basada en el arreglo es difcil de vencer. Las pilas
tienen un papel importante en un n umero de aplicaciones computacionales,
por lo tanto es util tener una implementacion rapida del ADT pila como una
implementacion simple basada en el arreglo.

5.1.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
164 Pilas y colas

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
size en tiempo constante, se lleva la cuenta del n umero actual de elementos
en una variable de instancia.
En vez de usar una lista ligada que solamente pueda guardar objetos
de un cierto tipo, como se mostro en la seccion 3.3, se quiere, en este caso,
implementar una pila generica usando una lista generica enlazada. Por lo
tanto, se necesita usar un nodo de tipo generico para implementar esta lista
enlazada. Se muestra tal clase Nodo en el listado 5.4.

1 /* *
2 * Nodo de una lista simple enlazada , la cual guarda referencias
3 * a su elemento y al siguiente nodo en la lista .
4 *
5 */
6 public class Nodo <E > {
7 // Variables de instancia :
8 private E elemento ;
9 private Nodo <E > sig ;
10 /* * Crea un nodo con referencias nulas a su elemento y al nodo sig . */
11 public Nodo () {
12 this ( null , null );
13 }
14 /* * Crear un nodo con el elemento dado y el nodo sig . */
15 public Nodo ( E e , Nodo <E > s ) {
16 elemento = e ;
17 sig = s ;
18 }
19 // M
e todos accesores :
20 public E getElemento () {
21 return elemento ;
22 }
23 public Nodo <E > getSig () {
24 return sig ;
25 }
26 // M
e todos modificadores :
27 public void setElemento ( E nvoElem ) {
28 elemento = nvoElem ;
29 }
30 public void setSig ( Nodo <E > nvoSig ) {
31 sig = nvoSig ;
32 }
33 }

Listado 5.4: La clase Nodo implementa un nodo generico para una lista simple
enlazada.
5.1 Pilas 165

Clase gen
erica NodoStack

Una implementacion con Java de una pila, por medio de un lista simple
enlazada, se da en el listado 5.5. Todos los metodos de la interfaz Stack son
ejecutados en tiempo constante. Ademas de ser eficiente en el tiempo, esta
implementacion de lista enlazada tiene un requerimiento de espacio que es
O(n), donde n es el n umero actual de elementos en la pila. Por lo tanto, esta
implementacion no requiere que una nueva excepcion sea creada para manejar
los problemas de desbordamiento de tama no. Se usa una variable de instancia
cima para referirse a la cabeza de la lista, la cual apunta al objeto null si la
lista esta vaca. Cuando se mete un nuevo elemento e en la pila, se crea un
nuevo nodo v para e, se referencia e desde v, y se inserta v en la cabeza de la
lista. De igual modo, cuando se quita un elemento de la pila, se remueve el
nodo en la cabeza de la lista y se regresa el elemento. Por lo tanto, se realizan
todas las inserciones y borrados de elementos en la cabeza de la lista.

5.1.4. Invertir un arreglo con una pila


Se puede usar una pila para invertir los elementos en un arreglo, teniendo
as un algoritmo no recursivo para el problema indicado en la seccion 3.6. La
idea basica es meter todos los elementos del arreglo en orden en una pila y
entonces llenar el arreglo de regreso otra vez sacando los elementos de la pila.
En el listado 5.6 se da una implementacion en Java del algoritmo descrito.
En el metodo invertir se muestra como se pueden usar tipos genericos en
una aplicacion simple que usa una pila generica. En particular, cuando los
elementos son sacados de la pila en el ejemplo, estos son automaticamente
regresados como elementos del tipo E; por lo tanto, estos pueden ser inmedia-
tamente regresados al arreglo de entrada. Se muestran dos ejemplos que usan
este metodo.
La salida del listado 5.6 se muestra a continuacion:

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


s = [Jorge, Paco, Pedro, Juan, Marta]
Invirtiendo ...
a = [42, 23, 16, 15, 8, 4]
s = [Marta, Juan, Pedro, Paco, Jorge]
166 Pilas y colas

1 /* *
2 * Implementaci
o n del ADT pila por medio de una lista simple enlazada .
3 *
4 * @see Nodo
5 */
6
7 public class NodoStack <E > implements Stack <E > {
8 protected Nodo <E > cima ; // referencia el nodo cabeza
9 protected int tam ; // n u mero de elementos en la pila
10 /* * Crear una pila vac a . */
11 public NodoStack () {
12 cima = null ;
13 tam = 0;
14 }
15 public int size () { return tam ; }
16 public boolean isEmpty () {
17 return cima == null ;
18 }
19 public void push ( E elem ) {
20 Nodo <E > v = new Nodo <E >( elem , cima ); // crear un nuevo nodo y enlazarlo
21 cima = v ;
22 tam ++;
23 }
24 public E top () throws E m p t y S t a c k E x c e p t i o n {
25 if ( isEmpty ()) throw new E m p t y S t a c k E x c e p t i o n ( " La pila est a vac
a . " );
26 return cima . getElemento ();
27 }
28 public E pop () throws E m p t y S t a c k E x c e p t i o n {
29 if ( isEmpty ()) throw new E m p t y S t a c k E x c e p t i o n ( " La pila est a vac
a . " );
30 E temp = cima . getElemento ();
31 cima = cima . getSig (); // desenlazar el antiguo nodo cima
32 tam - -;
33 return temp ;
34 }
35 /* *
36 * Regresar una representaci o n de cadena de la pila como una lista
37 * de elementos con el elemento cima al final : [ ... , prev , cima ].
38 * Este m e todo se ejecuta en tiempo O ( n ) , donde n es el n u mero de
39 * elementos en la pila .
40 * @return representaci o n textual de la pila .
41 */
42 public String toString () {
43 Nodo <E > cur = cima ;
44 String s = " [ " ;
45 while ( cur != null ) {
46 if ( cur == cima ) // primer elemento
47 s += cur . getElemento ();
48 else
49 s += " , " + cur . getElemento ();
50 cur = cur . getSig ();
51 }
52 return s + " ] " ;
53 }
54 /* *
55 * Imprime informaci o n acerca de una operaci o n y la pila .
56 * @param S pila de la cual se reporta su estado
57 * @param op operaci o n realizada
58 * @param elemento elemento regresado por la operacion
59 */
60 public static void estado ( Stack S , String op , Object elemento ) {
61 System . out . println ( " - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - " );
62 System . out . println ( op );
63 System . out . println ( " Regresado : " + elemento );
64 String estadoVacio ;
65 if ( S . isEmpty ())
66 estadoVacio = " vac o";
67 else
68 estadoVacio = " no vac o";
69 System . out . println ( " tam = " + S . size () + " , " + estadoVacio );
70 System . out . println ( " Pila : " + S );
5.1 Pilas 167

1 public class I nv er ti r Ar re gl o {
2
3 public static <E > void invertir ( E [] a ) {
4 Stack <E > S = new ArregloStack <E >( a . length );
5 for ( int i =0; i < a . length ; i ++)
6 S . push ( a [ i ]);
7 for ( int i =0; i < a . length ; i ++)
8 a [ i ] = S . pop ();
9 }
10
11 /* * Rutina para probar la inversi o n de arreglos */
12 public static void main ( String args []) {
13 // autoboxing : a partir de Java 1.5 , hace la conversi o n de tipos
14 // primitivos a objetos autom a ticamente y viceversa .
15 // Se usa a continuaci on.
16 Integer [] a = {4 , 8 , 15 , 16 , 23 , 42};
17 String [] s = { " Jorge " ," Paco " ," Pedro " ," Juan " ," Marta " };
18 System . out . println ( " a = " + Arrays . toString ( a ));
19 System . out . println ( " s = " + Arrays . toString ( s ));
20 System . out . println ( " Invirtiendo ... " );
21 invertir ( a );
22 invertir ( s );
23 System . out . println ( " a = " + Arrays . toString ( a ));
24 System . out . println ( " s = " + Arrays . toString ( s ));
25 }
26 }

Listado 5.6: Un metodo generico que invierte los elementos en un arreglo generico
mediante una pila de la interfaz Stack<E>
168 Pilas y colas

5.1.5. Aparear par


entesis y etiquetas HTML
En esta subseccion, se exploran dos aplicaciones relacionadas de pilas, la
primera es para el apareamiento de parentesis y smbolos de agrupamiento en
expresiones aritmeticas.
Las expresiones aritmeticas pueden contener varios pares de smbolos de
agrupamiento, tales como:

Parentesis: ( y )

Llaves: { y }

Corchetes: [ y ]

Smbolos de la funcion piso: b y c

Smbolos de la funcion techo: d y e

y cada smbolo de apertura debera aparearse con su correspondiente smbolo


de cierre. Por ejemplo, un corchete izquierdo, [, debera aparearse con su
correspondiente corchete derecho, ], como en la siguiente expresion:

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

Los siguientes ejemplos ilustran este concepto:

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

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

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

Incorrecto: ({[ ])}

Incorrecto: (
5.1 Pilas 169

Un algoritmo para el apareamiento de par


entesis
Un problema importante en el procesamiento de expresiones aritmeticas es
asegurar que los smbolos de agrupamiento se apareen correctamente. Se puede
usar una pila S para hacer el apareamiento de los smbolos de agrupamiento
en una expresion aritmetica con una sola revision de izquierda a derecha. El
algoritmo prueba que los smbolos izquierdo y derecho se apareen y tambien
que ambos smbolos sean del mismo tipo.
Suponiendo que se da una secuencia X = x0 x1 x2 . . . xn1 , donde cada xi es
un smbolo que puede ser un smbolo de agrupamiento, un nombre de variable,
un operador aritmetico, o un n umero. La idea basica detras de la revision de
que los smbolos de agrupamiento en S empaten correctamente, es procesar los
smbolos en X en orden. Cada vez que se encuentre un smbolo de apertura,
se mete ese smbolo en S, y cada vez que se encuentre un smbolo de cierre, se
saca el smbolo de la cima de la pila S, suponiendo que S no esta vaca, y se
revisa que esos smbolos sean del mismo tipo. Suponiendo que las operaciones
push y pop son implementadas para ejecutarse en tiempo constante, este
algoritmo corre en tiempo O(n), o sea lineal. Se da un pseudocodigo de este
algoritmo a continuacion.
Algoritmo ApareamientoPare ntesis(X, n):
Entrada: Un arreglo X de n smbolos, cada uno de los cuales
es un smbolo de agrupamiento, una variable, un operador
aritmetico, o un numero.
Salida: true si y solo si todos los smbolos de agrupamiento
en X se aparean.
Sea S una pila vaca
para i 0 Hasta n 1 hacer
si X[i] es un smbolo de apertura entonces
S.push(X[i])
si no Si X[i] es smbolo de cierre entonces
si S.isEmpty() entonces
regresar false { nada para aparear }
si S.pop() no empata el tipo de X[i] entonces
regresar false { tipo incorrecto }
si S.isEmpty() entonces
regresar true { cada smbolo apareado }
si no
regresar false { algunos smbolos no empataron }
170 Pilas y colas

Apareamiento de etiquetas en un documento HTML


Otra aplicacion en la cual el apareamiento es importante es en la validacion
de documentos HTML. HTML es el formato estandar para documentos
hiperenlazados en el Internet. En un documento HTML, porciones de texto
estan delimitados por etiquetas HTML. Una etiqueta simple de apertura
HTML tiene la forma <nombre> y la correspondiente etiqueta de cierre tiene
la forma </nombre>. Las etiquetas HTML com unmente usadas incluyen

body: cuerpo del documento

h1: seccion cabecera

blockquote: seccion sangrada

p: parrafo

ol: lista ordenada

li: elemento de la lista

Idealmente, un documento HTML debera tener todas sus etiquetas apa-


readas, sin embargo varios navegadores toleran un cierto n umero de etiquetas
no apareadas.
El algoritmo de la seccion para el apareamiento de parentesis puede ser
usado para aparear las etiquetas en un documento HTML. En el listado 5.7
se da un programa en Java para aparear etiquetas en un documento HTML
ledo de la entrada estandar. Por simplicidad, se asume que todas las etiquetas
son etiquetas simples de apertura y cierre, es decir sin atributos, y que no
hay etiquetas formadas incorrectamente. El metodo esHTMLApareada usa una
pila para guardar los nombres de las etiquetas

5.2. 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
elementos pueden ser insertados en cualquier momento, pero solamente el
elemento que ha estado en la cola mas tiempo puede ser removido en cualquier
momento.
5.2 Colas 171

1 import java . io . IOException ;


2 import java . util . Scanner ;
3
4 /* * Prueba simplificada de apareamiento de etiquetas en un documento HTML . */
5 public class HTML {
6 /* * Quitar el primer car a cter y el u litmo de una < etiqueta > cadena . */
7 public static String q uitarExt remos ( String t ) {
8 if ( t . length () <= 2 ) return null ; // esta es una etiqueta degenerada
9 return t . substring (1 , t . length () -1);
10 }
11 /* * Probar si una etiqueta desnuda ( sin los s mbolos inicial y final )
12 * esta vac a o es una etiqueta verdadera de apertura . */
13 public static boolean e s E t i q u e t a A p e r t u r a ( String etiqueta ) {
14 return etiqueta . length ()==0 || etiqueta . charAt (0)!= / ;
15 }
16 /* * Probar si etiqueta1 desnuda aparea con etiqueta2 de cierre . */
17 public static boolean seAparean ( String etiqueta1 , String etiqueta2 ) {
18 // probar contra el nombre despu e s de /
19 return etiqueta1 . equals ( etiqueta2 . substring (1));
20 }
21 /* * Probar si cada etiqueta de apertura tiene una etiqueta de cierre . */
22 public static boolean esHTML Apareada ( String [] etiqueta ) {
23 Stack < String > S = new NodoStack < String >(); // Pila para aparear etiquetas
24 for ( int i =0; (i < etiqueta . length )&&( etiqueta [ i ]!= null ); i ++)
25 if ( e s E t i q u e t a A p e r t u r a ( etiqueta [ i ]))
26 S . push ( etiqueta [ i ]); // etiqueta apertura ; meterla en la pila
27 else {
28 if ( S . isEmpty ())
29 return false ; // nada para aparear
30 if (! seAparean ( S . pop () , etiqueta [ i ]))
31 return false ; // apareamiento incorrecto
32 }
33 if ( S . isEmpty ()) return true ; // se apareo todo
34 return false ; // hay algunas etiquetas que nunca fueron apareadas
35 }
36 public final static int CAPACIDAD = 1000; // Capacidad del arreglo etiqueta
37 /* Dividir un documento HTML en un arreglo de etiquetas html */
38 public static String [] parseHTML ( Scanner s ) {
39 String [] etiqueta = new String [ CAPACIDAD ]; // el arreglo etiqueta
40 int contador = 0; // contador de etiquetas
41 String elemento ; // elemento regresado por el scanner s
42 while ( s . hasNextLine ()) {
43 while (( elemento = s . findInLine ( " <[^ >]* > " ))!= null ) // encontrar sig etiq
44 etiqueta [ contador ++]= qui tarExtre mos ( elemento );
45 s . nextLine (); // ir a la siguiente l nea
46 }
47 return etiqueta ; // arreglo de etiquetas desnudas
48 }
49 public static void main ( String [] args ) throws IOException { // probador
50 System . out . print ( " El archivo de entrada es un documento " );
51 if ( esHTMLAp areada ( parseHTML ( new Scanner ( System . in ))))
52 System . out . println ( " HTML apareado " );
53 else
54 System . out . println ( " HTML NO apareado " );
55 }
56 }

Listado 5.7: Clase HTML para revisar el apareamiento de las etiquetas en un docu-
mento HTML.
172 Pilas y colas

Se dice que los elementos entran a una cola por la parte de atras y son
removidos del frente. La metafora para esta terminologa es una fila de gente
esperando para subir a un juego mecanico. La gente que espera para tal juego
entra por la parte trasera de la fila y se sube al juego desde el frente de la
lnea.

5.2.1. Tipo de dato abstracto cola


El tipo de dato abstracto cola define una coleccion que guarda los objetos
en una secuencia, donde el acceso al elemento y borrado estan restringidos
al primer elemento en la secuencia, el cual es llamado el frente de la cola, y
la insercion del elemento esta restringida al final de la secuencia, la cual es
llamada la parte posterior de la cola. Esta restriccion forza la regla de que
los elementos son insertados y borrados en una cola de acuerdo al principio
FIFO.
El ADT cola soporta los siguientes dos metodos fundamentales:

enqueue(e):insertar el elemento e en la parte posterior de la


cola.
dequeue():quitar el elemento del frente de la cola y regresarlo;
un error ocurre si la cola esta vaca.

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

size():regresar el numero de elementos en la cola.


isEmpty():regresar un booleano indicando si la cola esta vaca.
front():regresar el elemento del frente de la cola, sin remo-
verlo; un error ocurre si la cola esta vaca.

La siguiente tabla muestra una serie de operaciones de cola y su efecto en


una cola Q inicialmente vaca de objetos enteros. Por simplicidad, se usan
enteros en vez de objetos enteros como argumentos de las operaciones.
5.2 Colas 173

Operacion Salida f rente Q zaga


enqueue(5) (5)
enqueue(3) (5,3)
dequeue() 5 (3)
enqueue(7) (3,7)
dequeue() 3 (7)
front() 7 (7)
dequeue() 7 ()
dequeue() error ()
isEmpty() true ()
enqueue(9) (9)
enqueue(7) (9,7)
size() 2 (9,7)
enqueue(3) (9,7,3)
enqueue(5) (9,7,3,5)
dequeue() 9 (7,3,5)

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.2.2. Interfaz cola


Una interfaz de Java para el ADT cola se proporciona en el listado 5.8. Esta
interfaz generica indica que objetos de tipo arbitrario pueden ser insertados
en la cola. Por lo tanto, no se tiene que usar conversion explcita cuando se
remuevan elementos.
Los metodos size e isEmpty tienen el mismo significado como sus con-
trapartes en el ADT pila. Estos dos metodos, al igual que el metodo front,
son conocidos como metodos accesores, por su valor regresado y no cambian
el contenido de la estructura de datos.
174 Pilas y colas

1 /* *
2 * Interfaz para una cola : una colecci o n de elementos que son insertados
3 * y removidos de acuerdo con el principio primero en entrar , primero en
4 * salir .
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 }

Listado 5.8: Interfaz Queue documentada con comentarios en estilo Javadoc.


5.2 Colas 175

5.2.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
actual de objetos en la cola. Si se quiere lograr tiempo constante para cada
metodo de la cola, se necesita una aproximacion diferente.

Usar un arreglo de forma circular


Para evitar mover objetos una vez que son colocados en Q, se definen dos
variables f y r, que tienen los siguientes usos:

f es un ndice a la celda de Q guardando el primer elemento de la


cola, el cual es el siguiente candidato a ser eliminado por una operacion
dequeue, a menos que la cola este vaca, en tal caso f = r.

r es un ndice a la siguiente celda del arreglo disponible en Q.

Inicialmente, se asigna f = r = 0, lo cual indica que la cola esta vaca.


Cuando se quita un elemento del frente de la cola, se incrementa f para indicar
la siguiente celda. Igualmente, cuando se agrega un elemento, se guarda este
en la celda Q[r] y se incrementa r para indizar la siguiente celda disponible
en Q. Este esquema permite implementar los metodos front, enqueue, y
dequeue en tiempo constante, esto es, tiempo O(1). Sin embargo, todava
hay un problema con esta aproximacion.
Considerar, por ejemplo, que sucede si repetidamente se agrega y se quita
un solo elemento N veces. Se tendra f = r = N . Si se intentara entonces
insertar el elemento una vez mas, se podra obtener un error de arreglo fuera
de lmites, ya que las N localidades validas en Q son de Q[0] a Q[N 1],
aunque hay espacio en la cola para este caso. Para evitar este problema y
176 Pilas y colas

poder utilizar todo el arreglo Q, se permite que los ndices reinicien al final
de Q. Esto es, se ve ahora a Q como un arreglo circular que va de Q[0] a
Q[N 1] y entonces inmediatamente regresan a Q[0] otra vez, ver figura 5.2.

Figura 5.2: Usando arreglo Q en un modo circular: (a) la configuracion normal


con f r; (b) la configuracion envuelto alrededor de con r < f

Implementar esta vista circular de Q es facil. Cada vez que se incremente


f o r, se calcula este incremento como (f + 1) mod N o (r + 1) mod N ,
respectivamente.

El operador mod es el operador m odulo, el cual es calculado tomando


el residuo despues de la division entera. Por ejemplo, 14 dividido por 4 es 3
con residuo 2, as 14 mod 4 = 2. Especficamente, dados enteros x y y tal
que x 0 y y 0, se tiene x mod y = x bx/ycy. Esto es, si r = x mod y,
entonces hay un entero no negativo q, tal que x = qy + r. Java usa % para
denotar el operador modulo. Mediante el uso del operador modulo, se puede
ver a Q como un arreglo circular e implementar cada metodo de la cola en
una cantidad constante de tiempo, es decir, tiempo O(1). Se describe como
usar esta aproximacion para implementar una cola enseguida.
5.2 Colas 177

Algoritmo size():
return (N f + r) mod N

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
La implementacion anterior contiene un detalle importante, la cual podra
ser ignorada al principio. Considerar la situacion que ocurre si se agregan N
objetos en Q sin quitar ninguno de ellos. Se tendra f = r, la cual es la misma
condicion que ocurre cuando la cola esta vaca. Por lo tanto, no se podra
decir la diferencia entre una cola llena y una vaca en este caso. Existen varias
formas de manejarlo.
La solucion que se describe es insistir que Q no puede tener mas de
N 1 objetos. Esta regla simple para manejar una cola llena se considera
en el algoritmo enqueue dado previamente. Para se nalar que no se pueden
insertar mas elementos en la cola, se emplea una excepcion especfica, llamada
FullQueueException. Para determinar el tama no de la cola se hace con
la expresion (N f + r) mod N , la cual da el resultado correcto en una
configuracion normal, cuando f r, y en la configuracion envuelta
cuando r < f . La implementacion Java de una cola por medio de un arreglo
178 Pilas y colas

es similar a la de la pila, y se deja como ejercicio.


Al igual que con la implementacion de la pila con arreglo, la u nica real
desventaja de la implementacion de la cola con arreglo es que artificialmente
se pone la capacidad de la cola a alg un valor fijo. En una aplicacion real,
se podra actualmente necesitar mas o menos capacidad que esta, pero si se
tiene una buena estimacion de la capacidad, entonces la implementacion con
arreglo es bastante eficiente.
La siguiente tabla muestra los tiempos de ejecucion de los metodos de
una cola empleando un arreglo. Cada uno de los metodos en la realizacion
arreglo ejecuta un n umero constante de sentencias involucrando operaciones
aritmeticas, comparaciones, y asignaciones. Por lo tanto, cada metodo en esta
implementacion se ejecuta en tiempo O(1).

Metodo Tiempo
size O(1)
isEmpty O(1)
front O(1)
enqueue O(1)
dequeue O(1)

5.2.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 5.9, una implementacion del metodo queue de
la cola, y en el listado 5.10 del metodo dequeue.
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
5.2 Colas 179

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 5.9: Implementacion del metodo enqueue del ADT cola por medio de una
lista simple ligada usando nodos de la clase Nodo.

1 public E dequeue () throws E m p t y Q u e u e E x c e p t i o n {


2 if ( tam == 0)
3 throw new E m p t y Q u e u e E x c e p t i o n ( " La cola est a
vac
a . " );
4 E e = cabeza . getElemento ();
5 cabeza = cabeza . getSig ();
6 tam - -;
7 if ( tam == 0)
8 cola = null ; // la cola est a ahora vac a
9 return e ;
10 }

Listado 5.10: Implementacion del metodo dequeue del ADT cola por medio de una
lista simple ligada usando nodos de la clase Nodo.
180 Pilas y colas

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.

5.2.5. Planificador Round Robin


Un uso popular de la estructura de datos cola es para implementar un pla-
nificador round robin, donde se itera a traves de una coleccion de elementos
en forma circular y se atiende cada elemento mediante la realizacion de
una accion dada sobre este. Dicha pauta es usada, por ejemplo, para asignar
equitativamente un recurso que debera ser compartido por una coleccion
de clientes. Por ejemplo, se puede usar un planificador round robin para
asignar una parte del tiempo del CPU a varias aplicaciones ejecutandose
concurrentemente en una computadora.
Se puede implementar un planificador round robin usando una cola, Q,
mediante la repeticion de los siguientes pasos:
1. e Q.dequeue()
2. Servir elemento e
3. Q.enqueue(e)

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

Resolver el problema de Josefo con una cola


Se puede resolver el problema de Josefo para una coleccion de n elementos
usando una cola, mediante la asociacion de la papa con el elemento en el
5.3 Colas con doble terminaci
on 181

frente de la cola y guardando elementos en la cola de acuerdo a su orden


alrededor del crculo. Por lo tanto, pasar la papa es equivalente a retirar un
elemento de la cola e inmediatamente agregarlo otra vez. Despues de que
este proceso se ha realizado k veces, se quita el elemento del frente usando
dequeue y descartandolo. Se muestra en el listado 5.11, un programa Java
completo para resolver el problema de Josefo, el cual describe una solucion
que se ejecuta en tiempo O(nk).

1 public class Josefo {


2 /* * Soluci o n del problema de Josefo usando una cola . */
3 public static <E > E Josefo ( Queue <E > Q , int k ) {
4 if ( Q . isEmpty ()) return null ;
5 while ( Q . size () > 1 ) {
6 System . out . println ( " Cola : " + Q + " k = " + k );
7 for ( int i =0; i < k ; i ++)
8 Q . enqueue ( Q . dequeue ()); // mover el elemento del frente al final
9 E e = Q . dequeue (); // quitar el elemento del frente de la coleccci o n
10 System . out . println ( " \ t " + e + " est
a eliminad@ " );
11 }
12 return Q . dequeue (); // el ganador
13 }
14 /* * Construir una cola desde un arreglo de objetos */
15 public static <E > Queue <E > construirCola ( E a []) {
16 Queue <E > Q = new ColaNodo <E >();
17 for ( E e : a )
18 Q . enqueue ( e );
19 return Q ;
20 }
21 /* * M e
todo probador */
22 public static void main ( String [] args ) {
23 String [] a1 = { " Jorge " ," Pedro " ," Carlos " };
24 String [] a2 = { " Gabi " ," To ~ n o " ," Paco " ," Mari " ," Iv
a n " ," Alex " ," N
e stor " };
25 String [] a3 = { " Rigo " ," Edgar " ," Karina " ," Edgar " ," Temo " ," H e ctor " };
26 System . out . println ( " Primer ganador es " + Josefo ( construirCola ( a1 ) ,7));
27 System . out . println ( " Segundo ganador es " + Josefo ( construirCola ( a2 ) ,10));
28 System . out . println ( " Tercer ganador es " + Josefo ( construirCola ( a3 ) ,2));
29 }
30 }

Listado 5.11: Un programa Java completo para resolver el problema de Josefo


usando una cola. La clase ColaNodo es mostrada en los listados 5.9 y 5.10.

5.3. Colas con doble terminaci


on
Considerar una estructura de datos parecida a una cola que soporte
insercion y borrado tanto en el frente y la parte posterior de la cola. Tal
182 Pilas y colas

extension de una cola es llamada una cola con doble terminaci on o en


ingles double-ended queue, o deque, esta ultima se pronuncia dek, para
evitar confusion con el metodo dequeue de un ADT cola regular, que se
pronuncia como dikiu

5.3.1. Tipo de dato abstracto deque


El tipo de dato abstracto es mas completo que los ADT pila y cola. Los
metodos fundamentales del ADT deque son los siguientes:
addFirst(e):inserta un nuevo elemento e al inicio de la deque.
addLast(e):inserta un nuevo elemento e al final de la deque.
removeFirst():retira y regresa el primer elemento de la deque; un
error ocurre si la deque esta vaca.
removeLast():retira y regresa el ultimo elemento de la deque; un
error ocurre si la deque esta vaca.
Adicionalmente, el ADT deque podra tambien incluir los siguientes meto-
dos de apoyo:
getFirst():regresa el primer elemento de la deque; un error
ocurre si la deque esta vaca.
getLast():regresa el u ltimo elemento de la deque; un error
ocurre si la deque esta vaca.
size():regresa el numero de elementos de la deque.
isEmpty():determina si la deque esta vaca.
La siguiente tabla muestra una serie de operaciones y sus efectos sobre
una deque D inicialmente vaca de objetos enteros. Por simplicidad, se usan
enteros en vez de objetos enteros como argumentos de las operaciones.
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 ()
5.3 Colas con doble terminaci
on 183

5.3.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 5.12 y una
implementacion de esta interfaz en el listado 5.13.
184 Pilas y colas

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 */
20 public E getFirst () throws E m p t y D e q u e E x c e p t i o n ;
21 /* *
22 * Regresa el u ltimo elemento ; una excepci o n es lanzada si la deque
23 * est a vac
a.
24 */
25 public E getLast () throws E m p t y D e q u e E x c e p t i o n ;
26 /* *
27 * Insertar un elemento para que sea el primero en la deque .
28 */
29 public void addFirst ( E elemento );
30 /* *
31 * Insertar un elemento para que sea el u ltimo en la deque .
32 */
33 public void addLast ( E elemento );
34 /* *
35 * Quita el primer elemento ; una excepci o n es lanzada si la deque
36 * est a vac
a.
37 */
38 public E removeFirst () throws E m p t y D e q u e E x c e p t i o n ;
39 /* *
40 * Quita el u ltimo elemento ; una excepci o n es lanzada si la deque
41 * est a vac
a.
42 */
43 public E removeLast () throws E m p t y D e q u e E x c e p t i o n ;
44 }

Listado 5.12: 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.
5.3 Colas con doble terminaci
on 185

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 /* *
27 * Esta funci o n regresa true si y s o lo si la deque est a vac
a.
28 * @return true si la deque est a vac a , false de otro modo .
29 */
30 public boolean isEmpty () {
31 return tam == 0;
32 }
33 /* *
34 * Devolver el primer elemento sin modificar la deque .
35 * @return El primer elemento en la secuencia .
36 */
37 public E getFirst () throws E m p t y D e q u e E x c e p t i o n {
38 if ( isEmpty ())
39 throw new E m p t y D e q u e E x c e p t i o n ( " Deque est a vac
a . " );
40 return cabeza . getSig (). getElemento ();
41 }
42 /* *
43 * Devolver el u ltimo elemento sin modificar la deque .
44 * @return El u ltimo elemento en la secuencia .
45 */
46 public E getLast () throws E m p t y D e q u e E x c e p t i o n {
47 if ( isEmpty ())
48 throw new E m p t y D e q u e E x c e p t i o n ( " Deque est a vac
a . " );
49 return cola . getPrev (). getElemento ();
50 }
51
52 public void addFirst ( E o ) {
53 NodoDL <E > segundo = cabeza . getSig ();
54 NodoDL <E > primero = new NodoDL <E >( o , cabeza , segundo );
55 segundo . setPrev ( primero );
56 cabeza . setSig ( primero );
57 tam ++;
58 }
59
60 public void addLast ( E o ) {
61 NodoDL <E > penultimo = cola . getPrev ();
62 NodoDL <E > ultimo = new NodoDL <E >( o , penultimo , cola );
63 penultimo . setSig ( ultimo );
64 cola . setPrev ( ultimo );
65 tam ++;
66 }
67
68 public E removeFirst () throws E m p t y D e q u e E x c e p t i o n {
69 if ( isEmpty ())
70 throw new E m p t y D e q u e E x c e p t i o n ( " Deque est
a vac
a . " );
186 Pilas y colas
Captulo 6

Listas e Iteradores

6.1. Lista arreglo


Supongase que se tiene una coleccion S de n elementos guardados en un
cierto orden lineal, de este modo se puede referir a los elementos en S como
primero, segundo, tercero, etc. Tal coleccion es generalmente referida como
una lista o secuencia. Se puede referir u nicamente a cada elemento e en S
usando un entero en el rango [0, n 1], esto es igual al n umero de elementos
de S que preceden a e en S. El ndice de un elemento e en S es el n umero
de elementos que estan antes que e en S. Por lo tanto, el primer elemento
en S tiene ndice cero y el u ltimo ndice tiene ndice n 1. Tambien, si un
elemento de S tiene ndice i, su elemento previo, si este existe, tiene ndice
i 1, y su siguiente elemento, si este existe, tiene ndice i + 1. Este concepto
de ndice esta relacionado al de rango de un elemento en una lista, el cual es
definido para que sea uno mas que su ndice; por lo que el primer elemento
esta en el rango uno, el segundo esta en rango dos, etc.
Una secuencia que soporte acceso a sus elementos mediante sus ndices
es llamado una lista arreglo, o tambien con el termino mas viejo vector.
Como la definicion de ndice es mas consistente con la forma como los arreglos
son ndizados en Java y otros lenguajes de programacion, como C y C++, se
hara referencia al lugar donde un elemento es guardado en una lista arreglo
como su ndice, no su rango.
Este concepto de ndice es simple pero potente, ya que puede ser usado
para indicar donde insertar un nuevo elemento en una lista o de donde quitar
un elemento viejo.
188 Listas e Iteradores

6.1.1. El tipo de dato abstracto lista arreglo


Como un ADT, una lista arreglo S tiene los siguientes metodos, ademas
de los metodos estandares size() e isEmpty():

get(i):regresa el elemento de S con ndice i; una condicion


de error ocurre si i < 0 o i >size()1.
set(i, e):reemplaza con e y regresa el elemento en el ndice
i; una condicion de error ocurre si i < 0 o i >
size() 1.
add(i, e):inserta un nuevo elemento e en S para tener ndice
i; una condicion de error ocurre si i < 0 o i >
size().
remove(i):quita de S el elemento en el ndice i; una condicion
de error ocurre si i < 0 o i > size() 1.

No se insiste en que un arreglo debera ser usado para implementar una


lista arreglo, as el elemento en el ndice 0 esta guardado en el ndice 0 en el
arreglo, aunque esta es una posibilidad muy natural. La definicion de ndice
ofrece una forma para referirse al lugar donde un elemento esta guardado en
una secuencia sin tener que preocuparse acerca de la implementacion exacta
de esa secuencia. El ndice de un elemento podra cambiar cuando la secuencia
sea actualizada, como se muestra en el siguiente ejemplo.

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)
6.1 Lista arreglo 189

6.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 6.1:

Metodo Deque Realizacion con Metodos


de Lista Arreglo
size(), isEmpty() size(), isEmpty()
getFirst() get(0)
getLast() get(size()-1)
addFirst(e) add(0,e)
addLast(e) add(size(),e)
removeFirst() remove(0)
removeLast() remove(size()-1)

Cuadro 6.1: Realizaci


on de una deque por medio de una lista arreglo.

6.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
190 Listas e Iteradores

involucra el desplazamiento de elementos hacia adelante y hacia atras para


mantener las celdas ocupadas en el arreglo de forma continua. Estas opera-
ciones de desplazamiento son requeridas para mantener la regla de guardar
siempre un elemento cuyo ndice en la lista es i en el ndice i del arreglo A,
ver figura 6.1.
Algoritmo add(i, e):
para j n 1, n 2, . . . , i hacer
A[j + 1] A[j] {hacer espacio para el nuevo elemento}
A[i] e
nn+1

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

Figura 6.1: Implementacion basada en un arreglo de una lista arreglo S que


esta guardando n elementos: (a) desplazando hacia adelante para una insercion en
el ndice i; (b) desplazamiento hacia atras para un borrado en el ndice i.
6.1 Lista arreglo 191

Rendimiento de una implementaci


on simple con un arreglo

El cuadro 6.2 muestra los tiempos de ejecucion en el peor caso de un


arreglo lista con n elementos realizados por medio de un arreglo. Los metodos
isEmpty, size, get y set se ejecutan en tiempo O(1), pero los metodos
de insercion y borrado pueden tomar mas tiempo que este. En particular,
add(i, e) se ejecuta en tiempo O(n). En efecto, el peor caso para esta operacion
ocurre cuando i es cero, ya que todos los n elementos existentes tienen que
ser desplazados hacia adelante. Un argumento similar se aplica al metodo
remove(i), el cual corre en tiempo O(n), porque se tiene que desplazar hacia
atras n 1 elementos en el peor caso cuando i es igual a cero. De hecho,
suponiendo que cada ndice posible es igualmente probable de que sea pasado
como un argumento a estas operaciones, su tiempo promedio es O(n), ya que
se tienen que desplazar n/2 elementos en promedio.

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)

Cuadro 6.2: Rendimiento de un lista arreglo con n elementos usando un arreglo. El


espacio usado es O(N ), donde N es el tama no del arreglo.

Para add(i, e) y remove(i) se observa que el tiempo en cada ejecucion es


O(ni+1), ya que solo aquellos elementos con ndice igual o mayor a i tienen
que ser desplazadas hacia atras o adelante. As, insertar y quitar un elemento
al final de una lista arreglo, usando los metodos add(n, e) y remove(n 1),
respectivamente toman tiempo O(1) cada uno. Por otra parte, esta observacion
tiene una consecuencia interesante para la adaptacion del ADT lista arreglo
al ADT deque, dado en la seccion 5.12, si el ADT lista arreglo en este caso es
implementado por medio de un arreglo como se describe previamente, entonces
los metodos addLast y removeLast del deque se ejecutan en tiempo O(1).
Sin embargo, los metodos addFirst y removeFirst del deque se ejecutan en
tiempo O(n).
192 Listas e Iteradores

Actualmente, con un poco de esfuerzo, se puede generar una implementa-


cion basada en un arreglo para que el ADT lista arreglo logre tiempo O(1)
para inserciones y borrados en el ndice 0, al igual que inserciones y borrados
al final de la lista arreglo. Para lograrlo se requiere que no se de la regla de
que un elemento con ndice i es guardado en el arreglo en el ndice i, se
ocupa en este caso usar una aproximacion de arreglo circular como la que fue
usada en la seccion 5.2 para implementar una cola.

6.1.4. Una interfaz simple y la clase java.util.ArrayList


Para construir una implementacion Java del ADT lista arreglo, se mues-
tra, en el listado 6.1, una interfaz Java, ListaIndice, que captura los
metodos principales del ADT lista de arreglo. En este caso, se usa una
IndexOutOfBoundsException para indicar un argumento con ndice invalido.

1 // import java . util . Iterator ;


2 /* *
3 * Una interfaz para listas arreglo .
4 */
5 public interface ListaIndice <E > {
6 /* * Regresa el n u mero de elementos en esta lista . */
7 public int size ();
8 /* * Prueba si la lista vac a . */
9 public boolean isEmpty ();
10 /* * Insertar un elemento e para estar en el ndice i ,
11 * desplazando todos los elementos despu e s de este . */
12 public void add ( int i , E e )
13 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 ;
14 /* * Regresar el elemento en el ndice i , sin quitarlo . */
15 public E get ( int i )
16 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 ;
17 /* * Quita y regresa el elemento en el ndice i ,
18 * desplazando los elementos despu e s de este . */
19 public E remove ( int i )
20 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 ;
21 /* * Reemplaza el elemento en el ndice i con e ,
22 * regresando el elemento previo en i . */
23 public E set ( int i , E e )
24 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 ;
25 }

Listado 6.1: La interfaz ListaIndice para el ADT lista arreglo.


6.1 Lista arreglo 193

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
todos los metodos dados en el listado 6.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.

6.1.5. Lista arreglo usando un arreglo extendible


Ademas de implementar los metodos de la interfaz ListaIndice y algunos
otros metodos u tiles, la clase java.util.ArrayList proporciona una carac-
terstica interesante que supera una debilidad en la implementacion simple de
la lista arreglo.
La mayor debilidad de la implementacion simple para el ADT lista arreglo
dado en la seccion 6.1.3, es que esta requiere una especificacion avanzada de la
capacidad fija, N , para el n umero total de elementos que podran ser guardados
en la lista arreglo. Si el n umero actual de elementos, n, de la lista arreglo es
mucho menor que N , entonces la implementacion desperdiciara espacio. Peor
a
un, si n se incrementa y rebasa a N , entonces esta implementacion fallara.
Para evitar lo anterior, java.util.ArrayList usa una tecnica interesante
de arreglo extendible con lo que no habra preocupacion de que el arreglo sea
desbordado cuando se usa esta clase.
Al igual que la clase java.util.ArrayList, se proporciona un medio
para crecer el arreglo A que guarda los elementos de una lista arreglo S. Por
supuesto, en Java y en otros lenguajes de programacion, no se puede crecer
el arreglo A; su capacidad esta fija a alg un n
umero N , como ya se ha visto.
Cuando un desbordamiento ocurre, es decir, n = N y se hace una llamada al
metodo add, se realizan los siguientes pasos adicionales.
194 Listas e Iteradores

1. Reservar un nuevo arreglo B de capacidad 2N .

2. Hacer B[i] A[i], para i = 0, . . . , N 1.

3. Hacer A B, esto es, se usa B como el arreglo que soporta a S.

4. Insertar el nuevo elemento en A.

Esta estrategia de reemplazo de arreglo es conocida como arreglo exten-


dible, y puede ser vista como extender la terminacion del arreglo subyacente
para hacerle espacio para mas elementos, ver figura 6.2. Intuitivamente, este
estrategia es parecida a la del cangrejo ermita no, el cual se cambia a un
caracol mas grande cuando sobrepasa al anterior.

Figura 6.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 6.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 )
6.1 Lista arreglo 195

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
20 for ( int i =0; i < tam ; i ++)
21 B [ i ] = A [ i ];
22 A = B;
23 }
24 for ( int i = tam -1; i >= r ; i - -) // desplazar elementos hacia adelante
25 A [ i +1] = A [ i ];
26 A[r] = e;
27 tam ++;
28 }
29 /* * Quitar el elemento guardado en el ndice dado . */
30 public E remove ( int r )
31 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 {
32 revisarIndice (r , size ());
33 E temp = A [ r ];
34 for ( int i = r ; i < tam -1; i ++) // desplazar elementos hacia atr as
35 A [ i ] = A [ i +1];
36 tam - -;
37 return temp ;
38 }
39 /* * Revisa si el ndice dado est a en el rango [0 , n - 1] */
40 protected void revisarIndice ( int r , int n ) //
41 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 { //
42 if ( r < 0 || r >= n )
43 throw new 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 ( "
I ndice ilegal : " + r );
44 }
45 /* * Regresar el n u mero de elementos en la lista indizada . */

Listado 6.2: Parte de la clase ArregloListaIndice realizando el ADT lista arreglo


por medio de un arreglo extendible. El metodo revisarIndice vigila que el ndice
r este entre 0 y n 1.

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 6.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.
196 Listas e Iteradores

Cantidad Tiempo Tamano


de elementos de ejecucion del arreglo
1 1 1
2 2 2
3 3 4
4 1 4
5 5 8
6 1 8
7 1 8
8 1 8
9 9 16
10 1 16
11 1 16
12 1 16
13 1 16
14 1 16
15 1 16
16 1 16

Cuadro 6.3: Tiempo de ejecucion para la insercion de elementos en un arreglo con


reemplazo.
6.1 Lista arreglo 197

Usando un patron de diseno algortmico llamado amortizaci on, 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
conteo donde se ve a la computadora como una aplicacion operada con
monedas que requiere el pago de un ciber-peso para una cantidad constante de
tiempo computacional. Cuando una operacion es ejecutada, se deberan tener
suficientes ciber-pesos disponibles en la cuenta bancaria para pagar las
tiempos de ejecucion de las operaciones. As, la cantidad total de ciber-pesos
gastados para cualquier computo sera proporcional al tiempo gastado en
ese computo. La ventaja de usar este metodo de analisis es que se pueden
sobrecargar algunas operaciones para ahorrar ciber-pesos para pagar otras.

Proposici on 6.1: Sea S una lista arreglo implementada por medio de un


arreglo extendible con un tama no inicial uno. El tiempo total para realizar
una serie de n inserciones al final de la lista en S, iniciando con S vaca es
O(n)

Justificacion. Suponiendo que un ciber-peso es suficiente para pagar por


la operacion de insercion al final de la lista en S, sin considerar el tiempo
gastado en crecer el arreglo. Suponer que el crecimiento del arreglo de tama no
k a 2k requiere k ciber-pesos por el tiempo gastado en copiar los elementos.
Se cargara en cada operacion de insercion tres ciber-pesos. As, se sobrecarga
cada operacion de insercion que no causa un sobreflujo por dos ciber-pesos.
Estos dos ciber-pesos se pueden pensar como un ahorro. Un sobreflujo ocurre
cuando la lista arreglo S tiene 2i elementos, para alg un i 0, y el tama no
del arreglo usado es 2i . Entonces, doblar el tama no del arreglo requerira 2i
ciber-pesos. Estos ciber-pesos se pueden tomar de los elementos guardados
desde la celda 2i1 hasta 2i 1 (ver figura 6.3). Observar que sobreflujo
previo ocurrio cuando la cantidad de elementos se hizo mas grande que 2i1 la
primera vez, y de esta forma los ciber-pesos ahorrados en las celdas 2i1 hasta
2i 1 no fueron previamente gastados. Por lo tanto, se tiene un esquema valido
de amortizacion en el cual cada operacion es cargada con tres ciber-pesos
y todos los tiempos de computo son pagados. Es decir, se puede pagar la
ejecucion de n inserciones al final usando 3n ciber-pesos. Es decir, el tiempo
de ejecucion amortizado para cada operacion de insercion al final es O(1);
por lo tanto, el tiempo total de ejecucion de n operaciones de inserciones al
final es O(n).
198 Listas e Iteradores

Figura 6.3: Ilustracion de una serie de operaciones de inserci on al final de la lista


arreglo: (a) un arreglo de 8 celdas lleno, con dos ciber-pesos ahorrados en las celdas
del 4 al 7; (b) una operacion de insercion causa un sobreflujo y un doblado de
la capacidad. Copiar los 8 viejos elementos al nuevo arreglo se ha pagado con
los ciber-pesos ahorrados. Insertar el nuevo elemento es pagado por uno de los
ciber-pesos dados, y dos ciber-pesos son ahorrados en la celda 8.

6.2. Listas nodo


Usar un ndice no es la unica forma de referirse al lugar donde un elemento
aparece en una secuencia. Si se tiene una secuencia S implementada con una
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 3.3 y 3.4,
usando el ADT posicion relacionada, el cual abstrae la nocion de lugar en
una lista nodo.

6.2.1. Operaciones basadas en nodos


Sea S una lista enlazada simple o doble. Se deseara definir metodos para
S que tomen nodos como parametros y proporcionen nodos como tipos de
regreso. Tales metodos podran dar aumentos de velocidad significativos sobre
los metodos basados en ndices, porque encontrar el ndice de un elemento
en una lista enlazada requiere buscar a traves de la lista incrementalmente
desde el inicio o final, contando los elementos conforme se recorra.
6.2 Listas nodo 199

Por ejemplo, se podra definir un metodo hipotetico remove(v) que quite


el elemento de S guardado en el nodo v de la lista. Usando un nodo como un
parametro permite remover un elemento en tiempo O(1) yendo directamente
al lugar donde esta guardado el nodo y entonces desenlazando este nodo
mediante una actualizacion a los enlaces sig y prev de sus vecinos. De igual
modo, se puede 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 posicion,
la cual formaliza la nocion intuitiva de lugar de un elemento relativo a los
otros en la lista.

6.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.
200 Listas e Iteradores

6.2.3. El tipo de dato abstracto lista nodo

Usando el concepto de posicion para encapsular la idea de nodo en


una lista, se puede definir otro tipo de ADT secuencia llamado el ADT lista
nodo. Este ADT soporta los siguientes metodos para una lista S:

first():regresa la posicion del primer elemento de S; un


error ocurre si S esta vaca.
last():regresa la posicion del u ltimo elemento de S; un
error ocurre si S esta vaca.
prev(p):regresa la posicion del elemento de S precediendo
al de la posicion p; un error ocurre si p esta en la
primera posicion.
next(p):regresa la posicion del elemento de S siguiendo al
de la posicion p; un error ocurre si p esta en la
u
ltima posicion.

Los metodos anteriores permiten referirse a posiciones relativas en una


lista, comenzando en el inicio o final, y moviendose incrementalmente a la
izquierda o a la derecha de la lista. Estas posiciones pueden ser pensadas como
nodos en la lista pero, se debe observar que no hay referencias especficas a
objetos nodos. Ademas, si se proporciona una posicion como un argumento a
un metodo lista, entonces la posicion debera representar una posicion valida
en esa lista.

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.
6.2 Listas nodo 201

set(p, e):reemplaza el elemento en la posicion p con e, re-


gresando el elemento anterior en la posicion p.
addFirst(e):inserta un nuevo elemento e en S como el primer
elemento.
addLast(e):inserta un nuevo elemento e en S como el u ltimo
elemento.
addBefore(p, e):inserta un nuevo elemento e en S antes de la posi-
cion p.
addAfter(p, e):inserta un nuevo elemento e en S despues de la
posicion p.
remove(p):quita y regresa el elemento e en la posicion p en
S, invalidando esta posicion en S.

El ADT lista nodo permite ver una coleccion ordenada de objetos en


terminos de sus lugares, sin tenerse que preocupar acerca de la forma exacta
como estos lugares estan representados, ver figura 6.4.

Figura 6.4: Una lista nodo. Las posiciones en el orden actual son p, q, r, y s.

Podra parecer a primera vista que hay redundancia en el repertorio


anterior de operaciones para el ADT lista nodo, ya que se puede hacer la ope-
racion addFirst(e) con addBefore(first(),e), y la operacion addLast(e)
con addAfter(last(),e). Pero estas sustituciones solo pueden ser hechas
para una lista no vaca.
Observar que una condicion de error ocurre si una posicion pasada como
argumento a una de las operaciones de la lista es invalida. Las razones para
que una posicion p sea invalida incluyen:

p = null

p fue previamente borrada de la lista

p es una posicion de una lista diferente


202 Listas e Iteradores

p es la primera posicion de la lista y se llama prev(p)

p es la u
ltima posicion de la lista y se llama next(p)

Se muestra enseguida una serie de operaciones para una lista nodo S


inicialmente vaca. Se usan las variables p1 , p2 , etc., para denotar diferentes
posiciones, y se muestra el objeto guardado actualmente en tal posicion entre
parentesis.

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)

El ADT lista nodo, con su nocion incorporada de posicion, es u til para


realizar ajustes. Por ejemplo, un programa que simula un juego de cartas
podra simular la mano de cada persona como una lista nodo. Ya que la
mayora de las personas guarda las cartas de la misma figura juntas, insertar
y sacar cartas de la mano de una persona podra ser implementado usando los
metodos del ADT lista nodo, con las posiciones siendo determinadas por un
orden natural de las figuras. Igualmente, un editor de texto simple encaja la
nocion de insercion posicional y remocion, ya que tales editores tpicamente
realizan todas las actualizaciones relativas a un cursor, el cual representa la
posicion actual en la lista de caracteres de texto que esta siendo editada.
Una interfaz Java representando el ADT posicion esta dado en el listado
6.3.
Una interfaz, con los metodos requeridos, para el ADT lista nodo, llamada
ListaPosicion, esta dada en el listado 6.4. Esta interfaz usa las siguientes
excepciones para indicar condiciones de error.
6.2 Listas nodo 203

1 public interface Posicion <E > {


2 /* * Regresa el elemento guardado en esta posici
o n . */
3 E elemento ();
4 }

Listado 6.3: Una interfaz Java para el ADT posicion.

BoundaryViolationException: lanzada si un intento es hecho al acceder


una elemento cuya posicion esta fuera del rango de posiciones de la
lista, por ejemplo llamando al metodo next en la u
ltima posicion de la
secuencia.
InvalidPositionException: lanzada si una posicion dada como argumento
no es valida, por ejemplo, esta es una referencia null o esta no tiene
lista asociada.

Otro adaptador Deque


El ADT lista nodo es suficiente para definir una clase adaptadora para el
ADT deque, como se muestra en la tabla 6.4.

Metodo Deque Realizacion con Metodos


de la lista nodo
size(), isEmpty() size(), isEmpty()
getFirst() first().elemento()
getLast() last().elemento()
addFirst(e) addFirst(e)
addLast(e) addLast(e)
removeFirst() remove(first())
removeLast() remove(last())

Cuadro 6.4: Realizaci


on de una deque por medio de una lista nodo.

6.2.4. Implementaci
on lista doblemente enlazada
Si se quiere implementar el ADT lista nodo usando una lista doblemente
enlazada, seccion 3.4, se debe hacer que los nodos de la lista enlazada im-
204 Listas e Iteradores

1 import java . util . Iterator ;


2 /* *
3 * Una interfaz para listas posicionales .
4 *
5 */
6 public interface ListaPosicion <E > extends Iterable <E > {
7 /* * Regresa un iterador de todos los elementos en la lista . */
8 public Iterator <E > iterator ();
9 /* * Regresa una colecci o n iterable de todos los nodos en la lista . */
10 public Iterable < Posicion <E > > positions ();
11 /* * Regresa el n u mero de elementos en esta lista . */
12 public int size ();
13 /* * Indica si la lista est a vac a . */
14 public boolean isEmpty ();
15 /* * Regresa el primer nodo en la lista . */
16 public Posicion <E > first ();
17 /* * Regresa el u ltimo nodo en la lista . */
18 public Posicion <E > last ();
19 /* * Regresa el nodo siguiente a un nodo dado en la lista . */
20 public Posicion <E > next ( Posicion <E > p )
21 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 ;
22 /* * Regresa el nodo anterior a un nodo dado en la lista . */
23 public Posicion <E > prev ( Posicion <E > p )
24 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 ;
25 /* * Inserta un elemento en el frente de la lista ,
26 * regresando la nueva posici o n . */
27 public void addFirst ( E e );
28 /* * Inserta un elemento en la parte de atr a s de la lista ,
29 * regresando la nueva posici o n . */
30 public void addLast ( E e );
31 /* * Inserta un elemento despu e s del nodo dado en la lista . */
32 public void addAfter ( Posicion <E > p , E e )
33 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 ;
34 /* * Inserta un elemento antes del nodo dado en la lista . */
35 public void addBefore ( Posicion <E > p , E e )
36 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 ;
37 /* * Quita un nodo de la lista , regresando el elemento guardado . */
38 public E remove ( 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 ;
39 /* * Reemplaza el elemento guardado en el nodo dado ,
40 regresando el elemento viejo . */
41 public E set ( Posicion <E > p , E e ) 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 ;
42 }

Listado 6.4: Interfaz para el ADT lista nodo.


6.2 Listas nodo 205

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 6.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 3.11, 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 ) {
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 6.5: Clase NodoD realizando un nodo de una lista doblemente enlazada
implementando la interfaz Posicion.

Dada una posicion p en S, se puede desenvolver p para revelar el nodo


206 Listas e Iteradores

v subyacente mediante una conversion de la posicion a un nodo. Una vez que


se tiene un nodo v, se puede, por ejemplo, implementar el metodo prev(p)
con v.getPrev, a menos que el nodo regresado por v.getPrev sea la cabeza,
en tal caso se senala un error. Por lo tanto, posiciones en una implementacion
lista doblemente enlazada se pueden soportar en una forma orientada al objeto
sin ningun gasto adicional de tiempo o espacio.
Considerar ahora como se podra implementar el metodo addAfter(p, e),
para insertar un elemento e despues de la posicion p. Se crea un nuevo nodo v
para mantener el elemento e, se enlaza v en su lugar en la lista, y entonces se
actualizan las referencias next y prev de los dos vecinos de v. Este metodo
esta dado en el siguiente pseudocodigo, y se ilustra en la figura 6.5, recordando
el uso de centinelas, observar que este algoritmo trabaja a un si p es la ultima
posicion real.
Algoritmo addAfter(p, e):
Crear un nuevo nodo v
v.setElemento(e)
v.setPrev(p) { enlazar v a su predecesor }
v.setNext(p.getNext()) { enlazar v a su sucesor }
(p.getNext()).setPrev(v) { enlazar el viejo sucesor de p a v }
p.setNext(v) { enlazar p a su nuevo sucesor, v }
Los algoritmos para los metodos addBefore, addFirst, y addLast son
similares al metodo addAfter. Se dejan sus detalles como un ejercicio.
Considerar, ahora, el metodo remove(e), el cual quita el elemento e guar-
dado en la posicion p. Para realizar esta operacion, se enlazan los dos vecinos
de p para referirse entre ellos como nuevos vecinosdesenlazando p. Observar
que despues de que p esta desenlazado, no hay nodos apuntando a p; por lo
tanto, el recolector de basura puede reclamar el espacio de p. Este algoritmo
esta dado en el siguiente pseudocodigo y se ilustra en la figura 6.6. Recordando
el uso de los centinelas cabeza y cola, este algoritmo trabaja a un si p es la
primera posicion, la ultima, o solamente una posicion real en la lista.
Algoritmo remove(p):
t p.elemento() { una variable temporal para guardar el valor de regreso }
(p.getPrev()).setNext(p.getNext()) { desenlazar p }
(p.getNext()).setPrev(p.getPrev())
p.setPrev(null) { invalidar la posicion p }
p.setNext(null)
regresar t
6.2 Listas nodo 207

Figura 6.5: Agregar un nuevo nodo despues de la posici on JFK:(a) antes de la


insercion; (b) creacion de un nodo v con el elemento BWI y el enlazado a la lista;
(c) despues de la insercion.

Figura 6.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.
208 Listas e Iteradores

En conclusion, usando una lista doblemente enlazada, se pueden ejecu-


tar todos los metodos del ADT lista nodo en tiempo O(1). As, una lista
doblemente enlazada es una implementacion eficiente del ADT lista nodo.

Implementaci
on de la lista nodo en Java
La clase Java ListaNodoPosicion, la cual implementa el ADT lista nodo
usando una lista doblemente enlazada, se muestra en el listado 6.6. En esta
clase se tienen declaradas variables de instancia para guardar el n umero
de elementos en la lista, as como los centinelas especiales para manejar
mas facilmente la lista. Tambien se tiene un constructor sin parametros que
permite crear una lista nodo vaca. En esta clase ademas de implementar los
metodos declarados en la interfaz ListaPosicion se tienen otros metodos
de conveniencia, por ejemplo para revisar si una posicion es valida, probar
si una posicion es la primera o la ultima, intercambiar los contenidos de dos
posiciones, u obtener una representacion textual de la lista.

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

6.3.1. Tipos de dato abstracto Iterador e Iterable


Un iterador (iterator) es un patron de dise no de software que abstrae
el proceso de explorar un elemento a la vez desde una coleccion de elementos.
Un iterador consiste de una secuencia S, un elemento actual de S, y una
forma de pasar al siguiente elemento en S y hacerlo el elemento actual. As,
un iterador extiende el concepto del ADT posicion que se introdujo en la
seccion 6.2. De hecho, una posicion puede ser pensada como un iterador que
no va a ninguna parte. Un iterador encapsula los conceptos de lugar y
siguiente en una coleccion de objetos.
Se define el ADT iterador soportando los siguientes dos metodos:

hasNext():prueba si hay elementos pendientes en el iterador.


next():regresa el siguiente elemento en el iterador.
6.3 Iteradores 209

1 import java . util . Iterator ;


2 /* *
3 * Realizaci o n de una ListaPosici o n usando una lista doblemente enlazada
4 * de nodos .
5 *
6 */
7 public class ListaNodoPosicion <E > implements ListaPosicion <E > {
8 protected int tam ; // n u mero de elementos en la lista
9 protected NodoD <E > cabeza , cola ; // centinelas especiales
10 /* * Constructor que crea una lista vac a ; tiempo O (1) */
11 public L i s t a N o d o P o s i c i o n () {
12 tam = 0;
13 cabeza = new NodoD <E >( null , null , null ); // crea cabeza
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 on ( " 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 on ( " 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 );
210 Listas e Iteradores

El ADT iterador usa el concepto de cursor como el elemento actual en


el recorrido de una secuencia. El primer elemento en un iterador es regresado
por la primera llamada al metodo next, asumiendo que el iterador contiene
al menos un elemento.
Un iterador da un esquema unificado para acceder a todos los elementos de
una coleccion de objetos en una forma que es independiente de la organizacion
especfica de la coleccion. Un iterador para una lista arreglo, lista nodo, o
secuencia debera regresar los elementos de acuerdo a su orden lineal.

Iteradores simples en Java


Java proporciona un iterador a traves de su interfaz java.util.Iterator.
La clase java.util.Scanner implementa esta interfaz. Esta interfaz soporta
un metodo adicional opcional para quitar el elemento previamente regresado
de la coleccion. Esta funcionalidad, de quitar elementos a traves de un iterador,
es algo controversial desde un punto de vista orientado al objeto, sin embargo,
no es sorprendente que su implementacion por las clases es opcional. Java
tambien da la interfaz java.util.Enumeration, la cual es historicamente
mas vieja que la interfaz iterador y usa los nombres hasMoreElements() y
nextElement().

El tipo de dato abstracto Iterable


Para dar un mecanismo unificado generico para explorar a traves de una
estructura de datos, los ADT que guardan colecciones de objetos deberan
soportar el siguiente metodo:

iterator():regresa un iterador de los elementos en la coleccion.

Este metodo esta soportado por la clase java.util.ArrayList y es


muy importante, por lo que hay una interfaz completa java.lang.Iterable
conteniendo solo este metodo. Este metodo puede simplificar al usuario la
tarea de recorrer los elementos de una lista. Para garantizar que una lista
nodo soporta el metodo anterior, por ejemplo, se podra agregar este metodo
a la interfaz ListaPosicion, como se muestra en el listado 6.4. En este
caso, tambien se quisiera indicar que ListaPosicion extiende Iterable. Por
lo tanto, se asume que las listas arreglo y listas nodo soportan el metodo
iterator().
6.3 Iteradores 211

1 public interface ListaPosicion <E > extends Iterable <E > {


2 /* * Regresa un iterador de todos los elementos en la lista . */
3 public Iterator <E > iterator ();

Listado 6.7: Agregando el metodo iterator a la interfaz ListaPosicion

Dada tal definicion de una ListaPosicion, se podra usar un iterador


regresado por el metodo iterator() para crear una representacion de cadena
de una lista nodo, como se muestra en el listado 6.8.

1 /* * Regresar una representaci o n textual de una lista nodo dada */


2 public static <E > String toString ( ListaPosicion <E > l ) {
3 Iterator <E > it = l . iterator ();
4 String s = " [ " ;
5 while ( it . hasNext ()) {
6 s += it . next (); // moldeo impl cito del elemento a String
7 if ( it . hasNext ())
8 s += " , " ;
9 }
10 s += " ] " ;
11 return s ;
12 }

Listado 6.8: Ejemplo de un iterador Java usado para convertir una lista nodo a
una cadena.

6.3.2. El ciclo for-each


Para recorrer los elementos regresados por un iterador es una construccion
com un, Java proporciona una notacion breve para tales ciclos, llamada el
ciclo ciclo for-each. La sintaxis para tal ciclo es la siguiente:

for (tipo nombre : expresion)


sentencia del ciclo

donde expresion eval


ua a una coleccion que implementa la interfaz java.lang.-
Iterable, tipo es el tipo de objeto regresado por el iterador para esta clase, y
nombre es el nombre de una variable que tomara los valores de los elementos
de este iterador en la sentencia del ciclo. Esta notacion es la version corta de
la siguiente:
212 Listas e Iteradores

for (Iterator<tipo> it=expresion.iterator(); it.hasNext(); ) {


Tipo nombre= it.next();
sentencia del ciclo
}

Por ejemplo, si se tiene una lista, valores, de objetos Integer, y valores


implementa java.lang.Iterable, entonces se pueden sumar todos los enteros
en valores como sigue:

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

Se podra leer el ciclo anterior como, para cada Integer i en valores,


hacer el cuerpo del ciclo, en este caso, sumar i a suma
Ademas de la forma anterior del ciclo for-each, Java tambien permite que
un ciclo for-each sea definido cuando expresion es un arreglo de tipo Tipo, el
cual puede ser un tipo base o un tipo objeto. Por ejemplo se puede totalizar
los enteros en un arreglo, v, el cual guarda los primeros diez enteros positivos,
como sigue:

int[] v={1, 2, 3, 4, 5, 6, 7, 8, 9, 10};


int total = 0;
for (int i: v)
total += i;

6.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
6.3 Iteradores 213

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 6.9, y se muestra en el listado 6.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 }

Listado 6.9: Una clase elemento iterador para una ListaPosicion.

1 /* * Regresa un iterador con todos los elementos en la lista . */


2 public Iterator <E > iterator () { return new ElementoIterador <E >( this ); }

Listado 6.10: El metodo iterator() de la clase ListaNodoPosicion.

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:
214 Listas e Iteradores

positions():regresa un objeto Iterable (como una lista arre-


glo o lista nodo) conteniendo las posiciones en la
coleccion como elementos.

Un iterador regresado por este metodo permite recorrer las posiciones de


una lista. Para garantizar que una lista nodo soporta este metodo, se podra
agregar a la interfaz ListaPosicion, por lo que se debe agregar una imple-
mentacion del metodo positions() en la clase ListaNodoPosicion, como se
muestra en el listado 6.12. Este metodo usa la propia clase ListaNodoPosicion
para crear una lista que contiene las posiciones de la lista original como sus ele-
mentos. Regresando esta lista posicion como su objeto Iterable permitiendo
llamar iterator() sobre este objeto para obtener un iterador de posiciones
de la lista original.
1 public interface ListaPosicion <E > extends Iterable <E > {
2 // ... todos los otros m e todos de la ADT lista ...
3 /* * Regresa una colecci
o n iterable de todos los nodos en la lista . */
4 public Iterable < Posicion <E > > positions ();
5 }

Listado 6.11: Agrega el metodo de Iterable a la interfaz ListaPosicion.

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 }

Listado 6.12: El metodo positions() de la clase ListaNodoPosicion.

El iterador regresado por iterator() y otros objetos Iterable definen un


tipo restringido de iterador que permite solo una pasada a los elementos. Sin
embargo, iteradores mas poderosos pueden tambien ser definidos, los cuales
permiten moverse hacia atras y hacia adelante sobre un cierto ordenamiento
de los elementos.
6.3 Iteradores 215

6.3.4. Iteradores lista en Java


La clase java.util.LinkedList no expone un concepto posicion a los
usuarios en su API. La forma para acceder y actualizar un objeto LinkedList
en Java, sin usar ndices, es usando un ListIterator que es generado por
LinkedList, usando el metodo listIterator. Tal iterador da metodos de
recorrido hacia atras y hacia adelante al igual que metodos locales para
actualizar. Este ve su posicion actual como estando antes del primer elemento,
entre dos elementos, o despues del u ltimo elemento. Esto es, usa un cursor
lista, parecido a como un cursor de pantalla es visto localizado entre dos
caracteres en una pantalla. La interfaz java.util.ListIterator incluye los
siguientes metodos:

add(e):agrega el elemento e al final de la lista.


add(i, e):inserta el elemento e en la posicion i especificada
en la lista.
hasNext():verdadero si y solo si hay un elemento despues de
la posicion actual del iterador.
hasPrevious():verdadero si y solo si hay un elemento antes de la
posicion actual del iterador.
previous():regresa el elemento e antes de la posicion actual y
mueve el cursor a e, por lo que puede usarse para
iterar hacia atras.
next():regresa el elemento e despues de la posicion actual
y mueve el cursor a e, por lo que puede usarse
para iterar hacia adelante.
nextIndex():regresa el ndice del siguiente elemento.
previousIndex():regresa el ndice del elemento previo.
set(e):reemplaza el u ltimo elemento regresado por next
o previous con el elemento indicado.
remove():quita de la lista el u ltimo elemento que fue regre-
sado por next o previous.

Es riesgoso usar m ultiples iteradores sobre la misma lista mientras se


modifica su contenido. Si inserciones, borrados, o reemplazos son ocupados
en multiples lugares en una lista, es mas seguro usar posiciones para
indicar estas localidades. Pero la clase java.util.LinkedList no expone
sus objetos posicion al usuario. Por lo tanto, para evitar los riesgos de
modificar una lista que ha creado m ultiples iteradores, por llamadas a su
216 Listas e Iteradores

metodo iterator, los objetos java.util.Iterator tienen una capacidad


fail-fast que inmediatamente invalida tal iterador si su coleccion subyacente
es modificada inesperadamente. Java permite muchos iteradores lista para
estar recorriendo una lista ligada al mismo tiempo, pero si uno de ellos la
modifica, usando add, remove o set, entonces todos los otros iteradores se
hacen invalidos.

6.4. ADT listas y el marco de colecciones


Se revisa en esta seccion los ADT lista general, los cuales combinan
metodos de la deque, lista arreglo, y lista nodo y como son usados en el marco
de colecciones de Java.

6.4.1. Listas en el marco de colecciones Java

Java proporciona un paquete de interfaces y clases para estructuras de


datos, las cuales conjuntamente definen el marco de colecciones Java (Java
Collections Framework ). Este paquete, java.util, incluye versiones de varias
de las estructuras de datos comentadas en este texto, algunas de las cuales
ya han sido discutidas y otras seran revisadas posteriormente. La interfaz
raz en el marco de colecciones Java es la interfaz Collection. esta es una
interfaz general para cualquier estructura de datos que contenga una coleccion
de elementos, como una lista. Collection extiende a java.lang.Iterable;
por lo tanto, incluye el metodo iterator(), el cual regresa un iterador de
los elementos en esta coleccion. Tambien es una superinterface para otras
interfaces en el marco de colecciones que guardan elementos, incluyendo las
interfaces Deque, List y Queue de java.util. Ademas Collection tiene
una subinterface que define el tipo de coleccion Set y tiene varias interfaces
importantes relacionadas, incluyendo las interfaces Iterator y ListIterator,
al igual que la interfaz Map.
El marco de colecciones de Java tambien incluye varias clases concretas
basadas en lista que implementan varias combinaciones de interfaces basadas
en lista. Las siguientes clases son incluidas en java.util:
6.4 ADT listas y el marco de colecciones 217

ArrayBlockingQueue:Una implementacion con arreglo de la interfaz


Queue con capacidad limitada.
ArrayDeque:Una implementacion de la interfaz Deque que usa
un arreglo, el cual puede crecer o decrecer, como
se requiera.
ArrayList:Una implementacion de la interfaz List que usa
un arreglo, el cual puede crecer o decrecer, como
se requiera.
ConcurrentLinkedQueue:Una implementacion con lista enlazada de la inter-
faz Queue que es proceso seguro.
LinkedBlockingDeque:Una implementacion con lista enlazada de la inter-
faz Deque con capacidad limitada.
LinkedBlockingQueue:Una implementacion con lista enlazada de la inter-
faz Queue con capacidad limitada.
LinkedList:Una implementacion con lista enlazada de la inter-
faz List.
Stack:Una implementacion del ADT pila.

Cada una de las clases anteriores implementan la interfaz java.lang.Iterable,


por lo tanto, incluyen el metodo iterator() y pueden ser usados en el ciclo for-
each. Ademas cualquier clase implementando la interfaz java.util.List, con
las clases ArrayList y LinkedList, tambien incluyen el metodo listIterator().
Como se indico previamente, estas interfaces son u tiles para recorrer los ele-
mentos de una coleccion o lista.

Algoritmos basados en lista en el marco de colecciones Java

Ademas de las clases basadas en lista que son dadas en el marco de


colecciones Java, tambien hay una cantidad de algoritmos simples que son
dados. Estos metodos estan implementados como metodos de clase en la clase
java.util.Collections y se incluyen los siguientes metodos.
218 Listas e Iteradores

disjoint(C, D):regresa un valor booleano indicando si las coleccio-


nes C y D son disjuntas.
fill(L, e):Reemplaza cada uno de los elementos en la lista
L con el mismo elemento, e.
frecuency(C, e):Regresa la cantidad de elementos en la coleccion
C que son iguales a e.
max(C):Regresa el elemento maximo en la coleccion C, ba-
sados en el ordenamiento natural de sus elementos.
min(C):Regresa el elemento mnimo en la coleccion C, ba-
sados en el ordenamiento natural de sus elementos.
replaceAll(L, e, f ):Reemplaza cada uno de los elementos en L que
son iguales a e con el mismo elemento, f.
reverse(L):Invierte el ordenamiento de los elementos en la
lista L.
rotate(L, d):Rota los elementos en la lista L por la distancia d,
la cual puede ser negativa, en una forma circular.
shuffle(L):Permuta pseudoaleatoriamente el ordenamiento de
los elementos en la lista L.
sort(L):Ordena la lista L, usando el ordenamiento natural
de los elementos en L.

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:

toArray():Regresa un arreglo de elementos del tipo Object


conteniendo todos los elementos en esta coleccion.
toArray(A):Regresa un arreglo de elementos del mismo tipo
de A conteniendo todos los elementos en esta co-
leccion.

Si la coleccion es una lista, entonces el arreglo devuelto debe tener sus


elementos guardados en el mismo orden que la lista original. Por lo tanto,
si se tiene un metodo u til basado en arreglo que se quiere usar en una lista
6.4 ADT listas y el marco de colecciones 219

u otro tipo de coleccion, entonces se puede usar el metodo toArray() de la


coleccion para producir una representacion arreglo de esa coleccion.

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:

asList(A):Regresa una representacion de lista del arreglo A,


con el mismo tipo de elemento que los elementos
de A.

La lista devuelta por este metodo usa el arreglo A como su representacion


interna para la lista. Por lo que esta lista esta garantizada para ser un
lista basada en arreglo y cualquier cambio hecho a esta automaticamente
sera reflejada en A. Por estos tipos de efectos laterales, el uso del metodo
asList() debera hacerse siempre con cuidado, para evitar consecuencias no
deseadas. Usando este metodo con cuidado puede ahorrar trabajo.

La interfaz java.util.List y sus implementaciones


Java proporciona funcionalidad similar a los ADT lista arreglo y lista
nodo en la interfaz java.util.List, la cual es implementada con un arreglo
en java.util.ArrayList y con una lista ligada en java.util.LinkedList.
Hay algunas compensaciones entre estas dos implementaciones. Java usa
iteradores para lograr una funcionalidad similar a la del ADT lista nodo
derivada de posiciones. El cuadro 6.5 indica la correspondencia entre los
metodos de los ADT lista (arreglo y nodo) con las interfaces java.util.List
y java.util.ListIterator.

6.4.2. Secuencias
Una secuencia es un ADT que soporta todos los metodos del ADT deque
(seccion 5.12), el ADT lista arreglo (seccion 6.1), y el ADT lista nodo (seccion
6.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:
220 Listas e Iteradores

Metodo del Metodo de Metodo de


ADT lista List ListIterator Notas
size() size() O(1)
isEmpty() isEmpty() O(1)
get(i) get(i) A es O(1),
L es O(mn{i, n i})
first() listIterator() Primer elemento
es next
last() listIterator(size())
Ultimo elemento
es previous
prev(p) previous() O(1)
next(p) next() O(1)
set(p, e) set(e) O(1)
set(i, e) set(i, e) A es O(1),
L es O(mn{i, n i})
add(i, e) add(i, e) O(n)
remove(i) remove(i) A es O(1),
L es O(mn{i, n i})
addFirst(e) add(0, e) A es O(n), L es O(1)
addFirst(e) addFirst(e) Solo con L, O(1)
addLast(e) add(e) O(1)
addLast(e) addLast(e) Solo con L, O(1)
addAfter(p, e) add(e) A es O(n), L es O(1)
insercion en el cursor
addBefore(p, e) add(e) A es O(n), L es O(1)
insercion en el cursor
remove(p) remove() A es O(n), L es O(1)
borrado en el cursor

Cuadro 6.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.
6.4 ADT listas y el marco de colecciones 221

atIndex(i):regresar la posicion del elemento con ndice i; una


condicion de error ocurre si i < 0 o i > size() 1.
indexOf(p):regresar el ndice del elemento en la posicion p.

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 6.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 }

Listado 6.13: La interfaz Secuencia definida por herencia m


ultiple por las interfaces
Deque, ListaIndice, y ListaPosicion y agregando los dos metodos puente.

Secuencia implementada con una lista ligada

Suponiendo que se implementa una secuencia con una lista doblemente en-
lazada, la cual, como la documentacion para la clase java.util.LinkedList
lo indica, es la implementacion para esta clase. Con esta implementacion,
todas metodos de actualizacion basados en posicion corren en tiempo O(1)
cada uno. De igual forma, todos los metodos del ADT deque corren en tiempo
O(1), ya que solo se requiere actualizar o acceder la lista en sus extremos.
Pero los metodos del ADT lista arreglo, los cuales estan incluidos en la clase
java.util.LinkedList, no son muy adecuados para una implementacion de
una secuencia con una lista doblemente enlazada.
222 Listas e Iteradores

Balance de eficiencia en una secuencia con lista enlazada


Como una lista enlazada no permite acceso indizado a sus elementos, reali-
zar la operacion get(i), para regresar el elemento en un ndice i dado, requiere
que se hagan saltos desde uno de los extremos de la lista, incrementando
o decrementando, hasta que se ubique el nodo que guarda el elemento con
ndice i. Como una leve optimizacion, se puede iniciar saltando del extremo
mas cercano de la lista, logrando as el siguiente tiempo de ejecucion

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

Secuencia implementada con un arreglo


Supongase que se quiere implementar la secuencia S guardando cada
elemento e de S en una celda A[i] de un arreglo A. Se puede definir un objeto
posicion p para guardar un ndice i y una referencia al arreglo A, como
variables de instancia en este caso. Entonces se puede implementar el metodo
elemento(p) para regresar A[i]. Una desventaja importante es que las celdas
en A no tendran forma de referirse a su correspondiente posicion. As, despues
de realizar la operacion addFirst, no se tiene forma de avisar a las posiciones
existentes en S que cada uno de sus ndices se debe incrementar en uno (la
6.4 ADT listas y el marco de colecciones 223

posiciones en una secuencia estan definidas relativamente a las posiciones


vecinas, no a los ndices). Por lo tanto, si se implementa una secuencia con
un arreglo, se necesita una aproximacion diferente.
Considerar una solucion alterna en la cual, en vez de guardar los elementos
de S en el arreglo A, se guarda un nuevo tipo de objeto posicion en cada
celda de A, y se guardan los elementos en las posiciones. El nuevo objeto
posicion p guarda el ndice i y el elemento asociado con p.
Con esta estructura de datos, ilustrada en la figura 6.7, se puede buscar a
traves del arreglo para actualizar la variable ndice i para cada posicion cuyo
ndice cambie debido a una insercion o borrado.

Figura 6.7: Una implementaci


on basada en un arreglo del ADT secuencia.

Balance de eficiencia en una secuencia con arreglo

En esta implementacion usando un arreglo para una secuencia, los metodos


addFirst, addBefore, addAfter, y remove toman tiempo O(n), por que se
tienen que desplazar objetos posicion para hacer espacio para la nueva posicion
o para llenar el espacio creado por la remocion de una vieja posicion (como en
los metodos insertar y remover basados en ndice). Todos los otros metodos
basados en la posicion toman tiempo O(1).
224 Listas e Iteradores

6.5. Caso de estudio: la heurstica mover al


frente
Supongase que se desea mantener una coleccion de elementos ademas de
saber el n
umero de veces que cada elemento es accedido. Conocer el numero de
accesos permite, por ejemplo, conocer cuales son los primeros diez elementos
mas populares. Ejemplos de tales escenarios incluyen los navegadores Web
que mantienen un registro de las direcciones Web mas populares visitadas
por un usuario o programa album de fotos que mantiene una lista de las
imagenes mas populares que un usuario ve. Adicionalmente, una lista de
favoritos podra ser usada en una interfaz grafica para conocer las acciones
mas populares de un men u desplegable, para despues presentar al usuario un
men u condensando conteniendo solo las opciones mas populares.
Se considera en esta seccion como se puede implementar el ADT lista de
favoritos, el cual soporta los metodos size() e isEmpty() ademas de los
siguientes:

access(e):accede el elemento e, incrementando su cuenta


de acceso, y agregandolo a la lista de favoritos si
todava no estaba presente.
remove(e):quitar el elemento e de la lista de favoritos, dado
que este se encuetra alla.
top():regresar un coleccion iterable de los k elementos
mas accedidos.

6.5.1. Implementaci on con una lista ordenada y una


clase anidada
La primera implementacion de una lista de favoritos que se considera (lista-
do 6.14) es construyendo una clase, ListaFavoritos, guardando referencias a
los objectos accesados en una lista ligada ordenada de forma decreciente, por
la cantidad de accesos. La clase usa una caracterstica de Java que permite
definir una clase anidada y relacionada dentro de la definicion de una clase. Tal
clase anidada debera ser declarada static, para indicar que esta definicion
esta relacionada a la clase que la contiene, y no a una instancia especifica
de esa clase. Mediante clases anidadas se pueden definir clases de ayuda o
soporte que pueden ser protegidas de uso externo.
6.5 Caso de estudio: la heurstica mover al frente 225

La clase anidada, Entrada, guarda para cada elemento e en la lista, un


par (c, v), donde c es la cuenta de accesos para e y v es una referencia valor
al propio elemento e. Cada vez que un elemento es accedido, se busca este
en la liga ligada (agregandolo si todava no esta) e incrementando su cuenta
de acceso. Remover un elemento requiere encontralo y quitarlo de la lista
ligada. Devolver los k elementos mas accedidos involucra copiar los valores de
entrada en un lista de salida de acuerdo a su orden en lista ligada interna.

6.5.2. Lista con heurstica mover al frente


La implementacion previa de la lista de favoritos hace el metodo access(e)
en tiempo proporcional al ndice de e en la lista de favoritos, es decir, si e es
el k-esimo elemento mas popular en la lista, entonces accederlo toma tiempo
O(k). En varias secuencias de acceso de la vida real, es com un, que una vez
que el elemento es accedido, es muy probable que sea accedido otra vez en el
futuro cercano. Se dice que tales escenarios poseen localidad de referencia.
Una heurstica, o regla de oro, que intenta tomar ventaja de la localidad
de referencia que esta presente en una secuencia de acceso es la heurstica
mover al frente. Para aplicar esta huerstica, cada vez que se accede un
elemento se mueve al frente de la lista. La esperanza es que este elemento sea
accedido otra vez en el futuro cercano. Por ejemplo, considerar un escenario
en el cual se tienen n elementos donde cada elemento es accedido n veces, en
el siguiente orden: se accede el primer elemento n veces, luego el segundo n
veces, y as hasta que el n-esimo elemento se accede n veces.
Si se guardan los elementos ordenados por la cantidad de accesos, e
insertando cada elemento la primera vez que es accedido, entonces:

cada acceso al elemento 1 se ejecuta en tiempo O(1);

cada acceso al elemento 2 se ejecuta en tiempo O(2);

...

cada acceso al elemento n se ejecuta en tiempo O(n);

As, el tiempo total para realizar las series de accesos es proporcional a

n + 2n + 3n + . . . + nn = n(1 + 2 + 3 + . . . + n) = n n(n + 1)/2


226 Listas e Iteradores

1 /* * Lista de elementos favoritos , con sus cuentas de acceso . */


2 public class ListaFavoritos <E > {
3 protected ListaPosicion < Entrada <E > > listaF ; // lista de entradas
4 /* * Constructor ; tiempo O (1) */
5 public Lista Favorit os () {
6 listaF = new ListaNodoPosicion < Entrada <E > >();
7 }
8 /* * Regresa el ` u nmero de elementos en la lista ; tiempo O (1). */
9 public int size () { return listaF . size (); }
10 /* * Prueba si la lista est a vac a ; tiempo O (1) */
11 public boolean isEmpty () { return listaF . isEmpty (); }
12 /* * Quitar un elemento dado , dado que esta en la lista ; tiempo O ( n ) */
13 public void remove ( E obj ) {
14 Posicion < Entrada <E > > p = encontrar ( obj ); // b u squeda de obj
15 if ( p != null )
16 listaF . remove ( p ); // quitar la entrada
17 }
18 /* * Incrementar la cuenta de accesos para el elemento dado y lo inserta
19 * si todav a no est a presente ; tiempo O ( n ) */
20 public void access ( E obj ) {
21 Posicion < Entrada <E > > p = encontrar ( obj ); // b u squeda de obj
22 if ( p != null )
23 p . elemento (). i n c r e m e n t a r C u e n t a (); // incrementar cuenta de acceso
24 else {
25 listaF . addLast ( new Entrada <E >( obj )); // agregar la entrada al final
26 p = listaF . last ();
27 }
28 moverArriba ( p );
29 }
30 /* * Encontar la posici o n de un elemento dado , o regresar null ; tiempo O ( n ) */
31 protected Posicion < Entrada <E > > encontrar ( E obj ) {
32 for ( Posicion < Entrada <E > > p : listaF . positions ())
33 if ( valor ( p ). equals ( obj ))
34 return p ; // encontrado en posici o n p
35 return null ; // no encontrado
36 }
37 protected void moverArriba ( Posicion < Entrada <E > > cur ) {
38 Entrada <E > e = cur . elemento ();
39 int c = cuenta ( cur );
40 while ( cur != listaF . first () ) {
41 Posicion < Entrada <E > > prev = listaF . prev ( cur ); // posici o n previa
42 if ( c <= cuenta ( prev ) ) break ; // entrada en su posic o n correcta
43 listaF . set ( cur , prev . elemento ()); // descender la entrada previa
44 cur = prev ;
45 }
46 listaF . set ( cur , e ); // guardar la entrada en su posici o n final
47 }
48 /* * Regresa los k elementos m a s accedidos , para un k ; tiempo O ( k ) */
49 public Iterable <E > top ( int k ) {
50 if ( k <0 || k > size () )
51 throw new I l l e g a l A r g u m e n t E x c e p t i o n ( " Argumento inv
a lido " );
52 ListaPosicion <E > T = new ListaNodoPosicion <E >(); // lista top - k
53 int i =0; // contador de las entradas agregadas a la lista
54 for ( Entrada <E > e : listaF ) {
55 if ( i ++ >= k )
56 break ; // todas las k entradas han sido agregadas
57 T . addLast ( e . valor () ); // agregar una entrada a la lista
58 }
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
6.5 Caso de estudio: la heurstica mover al frente 227

lo cual es O(n3 ).
Por otra parte, si se emplea la heurstica mover al frente, insertando cada
elemento la primera vez, entonces

cada acceso al elemento 1 se ejecuta en tiempo O(1);

cada acceso al elemento 2 se ejecuta en tiempo O(1);

...

cada acceso al elemento n se ejecuta en tiempo O(1);

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 6.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.
El listado 6.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 n umeros 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.

El compromiso con la heurstica mover al frente


La lista de favoritos no sera mantenida en orden por la cantidad de accesos,
cuando se pide encontrar los k elementos mas accedidos, se necesita buscarlos.
Se puede implementar el metodo top(k) como sigue:
228 Listas e Iteradores

1 /* * Lista de elementos favoritos con la heur stica mover al frente */


2 public class ListaFavoritosMF <E > extends ListaFavoritos <E >{
3 /* * Constructor por defecto */
4 public L i s t a F a vo r i t o s M F () { }
5 /* * Mueve una entrada a la primera posici o n : tiempo O (1) */
6 protected void moverArriba ( Posicion < Entrada <E > > pos ) {
7 listaF . addFirst ( listaF . remove ( pos ));
8 }
9 /* * Regresa los k elementos m a s accedidos , para un k ; tiempo O ( k ) */
10 public Iterable <E > top ( int k ) {
11 if ( k <0 || k > size () )
12 throw new I l l e g a l A r g u m e n t E x c e p t i o n ( " Argumento inv a lido " );
13 ListaPosicion <E > T = new ListaNodoPosicion <E >(); // lista top - k
14 if ( ! isEmpty () ){
15 // copiar las entradas en una lista temporal C
16 ListaPosicion < Entrada <E > > C = new ListaNodoPosicion < Entrada <E > >();
17 for ( Entrada <E > e : listaF )
18 C . addLast ( e );
19 // encontrar los top k elementos , uno a la vez
20 for ( int i =0; i < k ; i ++) {
21 Posicion < Entrada <E > > posMax = null ; // posici o n de la entrada sup .
22 int cuentaMax = -1; // cuenta de accesos de la entrada top
23 for ( Posicion < Entrada <E > > p : C . positions ()) {
24 // examinar todas las entradas de C
25 int c = cuenta ( p );
26 if ( c > cuentaMax ) { // encontr o una entrada con una cuenta mayor
27 cuentaMax = c ;
28 posMax = p ;
29 }
30 }
31 T . addLast ( valor ( posMax ) ); // agregar entrada top a T
32 C . remove ( posMax ); // quitar la posici o n que ya fue agregada
33 }
34 }
35 return T ;
36 }
37 } // Fin de la clase L i st a F a v o r i t o s M F

Listado 6.15: La clase ListaFavoritosMF implementa la heurstica mover al frente.


Esta clase extiende a la clase ListaFavoritos y anula los metodos moverArriba
y top.
6.5 Caso de estudio: la heurstica mover al frente 229

1 import java . io .*;


2 import javax . swing .*;
3 import java . awt .*;
4 import java . net .*;
5 import java . util . Random ;
6 /* * Programa ejemplo para las clases List aFavori tos y L i s t a F a v o r i t o s M F */
7 public class P r o b a d o r F a v o r i t o s {
8 public static void main ( String [] args ) {
9 String [] arregloURL = { " http :// google . com " ," http :// mit . edu " ,
10 " http :// bing . com " ," http :// yahoo . com " ," http :// unam . mx " };
11 ListaFavoritos < String > L1 = new ListaFavoritos < String >();
12 ListaFavoritosMF < String > L2 = new ListaFavoritosMF < String >();
13 int n = 20; // cantidad de operaciones de acceso
14 // Escenario de simulaci o n : acceder n veces un URL aleatorio
15 Random rand = new Random ();
16 for ( int k =0; k < n ; k ++) {
17 System . out . println ( " _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ " );
18 int i = rand . nextInt ( arregloURL . length ); // ndice aleatorio
19 String url = arregloURL [ i ];
20 System . out . println ( " Accediendo : " + url );
21 L1 . access ( url );
22 System . out . println ( " L1 = " + L1 );
23 L2 . access ( url );
24 System . out . println ( " L2 = " + L2 );
25 }
26 int t = L1 . size ()/2;
27 System . out . println ( " - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - " );
28 System . out . println ( " Top " + t + " en L1 = " + L1 . top ( t ));
29 System . out . println ( " Top " + t + " en L2 = " + L2 . top ( t ));
30 // Mostrar una ventana navegador del URL m a s popular de L1
31 try {
32 String popular = L1 . top (1). iterator (). next (); // el m a s popular
33 JEditorPane jep = new JEditorPane ( popular );
34 jep . setEditable ( false );
35 JFrame frame = new JFrame ( " URL m a s popular en L1 : " + popular );
36 frame . getCo ntentPan e (). add ( new JScrollPane ( jep ) , BorderLayout . CENTER );
37 frame . setSize (640 ,480);
38 frame . setVisible ( true );
39 frame . s e t D e f a u l t C l o s e O p e r a t i o n ( JFrame . EXIT_ON_CLOSE );
40 } catch ( IOException e ) { /* ignorar excepciones I / O */ }
41 }
42 }

Listado 6.16: Se emplea en la clase ProbadorFavoritos las clases ListaFavoritos


y ListaFavoritosMF para contar la simulaci on de accesos a un conjunto de p
aginas
Web. Se muestra tambien la p agina mas popular.
230 Listas e Iteradores

1. Se copian las entradas de la lista de favoritos en otra lista, C, y se crea


una lista vaca, A.

2. La lista C se explora k veces. En cada ocasion, se encuentra una entrada


de C con la cuenta de acceso mas grande, quitando esta entrada de C,
e insertando su valor al final de A.

3. Se regresa la lista A.

La implementacion del metodo top toma tiempo O(kn). As, cuando k es


una constante, el metodo se ejecuta en tiempo O(n). Esto ocurre si se quiere
obtener la lista del top-ten. Sin embargo, si k es proporcional a n, entonces la
ejecucion toma tiempo O(n2 ) y sucede cuando se quiere la lista del top 25 %.
Captulo 7

Arboles

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

7.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
colocando elementos dentro de ovalos o rectangulos, y dibujando las conexio-
232
Arboles

nes entre padres e hijos con lneas rectas, ver figura 7.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.

Figura 7.1: Un arbol con 17 nodos que representan la organizaci


on de una corpora-
ci
on.

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

Si A no esta vaco, este tiene un nodo especial, llamado la raz (root)


de A, que no tiene padre.

Cada nodo v de A diferente de la raz tiene un nodo padre unico w; cada


nodo con padre w es un hijo de w.

7.1 Arboles generales 233

De acuerdo a lo anterior, un arbol puede estar vaco, es decir, este no tiene


nodos. Esta convencion tambien permite definir un arbol recursivamente, tal
que un arbol A esta vaco o consiste de un nodo r, llamado la raz de A, y
un conjunto de arboles, posiblemente vaco, donde las races del conjunto de
arboles son los hijos de r.
Dos nodos que son hijos del mismo padre son hermanos. Un nodo v es
externo si v no tiene hijos. Un nodo v es interno si este tiene uno o mas
hijos. Los nodos externos son tambien conocidos como hojas.
Ejemplo 7.1. En la mayora de los sistemas operativos, los archivos son
organizados jerarquicamente en directorios anidados, tambien conocidos como
carpetas, los cuales son presentados al usuario en la forma de un arbol, ver
figura 7.2. Mas especficamente, los nodos internos del arbol estan asociados
con directorios y los nodos externos estan asociados con archivos regulares.
En el sistema operativo UNIX. la raz del arbol es llamada el directorio raz,
y es representada por el smbolo /.

Figura 7.2: Representaci


on de
arbol de una porcion de un sistema de archivos.

Un nodo u es un ancestro de un nodo v si u = v o u es un ancestro del


padre de v. A la inversa, se dice que un nodo v es un descendiente de un nodo
234
Arboles

u si u es un ancestro de v. En la figura 7.2 cs252 es un ancestro de papers,


y pr3 es un descendiente de cs016/. El sub arbol enrazado de A es el arbol
consistente de todos los descendientes de v en A, incluyendo al propio v. En
la figura 7.2, el subarbol enraizado en cs016/ consiste de los nodos cs016/,
grades, homeworks/, programs/, hw1, hw2, hw3, pr1, pr2 y pr3.

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 7.2 contiene el camino cs016/programs/pr2.
Ejemplo 7.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

Un arbol es
arbol ordenado si hay un ordenamiento lineal definido para
los hijos de cada nodo; esto es, se puede identificar los hijos de un nodo
como el primero, el segundo, el tercero, etc. Tal ordenamiento es visualizado
arreglando los hermanos de izquierda a derecha, de acuerdo a su ordenamiento.
Los arboles ordenados indican el orden lineal entre hermanos listandolos en
el orden correcto.
Ejemplo 7.3. Los componentes de un documento estructurado, tal como un
libro, estan jer
arquicamente organizados como un arbol cuyos nodos internos
son partes, captulos, y secciones, y cuyos nodos externos son parrafos, tablas,
figuras, etcetera, ver figura 7.3. La raz del arbol corresponde al propio libro.
Se podra considerar expandir mas el arbol para mostrar parrafos consistentes
de sentencias, sentencias consistentes de palabras, y palabras consistentes
de caracteres. Tal arbol es un ejemplo de un arbol ordenado, porque hay un
ordenamiento bien definido entre los hijos de cada nodo.

7.1 Arboles generales 235

Figura 7.3: Un
arbol ordenado asociado con un libro.

7.1.3. El tipo de dato abstracto


arbol
El ADT arbol guarda elementos en posiciones, las cuales, al igual que las
posiciones en una lista, estan definidas relativamente a posiciones vecinales.
Las posiciones en un arbol en un arbol son sus nodos y las posiciones
vecinales satisfacen las relaciones padre-hijo que definen un arbol valido.
Por lo tanto, se usan los terminos posicion y nodo indistintamente para
arboles. Al igual que con una lista posicion, un objeto posicion para un arbol
soporta el metodo:

elemento():regresa el objeto guardado en esta posicion.

Sin embargo, el poder real de posiciones nodo en un arbol, viene de los


metodos accesores del ADT arbol que aceptan y regresan posiciones, tales
como las siguientes:
root():regresa la raz del arbol; un error ocurre si el arbol
esta vaco.
parent(v):regresa el padre de v; un error ocurre si v es la
raz.
children(v):regresa una coleccion iterable que lleva los hijos
del nodo v.
Si un arbol A esta ordenado, entonces la coleccion iterable, children(v),
guarda los hijos de v en orden. Si v es un nodo externo, entonces children(v)
esta vaco.
236
Arboles

Ademas de los metodos accesores, tambien se incluyen los siguientes


metodos de consulta:

isInternal(v):indica si el nodo v es interno.


isExternal(v):indica si el nodo v es externo.
isRoot(v):indica si el nodo v es la raz.

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:

size():regresa el n umero de nodos en el arbol.


isEmpty():valida si el arbol tiene alg
un nodo o no.
iterator():regresa un iterador de todos los elementos guarda-
dos en nodos del arbol.
positions():regresa una coleccion iterable de todos los nodos
del arbol.
replace(v, e):regresa el elemento del nodo v antes de cambiarlo
por e.

Cualquier metodo que tome una posicion como un argumento podra


generar una condicion de error si esa posicion es invalida. No se definen
metodos de actualizacion especializados para arboles. En su lugar, se describen
metodos de actualizacion diferentes en conjuncion con aplicaciones especficas
de arboles.

7.1.4. Implementaci
on un
arbol
La interfaz Java mostrada en el listado 7.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.

7.1 Arboles generales 237

1 import java . util . Iterator ;


2 /* *
3 * Una interfaz para un a rbol donde los nodos pueden tener
4 * una cantidad arbitraria de hijos .
5 */
6 public interface Tree <E > {
7 /* * Regresa el n u mero de nodos en el a rbol . */
8 public int size ();
9 /* * Valida si el a rbol est a vac o . */
10 public boolean isEmpty ();
11 /* * Regresa un iterador de los elementos guardados en el a rbol . */
12 public Iterator <E > iterator ();
13 /* * Regresa una colecci o n iterable de los nodos . */
14 public Iterable < Posicion <E > > positions ();
15 /* * Reemplaza el elemento guardado en un nodo dado . */
16 public E replace ( Posicion <E > v , E e )
17 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 ;
18 /* * Regresa la ra z del a rbol . */
19 public Posicion <E > root () throws E m p t y T r e e E x c e p t i o n ;
20 /* * Regresa el padre de un nodo dado . */
21 public Posicion <E > parent ( Posicion <E > v )
22 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 ;
23 /* * Regresa una colecci o n iterable de los hijos de un nodo dado . */
24 public Iterable < Posicion <E > > children ( Posicion <E > v )
25 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 ;
26 /* * Valida si un nodo dado es interno . */
27 public boolean isInternal ( Posicion <E > v )
28 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 ;
29 /* * Valida si un nodo dado es externo . */
30 public boolean isExternal ( Posicion <E > v )
31 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 ;
32 /* * Valida si un nodo dado es la ra z del a rbol . */
33 public boolean isRoot ( Posicion <E > v )
34 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 ;
35 }

Listado 7.1: La interfaz Java Tree representando el ADT arbol. Metodos adicionales
de actualizaci
on podran ser agregados dependiendo de la aplicacion.
238
Arboles

Una estructura enlazada para


arboles generales

Una forma natural para crear un arbol A es usar una estructura enlazada,
donde se representa cada nodo v de A por un objeto posicion, ver figura 7.4
(a), con los siguientes campos: una referencia al elemento guardado en v,
un enlace al padre de v, y algun tipo de coleccion, por ejemplo, una lista o
arreglo, para guardar enlaces a los hijos de v. Si v es la raz de A entonces el
campo parent de v es null. Tambien, se guarda una referencia a la raz de
A y el numero de nodos de A en variables internas. Se muestra en la figura
7.4 (b) la estructura esquematicamente.

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

La tabla 7.1 resume el rendimiento de la implementacion de un arbol


general usando una estructura enlazada. Se observa que usando una coleccion
para guardar los hijos de cada nodo v, se puede implementar children(v)
regresando una referencia a esta coleccion.

7.2. Algoritmos de recorrido para


arbol
En esta seccion, se presentan algoritmos para hacer recorridos en un arbol
accediendo este a traves de los metodos del ADT arbol.
7.2 Algoritmos de recorrido para
arbol 239

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)

Cuadro 7.1: Tiempos de ejecuci on de los metodos de un


arbol general de n-nodos
con una estructura enlazada. cv denota el n
umero de hijos de un nodo v. El espacio
usado es O(n).

7.2.1. Profundidad y altura


Sea v un nodo de un arbol A. La profundidad de v es el n umero de ances-
tros de v, excluyendo al propio v. Esta definicion implica que la profundidad
de la raz de A es cero.
La profundidad de un nodo v puede tambien ser definida recursivamente
como sigue:
Si v es la raz, entonces la profundidad de v es cero.

De otra forma, la profundidad de v es uno mas la profundidad del padre


de v.
Basada en esta definicion se presenta un algoritmo recursivo simple, depth
para calcular la profundidad de un nodo v de A. Este metodo se llama a
s mismo recursivamente en el padre de v, y agrega uno al valor regresado.
Algoritmo depth(A, v):
si v es la raz de A entonces
regresar 0
si no
regresar 1+depth(A, w), donde w es el padre de v en A
Una implementacion Java simple de este algoritmo se muestra en el listado
7.2.
El tiempo de ejecucion del algoritmo depth(A, v) es O(dv ), donde dv
denota la profundidad del nodo v en el arbol A, porque el algoritmo realiza
un paso recursivo tiempo constante para cada ancestro de v. As, el algoritmo
240
Arboles

1 public static <E > int depth ( Tree <E > T , Posicion <E > v ) {
2 if ( T . isRoot ( v ) )
3 return 0;
4 else
5 return 1 + depth (T , T . parent ( v ) );
6 }

Listado 7.2: El metodo depth escrito en Java.

depth(A, v) se ejecuta en tiempo O(n) en el peor caso, donde n es el n umero


total de nodos de A, ya que un nodo de A podra tener profundidad n 1 en
el peor caso. Aunque tal tiempo de ejecucion es una funcion del tama no de
entrada, es mas preciso caracterizar el tiempo de ejecucion en terminos del
parametro dv , debido a que este parametro puede ser mucho menor que n.

Altura
La altura de un nodo v en un arbol A esta tambien definida recursiva-
mente:

Si v es un nodo externo, entonces la altura de v es cero.

De otra forma, la altura de v es uno mas la altura maxima de los hijos


de v.

La altura de un arbol no vaco A es la altura de la raz de A. Ademas la


altura puede tambien ser vista como sigue.
Proposici on 7.1: La altura de un arbol no vaco A es igual a la maxima
profundidad de un nodo externo de A.
El algoritmo height1 halla la altura de un arbol no vaco A usando la
proposicion anterior y el algoritmo depth. El listado 7.3 muestra su imple-
mentacion en Java.
Algoritmo height1(A):
h0
para cada vertice v en A hacer
si v es un nodo externo de A entonces
h max(h, depth(A, v))
regresar h
Desafortunadamente, el algoritmo height1 no es muy eficiente. Como
height1 llama al algoritmo depth(v) en cada nodo externo v de A, el tiempo
7.2 Algoritmos de recorrido para
arbol 241

1 public static <E > int height1 ( Tree <E > T ) {


2 int h = 0;
3 for ( Posicion <E > v : T . positions () )
4 if ( T . isExternal ( v ) )
5 h = Math . max ( h , depth (T , v ) );
6 return h ;
7 }

Listado 7.3: El metodo height1 escrito en Java. Se emplea el metodo max de la


clase java.lang.Math

P
de ejecucion de height1 esta dado por O(n + v (1 + dv )), donde n es el
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 7.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
si no
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 }

Listado 7.4: El metodo height2 escrito en Java.

El algoritmo height2 es mas eficiente que height1. El algoritmo es


recursivo, y, si este es inicialmente llamado con la raz de A, este eventualmente
sera llamado con cada nodo de A. As, se puede determinar el tiempo de
ejecucion de este metodo sumando, sobre todos los nodos, la cantidad de
242
Arboles

tiempo gastado en cada nodo. Procesar cada nodo en children(v) toma


tiempo O(cv ), donde cv es el n umero de hijos de un nodo v. Tambien, el
ciclo while tiene cv iteraciones y cada iteracion en el ciclo toma tiempo O(1)
mas el tiempo para la llamada recursiva con un hijo de v. As, el algoritmo
height2
P gasta tiempo O(1 + cv ) en cada nodo v, y su tiempo de ejecucion es
O( v (1 + cv )).
Sea un arbol A con n nodos, y cv denota el n umeroP de hijos de un nodo
v de A, entonces sumando sobre los vertices en A, v cv = n 1, ya que
cada nodo de A, con la excepcion de la raz, es un hijo de otro nodo, y
as contribuye una unidad a la suma anterior.
Con el resultado anterior, el tiempo de ejecucion del algoritmo height2,
cuando es llamado con la raz de A, es O(n), donde n es el n umero de nodos
de A.

7.2.2. Recorrido en preorden


Un recorrido de un arbol A es una forma sistematica de acceder, visitar,
todos los nodos de A. Se presenta un esquema basico de recorrido para arboles,
llamado recorrido en preorden. En la siguiente seccion, se revisa otro esquema
basico de recorrido, llamado recorrido en postorden.
En un recorrido en preorden de un arbol A, la raz de A es visitada
primero y entonces los subarboles enraizados con sus hijos son recorridos re-
cursivamente. Si el arbol esta ordenado, entonces los subarboles son recorridos
de acuerdo al orden de los hijos. La accion especfica asociada con la visita
de un nodo v depende de la aplicacion de este recorrido, y podra involucrar
desde incrementar un contador hasta realizar alg un calculo complejo para v.
El pseudocodigo para el recorrido en preorden del subarbol enraizado en un
nodo v se muestra enseguida.
Algoritmo preorden(A, v):
Realizar la accion visita para el nodo v
para cada hijo w de v en A hacer
preorden(A, w) {recursivamente recorrer el subarbol en w}
El algoritmo de recorrido en preorden es util para producir un ordenamiento
lineal de los nodos de un arbol donde los padres deberan estar antes que
sus hijos en el ordenamiento. Tales ordenamientos tienen varias aplicaciones
diferentes. Se comenta a continuacion un ejemplo simple de tal aplicacion en
7.2 Algoritmos de recorrido para
arbol 243

el siguiente ejemplo.
Ejemplo 7.4. El recorrido en preorden del arbol asociado con un documento
examina un documento entero secuencialmente, desde el inicio hasta el fin.
Si los nodos externos son quitados antes del recorrido, entonces el recorrido
examina la tabla de contenido del documento. Ver figura 7.5.

Figura 7.5: Recorrido en preorden de un arbol ordenado, donde los hijos de cada
nodo est
an ordenados de izquierda a derecha.

El recorrido en preorden es tambien una forma eficiente para acceder a


todos los nodos de un arbol. El analisis es similar al del algoritmo height2.
En cada nodo v, la parte no recursiva del algoritmo de recorrido en preorden
requiere tiempo O(1 + cv ), donde cv es el numero de hijos de v, As, el tiempo
total de ejecucion en el recorrido en preorden de A es O(n). Tambien se puede
observar que cada nodo es visitado solo una vez.
El algoritmo toStringPreorden implementado con Java, listado 7.5,
hace una impresion en preorden del subarbol de un nodo v de A, es decir,
realiza el recorrido en preorden del subarbol enraizado en v e imprime los
elementos guardados en un nodo cuando el nodo es visitado. El metodo
A.children(v) regresa una coleccion iterable que accede a los hijos de v en
orden.
Hay una aplicacion interesante del algoritmo de recorrido en preorden
que produce una representacion de cadena de un arbol entero. Suponiendo
que para cada elemento e guardado en el arbol A, la llamada e.toString()
regresa una cadena asociada con e. La representaci on parent etica de
cadena P (A) del arbol A definida recursivamente como sigue. Si A tiene un
solo nodo v, entonces
244
Arboles

1 public static <E > String t oS t r i n g P r e o r d e n ( Tree <E > T , Posicion <E > v ) {
2 String s = v . elemento (). toString (); // acci o n principal de visita
3 for ( Posicion <E > h : T . children ( v ))
4 s += " , " + t o St r i n g P r e o r d en (T , h );
5 return s ;
6 }

Listado 7.5: Metodo toStringPreordenque hace una impresion en preorden de los


elementos en el subarbol del nodo v de A.

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.
La definicion de P (A) es recursiva y se usa el operador + para indicar
la concatenacion de cadenas. La representacion parentetica del arbol de la
figura 7.1 se muestra enseguida, donde el sangrado y los espacios han sido
agregados para claridad.

Electronics RUs (
R&D
Sales (
Domestic
International (
Canada
S. America
Overseas ( Africa Europe Asia Australia )
)
)
Purchasing
Manufacturing ( TV CD Tuner )
)

El metodo Java representacionParentetica, listado 7.6, es una varia-


cion del metodo toStringPreorden, listado 7.5. Esta implementado por la
7.2 Algoritmos de recorrido para
arbol 245

definicion anterior para obtener una representaion parentetica de cadena de


un arbol A. El metodo representacionParentetica hace uso del metodo
toString que esta definido para cada objeto Java. Se puede ver a este metodo
como un tipo de metodo toString() para objetos arbol.

1 public static <E > String r e p r e s e n t a c i o n P a r e n t e t i c a (


2 Tree <E > T , Posicion <E > v ) {
3 String s = v . elemento (). toString (); // acci o n principal de visita
4 if ( T . isInternal ( v )) {
5 Boolean primeraVez = true ;
6 for ( Posicion <E > w : T . children ( v ))
7 if ( primeraVez ) {
8 s += " ( " + r e p r e s e n t a c i o n P a r e n t e t i c a (T , w ); // 1 er hijo
9 primeraVez = false ;
10 }
11 else
12 s += " , " + r e p r e s e n t a c i o n P a r e n t e t i c a (T , w ); // hijos siguientes
13 s += " ) " ; // cerrar par e ntesis
14 }
15 return s ;
16 }

Listado 7.6: Metodo representacionParentetica donde se emplea el operador +


para concatenar dos cadenas.

7.2.3. Recorrido en postorden


El algoritmo de recorrido en postorden puede ser visto como el opuesto
al recorrido en preorden, porque este recursivamente recorre los subarboles
enraizados en los hijos de la raz primero, y despues visita la raz. Al igual que
en el recorrido en preorden se emplea el algoritmo para resolver un problema
particular especializando una accion asociada con la visita de un nodo v. Si
el arbol esta ordenado, se hacen llamadas recursivas para los hijos de un nodo
v de acuerdo a su orden indicado. El pseudocodigo es dado a continuacion.
Algoritmo postorden(A, v):
para cada hijo w de v en A hacer
postorden(A, w) {recursivamente recorrer el subarbol en w}
Realizar la accion visita para el nodo v
El nombre de recorrido en postorden se debe a que este metodo de recorrido
visitara un nodo v despues de que este ha visitado todos los otros nodos en
los subarboles enraizados en v. Ver figura 7.6.
246
Arboles

Figura 7.6: Recorrido en postorden del arbol ordenado de la figura 7.5

El analisis del tiempo de ejecucion del recorrido en postorden es analogo


al del recorrido en preorden. El tiempo total gastado en las porciones no
recursivas del algoritmo es proporcional al tiempo gastado en visitar los hijos
de cada nodo en el arbol. Entonces el recorrido de un arbol A con n nodos
toma tiempo O(n), suponiendo que cada visita tome tiempo O(1), as el
recorrido se ejecuta en tiempo lineal.
En el listado 7.7 se muestra el metodo Java toStringPostOrden el cual
realiza un recorrido en postorden de un arbol A. Este metodo muestra el
elemento guardado en un nodo cuando este es visitado. Este metodo tambien
llama implcitamente llama al metodo toString con los elementos, cuando
estan involucrados en una operacion de concatenacion de cadenas.

1 public static <E > String t o S t r i n g P o s t o r d e n ( Tree <E > T , Posicion <E > v ) {
2 String s = " " ;
3 for ( Posicion <E > h : T . children ( v ))
4 s += " , " + t o St r i n g P r e o r d en (T , h )+ " " ;
5 s += v . elemento ();
6 return s ;
7 }

Listado 7.7: Metodo toStringPostorden que realiza una impresion en postorden


de los elementos en el subarbol del nodo v de A.

El metodo de recorrido en postorden es util para resolver problemas donde


se desea calcular alguna propiedad para cada nodo v en un arbol, pero para
poder calcular esa propiedad sobre v se requiere que ya se haya calculado
esa propiedad para los hijos de v. Se muestra una aplicacion en el siguiente
7.2 Algoritmos de recorrido para
arbol 247

ejemplo.
Ejemplo 7.5. Considerar un arbol sistema de archivos A, donde los nodos
externos representan archivos y los nodos internos representan directorios.
Supongase que se quiere calcular el espacio en disco usado en un directorio,
ver figura 7.7, lo cual es recursivamente dado por la suma de:

El tama
no del directorio propio.

Los tama
nos de los archivos en el directorio.

El espacio usado por los directorios hijos.

El calculo puede ser hecho en un recorrido en postorden del arbol A.


Despues de que los subarboles de un nodo interno v han sido recorridos, se
calcula el espacio usado por v agregando el tama no del propio directorio v y
los tama nos de los archivos contenidos en v al espacio usado por cada hijo
interno de v, el cual fue calculado por los recorridos recursivos en postorden
de los hijos de v.

Calcular el espacio en disco


De acuerdo con el ejemplo anterior, el metodo espacioDisco, mostrado en
el listado 7.8, realiza un recorrido en postorden de un arbol sistema de archivos
A, imprimiendo el nombre y el espacio usado por el directorio asociado con
cada nodo interno de A. Cuando se llama desde la raz de A, espacioDisco
se ejecuta en tiempo O(n), donde n es el n umero de nodos de A dados que
los metodos auxiliares nombre y tam toman tiempo O(1).

Otros tipos de recorridos


A pesar de que los recorridos en preorden y postorden son formas comu-
nes de visitar los nodos de un arbol, se pueden tener otros recorridos. Por
ejemplo, se podra recorrer un arbol del que se visiten todos los nodos de una
profundidad p antes que se visiten los nodos en una profundidad d + 1. Este
recorrido es conocido por niveles. Cuando se recorren los nodos del arbol
consecutivamente numerandoles conforme se visitan se llama la numeraci on
por nivel de los nodos de A.
248
Arboles

Figura 7.7: El arbol de la figura 7.2 representando el sistema de archivos, mostrando


el nombre y el tama no asociado para cada archivo o directorio dentro de cada nodo,
y el espacio en disco usado por el directorio encima de cada nodo interno.

1 public static <E > int espacioDisco ( Tree <E > T , Posicion <E > v ) {
2 int t = tam ( v ); // iniciar con el tama ~ n o del propio nodo
3 for ( Posicion <E > h : T . children ( v ))
4 // agregar espacio calculado recurs ivament e usado por los hijos
5 t += espacioDisco (T , h );
6 if ( T . isInternal ( v ))
7 // imprimir nombre y espacio en disco usado
8 System . out . print ( nombre ( v ) + " : " + t );
9 return t ;
10 }

Listado 7.8: El metodo espacioDisco imprime el nombre y el espacio en disco


usado por los directorios asociados con cada nodo interno de un arbol sistema de
archivos. Los metodos auxiliares nombre y tam deben ser definidos para regresar
el nombre y el tamano asociado con un nodo.

7.3 Arboles Binarios 249

7.3.
Arboles Binarios
Un
arbol binario es un arbol ordenado con las siguientes propiedades:
1. Cada nodo tiene a lo mas dos hijos.
2. Cada nodo hijo esta etiquetado ya se como un hijo izquierdo o como un
hijo derecho.
3. Un hijo izquierdo precede a un hijo derecho en el ordenamiento de los
hijos de un nodo.
El subarbol enraizado en un hijo izquierdo o un hijo derecho de un
nodo interno v es llamado un sub arbol izquierdo o sub arbol derecho,
respectivamente, de v. Un arbol binario es arbol binario propio si cada
nodo tiene cero o dos hijos. Algunos autores tambien se refieren a tales arboles
como arboles binarios arbol binario completo. As, en un arbol binario
propio, cada nodo interno tiene exactamente dos hijos. Un arbol binario que
no es propio es arbol impropio.
Ejemplo 7.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 decisi on, 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 7.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.
Ejemplo 7.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 7.9. Cada nodo en tal arbol tiene un valor asociado con este.
Si un nodo es externo, entonces su valor es aquel de su variable o
constante.
250
Arboles

Figura 7.8: Un
arbol de decision para ordenar tres variables en orden creciente

Si un nodo es interno, entonces su valor esta definido aplicando la


operacion a los valores de sus hijos.

Un arbol de expresion aritmetica es un arbol binario propio, ya que los


operadores +, , y / requieren exactamente dos operandos.

Figura 7.9: Un arbol binario representando la expresion aritmetica ((((3 + 1)


3)/((9 5) + 2)) ((3 (7 4)) + 6)).

7.3 Arboles Binarios 251

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.

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

left(v):Regresa el hijo izquierdo de v; una condicion de


error ocurre si v no tiene hijo izquierdo.
right(v):Regresa el hijo derecho de v; una condicion de
error ocurre si v no tiene hijo derecho.
hasLeft(v):Prueba si v tiene hijo izquierdo.
hasRight(v):Prueba si v tiene hijo derecho.

No se definen metodos de actualizacion especializados sino que se con-


sideran algunas posibles actualizaciones de metodos cuando se describan
implementaciones especficas y aplicaciones de arboles binarios.

7.3.2. Una interfaz


arbol binario en Java
Se modela un arbol binario como un tipo de dato abstracto que extiende el
ADT arbol y agrega los cuatro metodos especializados para un arbol binario.
En el listado 7.9, se muestra la interfaz que se define con esta aproximacion.
Como los arboles binarios son arboles ordenados, la coleccion iterable regresada
por el metodo children(v), que se hereda de la interfaz Tree guarda el hijo
izquierdo de v antes que el hijo derecho.

7.3.3. Propiedades del


arbol binario
Las propiedades de los arboles binarios se relacionan con sus alturas y el
n
umero de nodos Se denota al conjunto de todos los nodos de un arbol A
252
Arboles

1 /* *
2 * Una interfaz para un a rbol binario donde cada nodo tiene
3 * cero , uno o dos hijos .
4 *
5 */
6 public interface ArbolBinario <E > extends Tree <E > {
7 /* * Regresa el hijo izquierdo de un nodo . */
8 public Posicion <E > left ( Posicion <E > v )
9 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 ;
10 /* * Regresa el hijo derecho de un nodo . */
11 public Posicion <E > right ( Posicion <E > v )
12 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 ;
13 /* * Indica si un un nodo tiene hijo izquierdo . */
14 public boolean hasLeft ( Posicion <E > v ) 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 ;
15 /* * Indica si un un nodo tiene hijo derecho . */
16 public boolean hasRight ( Posicion <E > v ) 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 ;
17 }

Listado 7.9: Interfaz Java ArbolBinario para el ADT arbol binario que extiende a
Tree (listado 7.1).

que estan a la misma profundidad d como el nivel de un arbol binario de


A. Para el arbol binario, el nivel 0 tiene a lo mas un nodo, la raz, el nivel 1
tiene a los mas dos nodos, los hijos de la raz, el nivel 2 tiene a lo mas 4 hijos,
y as sucesivamente. Ver figura 7.10. En general, el nivel d tiene a lo mas 2d
nodos.
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 7.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

Tambien, si A es propio, entonces A tiene las siguientes propiedades

1. 2h + 1 n 2h+1 1

7.3 Arboles Binarios 253

Figura 7.10: N
umero m
aximo de nodos en los niveles de un arbol binario.

2. h + 1 nE 2h

3. h nI 2h 1

4. log(n + 1) 1 h (n 1)/2

Tambien se cumple la siguiente relacion entre el n


umero de nodos internos
y los nodos externos en un arbol binario propio

nE = nI + 1.
Para mostrar lo anterior se pueden quitar los nodos de A y dividirlos en
dos pilas, una pila con los nodos internos y otra con los nodos externos,
hasta que A quede vaco. Las pilas estan inicialmente vacas. Al final, la
pila de nodos externos tendra un nodo mas que la pila de nodos internos,
considerar dos casos:

Caso 1: Si A tiene un solo nodo v, se quita v y se coloca en


la pila externa. As, la pila de nodos externos tiene un solo nodo
y la pila de nodos internos esta vaca.
254
Arboles

Caso 2: De otra forma, A tiene mas de un nodo, se quita de


A un nodo externo arbitrario w y su padre v, el cual es un nodo
interno. Se coloca a w en la pila de nodos externos y v en la pila
de nodos internos. Si v tiene un padre u, entonces se reconecta u
con el ex-hermano z de w, como se muestra en la figura 7.11. Esta
operacion quita un nodo interno y uno externo, y deja al arbol
como un arbol binario propio.

Figura 7.11: Operacion para quitar un nodo externo y su nodo padre, usado en la
demostracion

Se repite esta operacion, hasta que eventualmente se tiene un


arbol final consistente de un solo nodo. Entonces se quita el nodo
del arbol final se coloca en la pila externa, teniendo as esta pila
un nodo mas que la pila interna.

7.3.4. Una estructura enlazada para


arboles binarios
Una forma natural de realizar un arbol binario A es usar una estructura
ligada, donde se representa cada nodo v de A por un objeto Posicion, ver
figura 7.12, con los campos dando referencias al elemento guardado en v y a
objetos Posicion asociados con los hijos y el padre de v.
Si v es la raz de A, entonces el campo padre de v es null. Tambien, se
guarda el n umero de nodos de A en la variable tam.

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,

7.3 Arboles Binarios 255

Figura 7.12: Un nodo (a) y una estructura enlazada (b) para representar un arbol
binario.
256
Arboles

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 7.10: Interfaz Java PosicionAB para definir los metodos que permiten
acceder a los elementos de un nodo del arbol binario.

setElemento y para poner y devolver el padre, el hijo izquierdo y el hijo


derecho, como se muestra en el listado 7.10.

La clase NodoAB, listado 7.11, implementa la interfaz PosicionAB para un


objeto con los campos elemento, izq, der, y padre, lo cual, para un nodo v,
se refiere al elemento en v, el hijo izquierdo de v, el hijo derecho de v, y el
padre de v respectivamente.

Implementaci
on Java del
arbol binario enlazado

En el listado 7.12 se muestra la clase ArbolBinarioEnlazado que imple-


menta la interfaz ArbolBinario, listado 7.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
el metodo accesor sibling(v), el cual regresa el hermano de un nodo v, y
los siguientes metodos de actualizacion:

7.3 Arboles Binarios 257

1 /* *
2 * La clase implementa un nodo de un a rbol binario guardando las referencias
3 * a un elemento , un nodo padre , un nodo izquierdo , y un nodo derecho .
4 *
5 */
6 public class NodoAB <E > implements PosicionAB <E > {
7 private E elemento ; // elemento guardado en este nodo
8 private PosicionAB <E > izq , der , padre ; // nodos adyacentes
9 /* * Constructor por defecto */
10 public NodoAB () { }
11 /* * Constructor principal */
12 public NodoAB ( E elemento , PosicionAB <E > padre ,
13 PosicionAB <E > izq , PosicionAB <E > der ) {
14 setElemento ( elemento );
15 setPadre ( padre );
16 setIzq ( izq );
17 setDer ( der );
18 }
19 /* * Regresa el elemento guardado en esta posici o n */
20 public E elemento () { return elemento ; }
21 /* * Pone el elemento guardado en esta posici o n */
22 public void setElemento ( E o ) { elemento = o ; }
23 /* * Regresa el hijo izquierdo de esta posici o n */
24 public PosicionAB <E > getIzq () { return izq ; }
25 /* * Pone el hijo izquierdo de esta posici o n */
26 public void setIzq ( PosicionAB <E > v ) { izq = v ; }
27 /* * Regresa el hijo derecho de esta posici o n */
28 public PosicionAB <E > getDer () { return der ; }
29 /* * Pone el hijo derecho de esta posici o n */
30 public void setDer ( PosicionAB <E > v ) { der = v ; }
31 /* * Regresa el padre de esta posici o n */
32 public PosicionAB <E > getPadre () { return padre ; }
33 /* * Pone el padre de esta posici o n */
34 public void setPadre ( PosicionAB <E > v ) { padre = v ; }
35 }

Listado 7.11: Clase auxiliar NodoAB para implementar los nodos del arbol binario.
258
Arboles

addRoot(e):Crea y regresa un nuevo nodo r que guarda el


elemento e y hace a r la raz del arbol, un error
ocurre si el arbol no esta vaco.
insertLeft(v, e):Crea y regresa un nuevo nodo w que guarda a e,
agrega a w como el hijo izquierdo de v y regresa w,
un error ocurre si v ya tiene un hijo izquierdo.
insertRight(v, e):Crea y regresa un nuevo nodo w que guarda a e,
agrega a w como el hijo derecho de v y regresa w,
un error ocurre si v ya tiene un hijo derecho.
remove(v):Quita el nodo v y lo reemplaza con su hijo, si
tiene, y regresa el elemento guardado en v; un
error ocurre si v tiene dos hijos.
attach(v, T1 , T2 ):Une T1 y T2 como los subarboles izquierdo y de-
recho del nodo externo v, una condicion de error
ocurre si v no es externo.

La clase ArbolBinarioEnlazado tiene un constructor sin argumentos


que regresa un arbol binario vaco. Iniciando con este arbol vaco, se puede
construir cualquier arbol binario creando el primer nodo con el metodo
addRoot y aplicando repetidamente los metodos insertLeft, insertRight
o el metodo attach. De igual forma, se puede desmantelar cualquier arbol
binario A usando la operacion remove, llegando hasta un arbol binario vaco.
Cuando una posicion v es pasado como un argumento a alguno de los
metodos de la clase ArbolBinarioEnlazado, su validez se revisa llamando un
metodo auxiliar de ayudo, checkPosition(v). Una lista de los nodos visitados
en un recorrido en preorden del arbol es construido por un metodo recursivo
preorderPositions. Las condiciones de error son indicadas lanzando excep-
ciones InvalidPositionException, BoundaryViolationException, Empty-
TreeException, y NonEmptyTreeException.

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:

Los metodos size() e isEmpty() usan una variable de instancia que


guarda el n
umero de nodos de A, y cada uno toma tiempo O(1).

7.3 Arboles Binarios 259

1 import java . util . Iterator ;


2 /* *
3 * Una implementaci o n de la interfaz ArbolBinario usando una estructura
4 * enlazada .
5 * @see ArbolBinario */
6 public class ArbolBinarioEnlazado <E > implements ArbolBinario <E > {
7 protected PosicionAB <E > raiz ; // referencia a la ra z
8 protected int tam ; // n u mero de nodos
9 /* * Crea un a rbol binario vac o */
10 public A r b o l B i n a r i o E n l a z a d o () {
11 raiz = null ; // empezar con un a rbol binario
12 tam = 0;
13 }
14 /* * Regresa el n u mero de nodos en el a rbol . */
15 public int size () {
16 return tam ;
17 }
18 /* * Indica si el a rbol est a vac o . */
19 public boolean isEmpty () {
20 return ( tam == 0);
21 }
22 /* * Indica si un nodo es interno . */
23 public boolean isInternal ( Posicion <E > v ) 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 {
24 revi saPosici on ( v ); // m e todo auxiliar
25 return ( hasLeft ( v ) || hasRight ( v ));
26 }
27 /* * Indica si un nodo es externo */
28 public boolean isExternal ( Posicion <E > v ) 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 {
29 return ! isInternal ( v );
30 }
31 /* * Indica si un nodo es la ra z */
32 public boolean isRoot ( Posicion <E > v ) 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 {
33 revi saPosici on ( v );
34 return ( v == root ());
35 }
36 /* * Indica si un nodo tiene hijo izquierdo */
37 public boolean hasLeft ( Posicion <E > v ) 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 {
38 PosicionAB <E > vv = revisaP osicion ( v );
39 return ( vv . getIzq () != null );
40 }
41 /* * Indica si un nodo tiene hijo derecho */
42 public boolean hasRight ( Posicion <E > v ) 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 {
43 PosicionAB <E > vv = revisaP osicion ( v );
44 return ( vv . getDer () != null );
45 }
46 /* * Regresa la ra z del a rbol */
47 public Posicion <E > root () throws E m p t y T r e e E x c e p t i o n {
48 if ( raiz == null )
49 throw new E m p t y T r e e E x c e p t i on ( " El a rbol est a vac
o " );
50 return raiz ;
51 }
52 /* * Regresa el hijo izquierdo de un nodo . */
53 public Posicion <E > left ( Posicion <E > v )
54 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 {
55 PosicionAB <E > vv = revisaP osicion ( v );
56 Posicion <E > posIzq = vv . getIzq ();
57 if ( posIzq == null )
58 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 ( " Sin hijo izquierdo " );
59 return posIzq ;
60 }
61 /* * Regresa el hijo izquierdo de un nodo . */
62 public Posicion <E > right ( Posicion <E > v )
63 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 {
64 PosicionAB <E > vv = revisaP osicion ( v );
65 Posicion <E > posDer = vv . getDer ();
66 if ( posDer == null )
67 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 ( " Sin hijo derecho " );
68 return posDer ;
69 }
70 /* * Regresa el padre de un nodo . */
260
Arboles

Los metodos accesores root, left, right, sibling, y parent toman


tiempo O(1).

El metodo replace(v, e) toma tiempo O(1).

Los metodos iterator() y positions() estan implementados ha-


ciendo un recorrido en preorden del arbol, usando el metodo auxiliar
preordenPosiciones. El iterador de salida es generado con el metodo
iterator() de la clase ListaNodoPosicion. Los metodos iterator()
y positions() toma tiempo O(n).

El metodo children emplea una aproximacion similar para construir


la coleccion iterable que se regresa, pero este corre en tiempo O(1), ya
que hay a lo mas dos hijos para cualquier nodo en un arbol binario.

Los metodos de actualizacion insertLeft, insertRight, attach, y


remove corren en tiempo O(1), ya que involucran manipulacion en
tiempo constante de un n
umero constante de nodos.

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

Si v es la raz de A, entonces p(v) = 1.

Si v es el hijo izquierdo de un nodo u, entonces p(v) = 2p(u).

Si v es el hijo derecho de un nodo u, entonces p(v) = 2p(u) + 1.

La funcion de numeracion p se conoce como numeraci on de nivel de


los nodos en un arbol binario A, esta numera los nodos en cada nivel de A en
orden creciente de izquierda a derecha, pudiendo brincarse algunos n umeros,
ver figura 7.13.
La funcion de numeracion de nivel p sugiere una representacion de un
arbol binario A por medio de una lista arreglo S tal que cada nodo v de A es
el elemento de S en el ndice p(v). Como se menciono en el captulo previo, se
lleva a cabo la lista arreglo S mediante un arreglo extendible, ver seccion 6.1.

7.3 Arboles Binarios 261

Figura 7.13: Numeraci


on por nivel de un arbol binario: (a) esquema general; (b)
un ejemplo.
262
Arboles

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

Figura 7.14: Representacion de un arbol binario A por medio de una lista arreglo
S.

Sea n el numero de nodos de A, y sea pM el valor maximo de p(v) sobre


todos los nodos de A. La lista arreglo S tiene tama no N = pM + 1 ya que el
elemento de S en el ndice 0 no esta asociado con ningun nodo de A. Tambien,
S tendra, en general, un n umero de elementos vacos que no se refieren a
nodos existentes de A. En el peor caso, N = 2n .
Los tiempos de ejecucion para un arbol binario A implementado con una
lista arreglo S, son iguales a los dados para el arbol binario enlazado, seccion
7.3.4.

7.3 Arboles Binarios 263

7.3.6. Recorrido de
arboles binarios

Como en los arboles generales, los calculos de arboles binarios involucran


recorridos. Se muestra enseguida ejemplos de recorridos que pueden ocuparse
para resolver algunos problemas.

Construcci
on de un
arbol de expresi
on

Para el problema de construir un arbol de expresion de una expresion


aritmetica completamente parentetizada de tama no n se da el algoritmo
construirExpresion, suponiendo que todas las operaciones aritmeticas
son binarias y las variables no estan parentetizadas. As, cada subexpresion
parentetizada contiene un operador en el centro. El algoritmo usa una pila P
mientras se explora la expresion de entrada E buscando variables, operadores,
y parentesis derechos.

Cuando se ve una variable u operador x, se crea un arbol binario de un


solo nodo A, cuya raz guarda a x, y se mete A a la pila.

Cuando se ve un parentesis derecho, ), se sacan de la cima tres arboles


de la pila S, los cuales representan una subexpresion (E1 E2 ). Entonces
se conectan los arboles para E1 y E2 en uno para , y se mete el resultado
de regreso a la pila P .

Se repite esto hasta que la expresion E ha sido procesada, en ese momento


el elemento de la cima en la pila es el arbol de expresion para E. El tiempo
total de ejecucion es O(n).
264
Arboles

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)
si no si ei =0 (0 entonces
Continuar el ciclo
si no { 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 7.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 7.2.3, puede ser especializado para arboles binarios, como se muestra

7.3 Arboles Binarios 265

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

El recorrido en postorden de un arbol binario puede ser usado para resolver


el problema de evaluacion del arbol de expresion. En este problema, se da
un arbol de expresion aritmetica, es decir, un arbol binario donde cada nodo
externo tiene asociado un valor con este y cada nodo interno tiene un operador
aritmetico asociado con este, y se quiere calcular el valor de la expresion
aritmetica representada por el arbol.
El algoritmo evaluarExpresion, dado enseguida, eval ua la expresion
asociada con el subarbol enraizado en el nodo v de un arbol A de expresion
aritmetica realizando un recorrido en postorden de A iniciando en v. En este
caso, la accion visita consiste realizar una sola operacion aritmetica. Se
supone que el arbol de expresion aritmetica es un arbol binario propio.
Algoritmo evaluarExpresion(A, v):
si v es un nodo interno en A entonces
Sea el operador guardado en v
x evaluarExpresion(A, A.left(v))
y evaluarExpresion(A, A.right(v))
regresar x y
si no
regresar el valor guardado en v
El algoritmo evaluarExpresion, al hacer un recorrido en postorden,
da un tiempo O(n) para evaluar un expresion aritmetica representada por un
arbol binario con n nodos. Al igual que en el recorrido en postorden general,
el recorrido en postorden para arboles binarios puede ser aplicado a otros
problemas de evaluacion bottom-up (ascendente) tambien, tal como calcular
el tamano dado en el ejemplo 7.5.
266
Arboles

Recorrido en orden de un
arbol binario

Un metodo de recorrido adicional para un arbol binario es el recorrido


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

Figura 7.15: Recorrido en orden de un arbol binario.



7.3 Arboles Binarios 267

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
arbol binario de
busqueda para S es un arbol binario propio A tal que

Cada nodo interno v de A guarda un elemento de S, denotado con x(v).

Para cada nodo interno v de A, los elementos guardados en el subarbol


izquierdo de v son menores que o iguales a x(v) y los elementos guardados
en el subarbol derecho de v son mayores que x(v).

Los nodos externos de A no guardan ning


un elemento.

Un recorrido en orden de los nodos internos de un arbol binario de b


usqueda
A visita los elementos en orden creciente, ver figura 7.16.

Figura 7.16: Un arbol binario de b


usqueda ordenando enteros. El camino azul solido
es recorrido cuando se busca exitosamente el 36. El camino azul discontinuo es
recorrido cuando se busca sin exito el 70.

Se puede usar un arbol binario de b usqueda A para un conjunto S para


encontrar si un valor de b usqueda y esta en S, recorriendo el camino descen-
dente del arbol A, iniciando con la raz, En cada nodo interno v encontrado,
se compara el valor de b usqueda y con el elemento x(v) guardado en v. Si
y < x(v), entonces la b usqueda contin ua en el subarbol izquierdo de v. Si
268
Arboles

y = x(v), entonces la b usqueda termina exitosamente. Si y > x(v), entonces


la busqueda continua en el subarbol derecho de v. Finalmente, si se alcanza
un nodo externo, la b usqueda termina sin exito. El arbol binario de busqueda
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 7.3.3. Por lo anterior los arboles
binarios de b
usqueda son mas eficientes cuando tienen alturas peque nas.

Recorrido en orden para dibujar un


arbol
El recorrido en orden puede ser aplicado al problema de dibujar un arbol
binario. Se puede dibujar un arbol binario A con un algoritmo que asigne
coordenadas x e y a un nodo de v de A usando las siguientes reglas, ver figura
7.17:

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.

En esta aplicacion, se toma la convencion comun en computacion para


las graficas de que la coordenada x se incrementa de izquierda a derecha y la
coordenada y se incrementa de arriba hacia abajo. As el origen esta en la
esquina superior izquierda de la pantalla de la computadora.

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.

7.3 Arboles Binarios 269

Figura 7.17: Un dibujo en orden de un arbol binario.

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 7.18. Cada nodo v de A es
encontrado tres veces por el recorrido de Euler:

Figura 7.18: Un recorrido de Euler de un arbol binario.

A la izquierda, antes del recorrido de Euler de subarbol izquierdo de


v.
Desde abajo, entre los recorridos de Euler de los dos subarboles de v.
270
Arboles

A la derecha, despues del recorrido de Euler de subarbol derecho de


v.
Si v es un nodo externo, entonces estas tres visitas suceden todas al
mismo tiempo. Se describe el recorrido de Euler del subarbol enraizado en v
en el siguiente algoritmo:
Algoritmo RecorridoEuler(A, v):
realizar la accion para visitar nodo v a la izquierda
si v tiene un hijo izquierdo u en A entonces
RecorridoEuler(A, u) { recursivamente recorrer subarbol izquierdo de v }
realizar la accion para visitar nodo v desde abajo
si v tiene un hijo derecho w en A entonces
RecorridoEuler(A, w) { recursivamente recorrer subarbol derecho de v }
realizar la accion para visitar nodo v a la derecha
El tiempo de ejecucion del recorrido de Euler de un arbol de n-nodos se
obtiene as, suponiendo que cada accion visita toma tiempo O(1). Como se
usa una cantidad constante de tiempo en cada nodo del arbol durante el
recorrido, el tiempo de ejecucion total es O(n).
El recorrido en preorden de un arbol binario es equivalente a un recorrido
de Euler tal que cada nodo tiene asociado una accion visita ocurriendo
solamente cuando este es encontrada a la izquierda. De la misma forma, los
recorridos en orden y postorden son equivalentes a un recorrido de Euler
cuando el nodo encontrado desde abajo o a la derecha, respectivamente.
Se puede usar el recorrido de Euler para hacer otros tipos de recorridos.
Por ejemplo, suponer que se desea calcular el n umero de descendientes de
cada nodo v en un arbol binario de n nodos. Se inicia un recorrido de Euler
inicializando un contador a cero, y entonces incrementando el contador cada
vez que se visita un nodo a la izquierda. Para determinar el n umero de
descendientes de un nodo v, se calcula la diferencia entre los valores del
contador cuando v es visitado a la izquierda y cuando es visitado a la derecha,
y agregado uno. Esta regla da el n umero de descendientes de v, porque cada
nodo en el subarbol enraizado es contado la visita de v y la visita de v a
la derecha. Por lo tanto, se tiene un metodo tiempo O(n) para calcular el
numero de descendientes de cada nodo.
Otra aplicacion del recorrido de Euler es imprimir una expresion aritmetica
completamente parentizada a partir de su arbol de expresion. El algoritmo
imprimirExpresio n, mostrado a continuacion, realiza esta tarea realizando
las siguientes acciones en un recorrido de Euler:

7.3 Arboles Binarios 271

Accion a la izquierda: si el nodo es interno, imprimir (


Accion desde abajo: imprimir el valor u operador guardado en el nodo
Accion a la derecha: si el nodo es interno, imprimir )
Algoritmo imprimirExpresio n(A, v):
si A.isInternal(v) entonces
imprimir (
si A.hasLeft(v) entonces
imprimirExpresion(A, A.left(v))
si A.isInternal(v) entonces
imprimir el operador guardado en v
si no
imprimir el valor guardado en v
si A.hasRight(v) entonces
imprimirExpresion(A, A.right(v))
si A.isInternal(v) entonces
imprimir )

7.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 m etodo
patr on. 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
272
Arboles

Cuando se llama con un nodo v, el metodo plantillaRecorridoEuler


llama varios metodos auxiliares en diferentes fases del recorrido. Como se
describe enseguida:

Crear una variable local r del tipo ResultadoRecorrido, la cual es usada


para guardar resultados intermedios del calculo, que tiene campos izq,
der y salida.
Se llama al metodo auxiliar visitarIzquierda(A, v, r), el cual realiza
los calculos asociados encontrandose a la izquierda del nodo.
Si v tiene hijo izquierdo, se llama recursivamente sobre el mismo con el
hijo izquierdo de v y se guarda el valor regresado en r.izq.
Se llama al metodo auxiliar visitarAbajo(A, v, r), el cual realiza los
calculos asociados encontrandose abajo del nodo.
Si v tiene hijo derecho, se llama recursivamente sobre el mismo con el
hijo derecho de v y se guarda el valor regresado en r.der.
Se llama al metodo auxiliar visitarDerecha(A, v, r), el cual realiza los
calculos asociados encontrandose a la derecha del nodo.
Regresar r.salida.

El metodo plantillaRecorridoEuler puede ser visto como una plantilla


o esqueleto de un recorrido Euleriano.

Implementaci
on Java
La clase RecorridoEuler, mostrada en el listado 7.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.
La clase, RecorridoEuler, por si misma no hace ning un calculo util. Sin
embargo, se puede extender y reemplazar los metodos auxiliares vacos para ha-
cer cosas utiles. Se ilustra este concepto usando arboles de expresion aritmetica,

7.3 Arboles Binarios 273

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
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 sitarDe recha ( 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 }

Listado 7.13: Clase RecorridoEuler.java que define un recorrido Euleriano generi-


co de un
arbol binario
274
Arboles

ver ejemplo 7.7. Se asume que un arbol de expresion aritmetica tiene objetos de
tipo TerminoExpresion en cada nodo. La clase TerminoExpresion tiene sub-
clases VariableExpresi on (para variables) y OperadorExpresion (para ope-
radores). A su vez, la clase OperadorExpresion tiene subclases para los ope-
radores aritmeticos, tales como OperadorSuma y OperadorMultiplicacion.
El metodo valor de TerminoExpresion es reemplazado por sus subclases.
Para una variable, este regresa el valor de la variable. Para un operador, este
regresa el resultado de aplicar el operador a sus operandos. Los operandos de
un operador son puesto por el metodo setOperandos de OperadorExpresion.
En los codigos de los listados 7.14, 7.15, 7.16 y 7.17 se muestran las clases
anteriores.

1 /* * Clase para un t e rmino ( operador o variable de una expresi


on
2 * aritm e
tica .
3 */
4 public class T e r m i n o E x p r e s i o n {
5 public Integer getValor () { return 0; }
6 public String toString () { return new String ( " " ); }
7 }

Listado 7.14: Clase TerminoExpresion.java para una expresion

1 /* * Clase para una variable de una expresi o n artim e tica . */


2 public class V a r i a b l e E x p r e s i o n extends Te r m i n o E x p r e s i o n {
3 protected Integer var ;
4 public V a r i a b l e E x p r e s i o n ( Integer x ) { var = x ; }
5 public void setVariable ( Integer x ) { var = x ; }
6 public Integer getValor () { return var ; }
7 public String toString () { return var . toString (); }
8 }

Listado 7.15: Clase VariableExpresion.java para una variable

En los listados 7.18 y 7.19, se muestran las clases RecorridoEvaluarExpresion


y RecorridoImprimirExpresion, especializando RecorridoEuler, que eval ua
e imprime una expresion aritmetica guardada en un arbol binario, respectiva-
mente. La clase RecorridoEvaluarExpresion reemplaza el metodo auxiliar
visitarDerecha(A, v, r) con el siguiente calculo:

Si v es un nodo externo, poner r.salida igual al valor de la variable


guardada en v;

7.3 Arboles Binarios 275

1 /* * Clase para un operador de una expresi o n aritm e tica . */


2 public class O p e r a d o r E x p r e s i o n extends Te r m i n o E x p r e s i o n {
3 protected Integer primerOperando , s eg un d oO pe ra n do ;
4 public void setOperandos ( Integer x , Integer y ) {
5 prim erOperan do = x ;
6 s eg un do O pe ra nd o = y ;
7 }
8 }

Listado 7.16: Clase OperadorExpresion.java para un operador generico

1 /* * Clase para el operador adici o n en una expresi o n aritm e


tica . */
2 public class OperadorSuma extends O p e r a d o r E x p r e s i o n {
3 public Integer getValor () {
4 // desencajonar y despu e s encajonar
5 return ( prim erOperan do + se gu n do Op er a nd o );
6 }
7 public String toString () { return new String ( " + " ); }
8 }

Listado 7.17: Clase OperadorSuma.java el operador suma

si no (v es un nodo interno), combinar r.izq y r.der con el operador


guardado en v, y pone r.salida igual al resultado de la operacion.

La clase RecorridoImprimirExpresion reemplaza los metodos visitarIzquierda,


visitarAbajo, visitarDerecha siguiendo la aproximacion de la version pseu-
docodigo dada previamente.
276
Arboles

1 /* * Calcular el valor de un a rbol de expresi o n aritm e


tica . */
2 public class R e c o r r i d o E v a l u a r E x p r e s i o n
3 extends RecorridoEuler < TerminoExpresion , Integer > {
4
5 public Integer ejecutar ( ArbolBinario < TerminoExpresion > T ) {
6 inicializar ( T ); // llamar al m e todo de la superclase
7 return recorr idoEule r ( arbol . root ()); // regresar el valor de la expresi o
n
8 }
9
10 protected void vi sitarDer echa ( Posicion < TerminoExpresion > v ,
11 ResultadoRecorrido < Integer > r ) {
12 T e r m i n o E x p r e s io n termino = v . elemento ();
13 if ( arbol . isInternal ( v )) {
14 O p e r a d o r E x p r e s i o n op = ( O p e r a d o r E x p r e s i o n ) termino ;
15 op . setOperandos ( r . izq , r . der );
16 }
17 r . salida = termino . getValor ();
18 }
19 }

Listado 7.18: Clase RecorridoEvaluarExpresion.java especializa


RecorridoEuler para evaluar la expresion asociada con un
arbol de expresion
aritmetica.

1 /* * Imprimir la expresi o n guardada en un a rbol de expresi o


n aritm
e tica . */
2 public class R e c o r r i d o I m p r i m i r E x p r e s i o n
3 extends RecorridoEuler < TerminoExpresion , String > {
4
5 public Integer ejecutar ( ArbolBinario < TerminoExpresion > T ) {
6 inicializar ( T );
7 System . out . print ( " Expresi
o n : " );
8 reco rridoEul er ( T . root ());
9 System . out . println ();
10 return null ; // nada que regresar
11 }
12
13 protected void v i s i t a r I z q u i e r d a ( Posicion < TerminoExpresion > v ,
14 ResultadoRecorrido < String > r ) {
15 if ( arbol . isInternal ( v )) System . out . print ( " ( " ); }
16
17 protected void visitarAbajo ( Posicion < TerminoExpresion > v ,
18 ResultadoRecorrido < String > r ) {
19 System . out . print ( v . elemento ()); }
20
21 protected void vi sitarDer echa ( Posicion < TerminoExpresion > v ,
22 ResultadoRecorrido < String > r ) {
23 if ( arbol . isInternal ( v )) System . out . print ( " ) " ); }
24 }

Listado 7.19: Clase RecorridoImprimirExpresion.java especializa


RecorridoEuler para imprimir la expresion asociada con un arbol de ex-
presi
on aritmetica.
Bibliografa

[1] R. Lafore, Data Structures & Algorithms, Second Edition, Sams, 2003.

[2] M. Goodrich, R. Tamassia, Data Structures and Algorithms in Java,


Fourth Edition, John Wiley & Sons, 2005.
Indice alfab
etico

ndice, 38, 185 super, 58


arbol, 231 switch, 32
arbol binario, 248 this, 58
arbol binario completo, 249 true, 23
arbol binario de busqueda, 270 try, 68
arbol binario propio, 249 void, 20
arbol impropio, 249 while, 33
arbol ordenado, 234
arboles, 231 abstraccion, 51
arboles de decision, 249 adaptabilidad, 50
Scanner, 43 adaptador, 187
break, 33, 37 ADT, 85
catch, 68, 69 alcance, 16
clone(), 42 alfabeto, 13
continue, 38 algoritmos, 85
do...while, 35 altura, 240
double, 23 amortizacion, 193
enum, 17 analisis amortizado, 193
extends, 58 analisis del peor caso, 147
false, 23 analisis promedio, 146
finally, 69 API, 70, 155
for, 34 arista, 234
if, 31 arreglo, 38
import, 47 arreglo bidimensional, 101
instanceof, 77 arreglo extendible, 192
int, 23 atrapada, 66, 68
length, 39 autoempacado, 30
new, 13
null, 14, 23 bloque, 8
return, 20, 36 bloque de sentencias, 22
INDICE ALFABETICO
279

camino, 234 especializacion, 57


campos, 7, 14 estructura de datos, 85
capacidad, 39 estructura generica, 80
caso base, 126 etiqueta, 37
casos recursivos, 126 etiquetas HTML, 169
celda, 38 evolucionabilidad, 50
ciclo for-each, 211 extension, 57
cifrador de Cesar, 99 externo, 233
clase, 7, 49, 51 extiende, 54
abstract, 9
final, 9 FIFO, 171
public, 9 firma, 18
amigable, 9 for
clase base, 54 condicion, 34
clases numero, 12 incremento, 34
cola con doble terminacion, 180 inicializacion, 34
concatenacion, 13, 27 funcion cubica, 142
constructor, 21 funcion constante, 139
conversion ancha, 76 funcion cuadratica, 141
conversion implcita, 29 funcion exponencial, 143
criptografa, 98 funcion exponente, 143
cuerpo, 18 funcion factorial, 125
funcion lineal, 141
cursor, 200
funcion logartmica, 140
definicion recursiva, 125 funciones polinomiales, 142
deque, 180
generador de n umeros pseudoaleato-
descifrado, 98
rios, 97
desempacado, 30
direccion, 13 herencia, 54
double-ended queue, 180 herencia unica, 58
herencia m ultiple, 73
elemento, 39 hermanos, 233
encadenamiento constructor, 58 heurstica, 226
encapsulacion, 51 heurstica mover al frente, 226
encriptamiento, 98 hijos, 231
enlazado dinamico, 56 hojas, 233
envo dinamico, 56
especializa, 54 identificador, 8
280 INDICE ALFABETICO

Implementar una interfaz, 51 nivel de un arbol binario, 252


insercion ordenada, 93 nodos, 105
interfaz, 70, 71 notacion Omega-grande, 150
interfaz de programacion de aplicacio- notacion Tetha-grande, 150
nes, 70, 155 numeracion de nivel, 263
iterador, 209 numeracion por nivel, 248
iterator, 209
O-grande, 147
jerarquicas, 231 objeto, 8
jerarquico, 52 objeto compuesto, 20
objetos, 7
lanza, 66 operaciones primitivas, 146
lanzados, 66 operador
LIFO, 153 new, 11
lista, 185
padre, 231
lista arreglo, 185
paquete, 46
lista circularmente enlazada, 119
parametros de tipo actual, 81
lista de favoritos, 224
parametros de tipo formal, 80
lista doblemente enlazada, 110 patron de diseno, 53
lista enlazada, 105 pila, 153
lista nodo, 197 plantilla metodo patron, 274
lista simple enlazada, 105 polimorfica, 56
literal, 23 por valor, 20
localidad de referencia, 226 portabilidad, 50
posicion, 197
metodo
posiciones en un arbol, 235
firma, 14, 56
problema de Josefo, 179
metodos, 7
profundidad, 239
modulo, 99, 175
progresion aritmetica, 59
marco de colecciones Java, 216
progresion Fibonacci, 64
matriz, 101 progresion geometrica, 59
mezcla, 73
miembros, 7 rango, 185
modificador recorrido, 242
final, 17 recorrido en orden, 269
static, 16 recorrido en postorden, 245
modificadores de clase, 9 recorrido en preorden, 242
modularidad, 52 recurrencia, 125
INDICE ALFABETICO
281

recurrencia binaria, 132


recurrencia de cola, 131
recurrencia m ultiple, 136
reemplazo, 58
referencia, 11, 13, 55
refinamiento, 58
reglas de exponentes, 144
reglas de logaritmos, 140
relativamente, 197
representacion parentetica de cadena,
243
robusto, 49
round robin, 178

secuencia, 185, 219


semilla, 97
sobrecarga, 56
subarbol derecho, 249
subarbol izquierdo, 249
sumatoria, 143
superclase, 54

tipado fuerte, 75
tipo, 7, 15
tipo generico, 80
tipos, 8
tipos base, 10
tipos de datos abstractos, 51, 85
tipos primitivos, 10

variables de instancia, 7, 14
variables locales, 22
vector, 185

Anda mungkin juga menyukai