Documente Academic
Documente Profesional
Documente Cultură
Héctor Tejeda V
Abril, 2010
Índice general
1. Introducción 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. Métodos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 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 explı́citas de control de flujo . . . . . . . . . 35
1.5. Arreglos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
1.5.1. Declaración de arreglos . . . . . . . . . . . . . . . . . . 39
1.5.2. Arreglos como objetos . . . . . . . . . . . . . . . . . . 40
1.6. Entrada y salida . . . . . . . . . . . . . . . . . . . . . . . . . 41
1.7. Clases anidadas y paquetes . . . . . . . . . . . . . . . . . . . . 45
7. Árboles 243
7.1. Árboles generales . . . . . . . . . . . . . . . . . . . . . . . . . 243
7.1.1. Definiciones de árboles y propiedades . . . . . . . . . . 243
7.1.2. El tipo de dato abstracto árbol . . . . . . . . . . . . . 247
7.1.3. Interfaz del ADT árbol . . . . . . . . . . . . . . . . . . 248
7.2. Algoritmos de recorrido para árboles . . . . . . . . . . . . . . 251
7.2.1. Profundidad y altura . . . . . . . . . . . . . . . . . . . 251
7.2.2. Recorrido en preorden . . . . . . . . . . . . . . . . . . 254
7.2.3. Recorrido en postorden . . . . . . . . . . . . . . . . . . 257
7.3. Árboles Binarios . . . . . . . . . . . . . . . . . . . . . . . . . 260
7.3.1. El ADT árbol binario . . . . . . . . . . . . . . . . . . . 262
7.3.2. Una interfaz árbol binario en Java . . . . . . . . . . . . 263
7.3.3. Propiedades del árbol binario . . . . . . . . . . . . . . 264
7.3.4. Una estructura enlazada para árboles binarios . . . . . 266
7.3.5. Árbol binario con lista arreglo . . . . . . . . . . . . . . 275
7.3.6. Recorrido de árboles binarios . . . . . . . . . . . . . . 278
7.3.7. Plantilla método patrón . . . . . . . . . . . . . . . . . 286
6 ÍNDICE GENERAL
Capı́tulo 1
Introducción
Las operaciones que se pueden realizar con los datos son llamados
métodos. Estos consisten de constructores, procedimientos, y funciones.
Los cuales definen el comportamiento de los objetos de esa clase.
8 Introducción
Listado 1.1: Clase Contador usada para llevar una cuenta simple, la cual puede ser
accesada, incrementada y decrementada
La definición de la clase está 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ública, 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 método constructor, Contador, el cual es llamado cuando se crea un
nuevo objeto Contador. La clase tiene un método accesor, getCuenta(),
el cual devuelve el valor actual del contador. También tiene dos métodos
actualizadores, incrementaCuenta() y decrementaCuenta(). Esta clase no
tiene método main() y por lo tanto no hace nada por sı́ misma.
El nombre de una clase, método, o variable en Java es llamado un identifi-
cador, el cual puede ser cualquier cadena de caracteres de tamaño arbitrario,
debiendo iniciar con una letra seguida de letras, números, y guiones bajos,
siendo letra y número 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
Cuadro 1.1: Lista de palabras reservadas en Java, por lo que no pueden ser usadas
como nombres de variables o métodos.
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 métodos abstractos. Los métodos
abstractos son declarados con la palabra reservada abstract y están
vacı́os, es decir, no tienen un bloque definiendo un cuerpo de código
para el método. Una clase abstract usualmente tiene una combinación
de métodos abstractos y métodos concretos. Lo anterior se aborda en la
sección 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úblicas son declaradas en su propio archivo separado teniendo el mismo
nombre de la clase y terminación .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ón
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 lı́nea, 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 documentación del software.
Se usa también “//” para iniciar comentarios en lı́nea y se ignora todo
hasta el final de la lı́nea.
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
Al usar el operador new con algún tipo de clase, se suceden los siguientes
tres eventos:
Un nuevo objeto es asignado dinámicamente 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ón
Objetos número
Cuando se requiere guardar números como objetos, se pueden usar las
clases envoltura de Java. A estas se les conoce como clases número y hay
una clase número por cada tipo base.
El cuadro 1.2 muestra los tipos base numéricos y su correspondiente clase
numérica. Desde Java 5, una operación de creación es realizada automática-
mente en cualquier momento que se pasa un número base a un método que
espera un objeto. De igual forma, el método accesor correspondiente es usado
automáticamente cada vez que se quiere asignar el valor de objeto Number a
un tipo base numérico.
Cuadro 1.2: Clases número de Java. Cada clase está 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ún alfa-
beto, el conjunto de todos los posibles caracteres. Cada carácter c que forma
1.1 Clases, tipos, y objetos 13
Concatenación
El procesamiento de cadenas requiere manejar cadenas. La operación
primaria para combinar cadenas es llamada concatenación, 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 operación de concatenación funciona como se indica en la
descripción. Ası́ es legal en Java escribir una sentencia de asignación como
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ón, 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ún 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 deberá referirse a algún objeto, a menos
que esta sea null, en tal caso no apunta a nada.
14 Introducción
estufa.cocinaCena();
estufa.cocinaCena(comida);
estufa.cocinaCena(comida,temporada);
Variables de instancia
Las clases de Java pueden definir variables de instancia, las cuales son
llamadas también campos. Estas variables representan los datos asociados
con los objetos de una clase. Las variables de instancia deberán 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
permitan. Por ejemplo, las variables de instancia public son accesibles para
1.1 Clases, tipos, y objetos 15
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 será usado para la variable. Adicionalmente, se puede opcionalmente
asignar un valor inicial a la variable con el operador de asignación “=”. Las
reglas para un nombre de variable son las misma que para cualquier otro
identificador. El parámetro 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 podrı́a asignar a una variable de instancia deberá 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 deberán 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
Además de los modificadores de variable de alcance, también están los
siguientes modificadores:
16 Introducción
1. static. Se usa para declarar que una variable que está asociada con la
clase, no con instancias individuales de esa clase. Las varibles static
son usadas para guardar información global de la clase y existen aún si
no se han creado instancias de la clase.
1.2. Métodos
Los métodos en Java son conceptualmente similares a funciones y procedi-
mientos en otro lenguajes de programación de alto nivel. Son lı́neas de código
que son llamadas para un objeto particular de alguna clase. Los métodos
pueden aceptar parámetros como argumentos y entonces el comportamiento
dependerá de estos y del objeto. Cada método es indicado en el cuerpo de
18 Introducción
Declaración de métodos
La sintaxis para definir un método es:
[modificadores] tipo nombre(tipo0 parámetro0, tipo1 parámetro1, . . . ){
// cuerpo del método ...
}
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 declaración define el tipo devuelto por el método. El
nombre es cualquier identificador Java válido. La lista de parámetros y sus
tipos declaran variables locales que corresponden a valores que son pasados
como argumentos al método. Cada declaración tipo puede ser cualquier tipo
Java y cada parámetro es un identificador Java. La lista de parámetros y
sus tipos puede estar vacı́a, por lo cual no se pasan valores al método al ser
llamado. Las variables parámetro, al igual que las variables de instancia de la
clase, pueden ser usadas dentro del cuerpo del método. De igual forma, otros
métodos de esta clase pueden ser llamados desde el cuerpo de un método.
Cuando un método de una clase es llamado, se invoca con una instancia
particular de esa clase y puede cambiar el estado de ese objeto, excepto para
1.2 Métodos 19
Modificadores de método
Al igual que las variables de instancia, los modificadores de método pueden
restringir el alcance de un método:
Tipos devueltos
Parámetros
Constructores
Un constructor es un tipo especial de método 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:
La sintaxis es casi la misma que para el método, excepto que el nombre del
constructor deberá ser el mismo nombre de la clase que construye, y además no
se indica que tipo devuelve, ya que su tipo devuelto es implı́citamente el mismo
que su nombre. Los modificadores del constructor siguen las mismas reglas
que los métodos, excepto que abstract, static y final no se permiten.
El constructor para una clase Camaron podrı́a ser el siguiente:
Una clase puede tener varios constructores, pero cada uno deberá tener
una firma diferente, es decir, cada uno deberá distinguirse por el tipo y la
cantidad de parámetros que toma.
22 Introducción
Método main()
Algunas clases en Java son diseñadas para ser usadas por otras clases,
otras son programas independientes. Las clases que definen programas inde-
pendientes deberán contener un tipo especial de método, el método 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
Los argumentos pasados como los parámetros args al método main() son
los argumentos lı́nea de comandos dados cuando el programa es ejecutado.
La variable args es un arreglo de objetos String, es decir, una colección
de cadenas indizadas, con la primera cadena siendo args[0], la segunda
args[1], etc.
tipo nombre;
tipo nombre = valor inicial;
{
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
asignación u otra expresión. Java permite los siguientes tipos de literales:
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 literal como float, esta debe
terminar con “F” o “f”. Literales punto flotante en notación cientı́fica
24 Introducción
también 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.
Carácter. Las constantes carácter son tomadas del alfabeto Unicode.
Un carácter está definido como un sı́mbolo individual encerrado entre
comillas simples. Por ejemplo, ’a’ y ’?’ son caracteres constantes.
Además Java define las constantes carácter especial siguientes:
1.3.2. Operadores
Las expresiones Java involucran componer literales y variables con opera-
dores.
Operador de asignación
El operador de asignación es “=”. Es usado para asignar un valor, prin-
cipalmente, a una variable de instancia o variable local. Su sintaxis es la
siguiente:
variable = expresión;
donde variable refiere a una variable que es permitida ser referenciada por el
bloque conteniendo la expresión. El valor de una operación es el valor de la
expresión que fue asignada. Por ejemplo, si i y j son ambas declaradas del
tipo int, es correcto tener una sentencia de asignación como:
i = j = 25;
La asignación anterior funciona ya que los operadores de asignación son
evaluados de derecha a izquierda.
1.3 Expresiones 25
Operadores aritméticos
Los siguientes son los operadores aritméticos binarios en Java:
+ adición
− sustracción
∗ multiplicación
/ división
% módulo
El operador módulo también es conocido como el operador “residuo”,
porque es el residuo sobrante después de realizar la división entera.
Java también proporciona un menos unario (−), el cual puede ser colocado
antes de la expresión aritmética para invertir su signo. Los paréntesis pueden
ser usados en cualquier expresión para definir el orden de evaluación. Java usa
reglas de precedencia de operadores para determinar el orden de evaluación
cuando los paréntesis 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 expresión. Si
es usado después de una variable, entonces el valor es primero leido y luego la
variable es incrementada o decrementada en uno. Por ejemplo, el fragmento
de código
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ógicos
Java permite los operadores de comparación estándar entre números:
< menor que
<= menor que o igual a
> mayor que
>= mayor que o igual a
== igual a
!= no igual a
26 Introducción
! negación (prefijo)
&& condicional y
|| condicional o
Operadores de asignación
Para expresiones del tipo
variable = variable operador expresión;
Java proporciona operadores de asignación que tienen efectos laterales de
operación. Estos operadores son de la forma
variable operador= expresión;
excepto si la variable contiene una expresión, como un ı́ndice arreglo, la
expresión es evaluada una sola vez. Ası́, el fragmento de código
1.3 Expresiones 27
a[5] = 20;
i = 5;
a[i++] += 3;
deja a[5] con el valor 23 e i con valor 6.
Concatenación de cadenas
Las cadenas pueden ser compuestas usando el operador de concatenación
(+).
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 paréntesis no son usados para determinar
el orden de evaluación. Los operadores en la misma lı́nea son evaluados
de izquierda a derecha, excepto para la asignación y operaciones prefijas,
las cuales son evaluadas de derecha a izquierda, de acuerdo a la regla de
evaluación condicional para las operaciones lógicas Y y O. Sin paréntesis, los
operadores de mayor precedencia son evaluados antes que los operadores de
menor precedencia.
Tipo Sı́mbolos
Postfijo exp++ exp−−
Prefijo ++exp −−exp +exp −exp ˜exp !exp
Conversión (tipo)exp
Mult./div. ∗ / %
add./subs. +−
desplazamiento << >> >>>
comparación < <= > >= instanceof
igualdad == ! =
y bitwise &
xor bitwise ˆ
o bitwise |
y &&
o ||
condicional ? :
asignación = += -= /= %= >>= <<= >>>= &= ˆ= |=
28 Introducción
Conversión ordinaria
Cuando se convierte de un tipo double a un int, se podrı́a perder precisión,
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
int iResultado = 3;
double dResultado = 3.2;
dResultado = i/d; // dResultado <-- 0.9375. i convertido a double
iResultado = i/d; // pérdida de precisión -> error de compilación
iResultado = (int )i/d; // iResultado <-- 0, se pierde la fracción
Sentencia if
La sintaxis de una sentencia if simple es:
if (expresión booleana)
sentencia true;
1.4 Control de flujo 31
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:
Sentencia switch
Java proporciona para control de flujo de valores múltiples la sentencia
switch, la cual es ú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 sección 1.1.3.
switch (d) {
case LUN:
32 Introducción
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");
}
1.4.2. Ciclos
Otro mecanismo importante del control de flujo en un lenguaje de progra-
mación es la repetición. Java tiene tres tipos de ciclos.
Ciclos while
El tipo de ciclo más simple es el ciclo while. Este ciclo prueba que una
cierta condición se cumpla y realizará el cuerpo del ciclo cada vez que la
condición sea evaluada a true. La sintaxis para probar una condición antes
que el cuerpo del ciclo sea ejecutado es:
while (condición)
1.4 Control de flujo 33
sentencia ciclo;
while (!regadera.estáVacı́a()) {
regar(actual,regadera);
actual = huerta.encontrarSigZanahoria():
}
}
Ciclos for
Otro tipo de ciclo es el ciclo for. Los ciclos for, en su forma más simple,
proporciona repetición de código para una determinada cantidad de veces,
pero se pueden hacer otras tareas también. La funcionalidad de un ciclo for
es flexible, gracias a que está dividido en cuatro secciones: la inicialización, la
condición, el incremento, y el cuerpo.
La sintaxis para un ciclo for es:
declarará una variable contador cuyo alcance es sólo el cuerpo del ciclo.
En la sección condición, se indica la condición para repetir el ciclo. Esta
deberá ser una expresión booleana. El cuerpo del ciclo for será ejecutado
cada vez que la condición sea true cuando sea evaluado al inicio de una
iteración posible. Tan pronto como la condición evalúe a false, entonces el
cuerpo del ciclo ya no es ejecutado, y la ejecución del programa continúa con
la siguiente sentencia después del ciclo for.
En la sección incremento, se declara la sentencia de incremento para el
ciclo. Esta deberá ser cualquier sentencia legal permitida para flexibilidad en
la codificación. La sintaxis de un ciclo for es equivalente a:
inicialización;
while (condición) {
sentencia ciclo;
incremento;
}
excepto que en Java, un ciclo while no puede tener una condición booleana
vacı́a, mientras en un ciclo for está permitido. El siguiente ejemplo muestra
un ciclo for
Ciclos do...while
El ciclo do...while prueba una condición después del cuerpo del ciclo,
a diferencia de los ciclos anteriores, los cuales prueban una condición antes
1.4 Control de flujo 35
Regresar de un método
Si un método está declarado con un tipo de retorno void, entonces el flujo
regresa cuando alcanza la última lı́nea de código en el método, o cuando se
encuentra una sentencia return sin argumento. Sin embargo, si un método
está declarado con un tipo de retorno, el método es una función y este
deberá salir regresando el valor de la función como un argumento a una
36 Introducción
Sentencia break
El uso tı́pico 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 más interna. Cuando es ejecutado break, el flujo pasa a la
siguiente sentencia después del cuerpo del ciclo o switch que contiene el
break.
La sentencia break también puede ser usada en una forma etiquetada
para salir del ciclo más externo o sentencia switch. La sintaxis es:
break etiqueta;
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 también cambia explı́citamente el flujo en un
programa, la cual tiene la siguiente sintaxis:
continue [etiqueta];
1.5. Arreglos
Una tarea de programación común es manejar un grupo numerado de
objetos relacionados. Por ejemplo, se podrı́a querer que un videojuego maneje
las diez puntuaciones mejores del juego. En vez de usar diez variables diferentes
para esa tarea, se podrı́a preferir usar un sólo nombre para el grupo y usar
un ı́ndice numérico para referirse a cada unna de las puntucaciones. De igual
forma, se podrı́a querer que un sistema de información médico maneje los
pacientes asignados a camas en un cierto hospital. De nueva cuenta no se
tienen que introducir 200 variables sólo por que el hospital tenga 200 camas.
En situaciones como las anteriores, se puede ahorrar esfuerzo de programa-
ción usando un arreglo, el cual es una colección numerada de variables todas
ellas con el mismo nombre. Cada variable, o celda, en un arreglo tiene un
38 Introducción
ı́ndice, el cual se refiere de forma única al valor guardado en esa celda. Las
celdas del arreglo a están numeradas por 0, 1, 2, etc. Se muestra enseguida
un arreglo de las diez puntuaciones más altas para un vı́deo juego.
Puntuaciones mejores
987 876 765 654 543 432 321 210 109 98
0 1 2 3 4 5 6 7 8 9
ı́ndices
Esta organización es útil, ya que permite hacer cálculos interesantes. En
el siguiente ejemplo, listado 1.6, el método, de la clase DemoArreglos suma
todos los números en un arreglo de enteros:
Listado 1.6: Método para sumar todos los elementos de un arreglo de enteros
Listado 1.7: Método para contar las veces que se encuentra k en el arreglo.
para la comparación a[i]>2 sólo será hecha si las primeras dos comparaciones
son exitosas.
El tipo elemento puede ser cualquier tipo base Java o nombre de clase, y
nombre arreglo es un identificador Java válido. Los valores iniciales deberán
ser del mismo tipo que el del arreglo. Por ejemplo, la siguiente declaración de
un arreglo que es inicializado con los primeros diez números 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 colección de celdas para el arreglo usando la siguiente sintaxis:
new tipo elemento[tamaño]
donde tamaño es un entero positivo indicando el tamaño del arreglo creado.
Usualmente esta expresión aparece en una sentencia de asignación con el
nombre del arreglo en lado izquierdo del operador de asignación. Los arreglos
creados son inicializados con ceros si el tipo del arreglo es un tipo base
numérico. Los arreglos de objetos son inicializados a referencias null. En el
siguiente código se define una variable de arreglo llamada a, y después se le
asigna un arreglo de diez celdas, cada una de tipo double, siendo después los
elementos inicializados a 1.0:
double [] a;
a = new double [10];
for (int k=0; k<a.length; ++k)
a[k] = 1.0;
b = a;
significa que a y b se refieren al mismo objeto. Ası́ que cuando se tenga una
sentencia como
b[3] = 5;
entonces también se tiene que a[3] guarda un 5.
Clonación de un arreglo
Cuando se quiere crear una copia exacta del arreglo, a, y asignar ese
arreglo al arreglo variable, b, se podrı́a 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 método clone() es un método incorporado
de cada objeto Java. Si antes de la asignación b[3] = 5; se hubiera clonado
el arreglo a en b, entonces los elementos en a y b para el ı́ndice 3 podrı́an 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.
new Scanner(System.in)
1.6 Entrada y salida 43
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 está fuertemente afiliada con otra clase. Por
ejemplo, definir la clase cursor como clase anidada en la definición de la clase
editor de texto mantiene a estas dos clases altamente relacionadas en el mismo
archivo. Más aún, permite que cada una de ellas acceda a los métodos no
públicos de la otra. Un punto técnico respecto a las clases anidadas es que
la clase anidada debe ser declarada como static. Esta declaración implica
que la clase anidada está asociada con la clase externa, no una instancia de
la clase externa, que es, un objeto especı́fico.
Paquetes
Un conjunto de clases, todas definidas en un subdirectorio común, pueden
ser un paquete Java. Cada archivo en un paquete inician con la lı́nea:
package nombre paquete;
El subdirectorio conteniendo el paquete deberá ser nombrado igual que el
paquete. Se puede también definir un paquete en sólo archivo que contenga
varias definiciones de clases, pero cuando este es compilado, todas las clases
serán compiladas en archivos separados en el mismo subdirectorio.
En Java, se pueden usar clases que están definidos en otros paquetes
prefijando los nombres de las clases con puntos que correspondan a las
estructuras de directorios de otros paquetes.
46 Introducción
import nombrePaquete.nombreClase;
package Proyecto;
import TA.Medidas.Termometro;
import TA.Medidas.Escala;
al inicio de una clase del paquete Proyecto, para indicar que se están impor-
tando las clases llamadas TA.Medidas.Termometro y TA.Medidas.Escala.
También se puede importar un paquete entero, con la siguiente sintaxis:
import nombrePaquete.*;
import TA.Medidas.*;
public boolean temperatura(Termometro termometro, int temp) {
//...
}
Robustez
Como buen programador se quiere desarrollar software que sea correcto,
es decir, que un programa dé la salida correcta para todas las entradas
anticipadas. Además, se quiere que el software sea robusto, esto es, capaz de
manejar entradas no esperadas las cuales no están explı́citamente definidas
para la aplicación. Por ejemplo, si un programa está esperando un número
punto flotante positivo, ya que representa el precio de un artı́culo, y en vez de
48 Diseño orientado al objeto
Adaptabilidad
Reutilización
Abstracción
Encapsulación
Modularidad
Abstracción
La noción de abstracción es extraer de un sistema complejo sus partes
más 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 abstracción al diseño de estructuras de datos da
lugar a tipos de datos abstractos o ADT (abstract data type). Un ADT
es un modelo matemático de una estructura de datos que especifica el tipo de
dato guardado, las operaciones soportadas, y los tipos de parámetros de las
operaciones. Un ADT indica qué puede realizar cada operación, pero no cómo
se hace. En Java, un ADT puede ser expresado por una interfaz, la cual es
una lista de declaraciones de métodos, donde cada método tiene un cuerpo
vacı́o. En la sección 2.4 se abordan las interfaces.
Un ADT es realizado por una estructura de datos concreta, la cual está 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 cómo las operaciones son realizadas
en el cuerpo de cada método. Implementar una interfaz, por una clase,
es cuando la clase incluye todos los métodos declarados en la interfaz, dando
un cuerpo para estos. Sin embargo, una clase puede tener mas métodos que
los de la interfaz.
Encapsulación
El concepto de encapsulación es otro principio importante en el diseño
orientado al objeto, el cual establece que los diferentes componentes de un
sistema de software no deberán mostrar los detalles internos de sus respectivas
implementaciones. Una de las ventajas principales de la encapsulación es
que esta da libertad al programador para implementar los detalles de un
50 Diseño orientado al objeto
Modularidad
Organización jerárquica
Recursión
Amortización
Divide y vencerás
Fuerza bruta
Método voraz
Programación dinámica
Posición
Adaptador
52 Diseño orientado al objeto
Iterador
Método modelo
Composición
Comparador
Decorador
2.2.1. Herencia
El paradigma orientado al objeto da una estructura organizacional modular
y jerárquica para reusar código, mediante la técnica de herencia. Esta técnica
permite el diseño de clases generales que pueden ser especializadas a clases
más particulares, con las clases especializadas reutilizando el código de la clase
general. La clase general, la cual también es conocida como una clase base o
superclase, puede definir variables de instancia y métodos generales que son
aplicables la mayorı́a de las veces. Una clase que especializa, o extiende o
hereda de, una superclase no necesita dar nuevas implementaciones para los
métodos generales, para eso los hereda. Sólo deberı́a definir aquellos métodos
que son especializados para la subclase particular.
Ejemplo. Considerar una clase S que define objetos con un campo, x
entero, y tres métodos, 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 métodos, d() y e() devolviendo diferentes tipos. La clase T
hereda los miembros de la clase S. Se ilustra la relación 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 rectángulo en el diagrama indica una clase,
poniendo en subrectángulos el nombre, los campos, y los métodos.
2.2 Herencia y polimorfismo 53
S
-x : int
+a : int
+b : int
+c (int n) : void
~
w
w
w
w
T
-y : int
+d : int
+e (int n) : void
Enlazado dinámico
Cuando un programa desea llamar un cierto método a() de algún objeto
o, este manda un mensaje a o, el cual se denota, usando la sintaxis operador
punto, como o.a(). Cuando se ejecuta la versión compilada del programa se le
indica al ambiente de ejecución que revise la clase T de o para indicar si la clase
T soporta un método a(), y si es ası́, lo ejecute. En la revisión, primero, se ve
si la clase T define un método 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úsqueda en la superclase de S. La búsqueda
54 Diseño orientado al objeto
2.2.2. Polimorfismo
Polimorfismo significa varias formas. En el contexto del diseño 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 deberá definir cual clase de objetos se le permite referir, en términos
de alguna clase S. Pero esto implica que o pueda también referirse a cualquier
objeto perteneciente a una clase T que extienda S. Ahora suponer lo que
sucede cuando S define un método a() al igual que T. El algoritmo de enlazado
dinámico para la invocación del método siempre inicia su búsqueda desde
la clase más restrictiva que aplique. Cuando o refiere a un objeto de clase T,
entonces usará el método a de T cuando se pida por o.a(), y no el de S. Se
dice, en este caso, que T anula el método a() de S. Por otra parte, cuando
o se refiere a un objeto de clase S , se ejecutará el método a() de S para
o.a(). El polimorfismo es ú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 método a()
se ejecute correctamente. Ası́, la variable objeto o puede ser polimórfica,
dependiendo de la clase especı́fica de los objetos que sean referenciados. Con
esta funcionalidad se permite que una clase especializada T que extiende una
clase S, herede los métodos estándar de S, y redefina otros métodos de S para
considerar las propiedad particulares de objetos de T.
Java también permite una técnica útil relacionada al polimorfismo, llamada
sobrecarga de métodos. La sobrecarga se da cuando una clase T tiene métodos
múltiples con el mismo nombre, y cada uno con firma diferente. La firma de
un método es la combinación del nombre y el tipo y la cantidad de argumentos
que le son pasados. No importa que varios métodos 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
métodos, el ambiente de ejecución determina cual método actual llamar para
un método especı́fico buscando hacia arriba en la jerarquı́a de clases para
2.2 Herencia y polimorfismo 55
encontrar el primer método con una firma que empate con el método llamado.
Por ejemplo, para una clase T, la cual define un método a() y extiende a
la clase U, qua a su vez define un método a(x,y). Si un objeto o de clase T
recibe el mensaje o.a(x,y), entonces esta es la versión de U del método a()
que es llamada. El polimorfismo verdadero aplica solo a métodos que tengan
la misma firma, pero que están definidos en clases diferentes.
Herencia, polimorfismo, y sobrecarga de métodos soportan el desarrollo de
sofware reutilizable. Se pueden definir clases que hereden variables y métodos
de instancia y luego se pueden definir variables y métodos de instancia nuevos
que traten los aspectos especiales de objetos de la nueva clase.
Especialización
Se realiza cuando se especializa una clase general a subclases particulares.
Tales subclases tienen una relación “es un(a)” a su superclase. Una subclase
hereda todos los métodos de la superclase. Para cada método heredado, si
ese método funciona correctamente no importando si está funcionando para
una especialización, no se requiere trabajo adicional. Por otra parte, si un
método general de la superclase no trabaja correctamente en la subclase,
entonces se podrı́a anular el método para lograr la funcionalidad correcta en
la subclase. Por ejemplo, se podrı́a tener una clase general, Perro, la cual
tiene métodos beber() y olfatear(). Especializando esta clase a Sabueso
quizás no requiera anular el método beber(), pero podrı́a requerirse anular
el método olfatear(), ya que un sabueso tiene más desarrollado su olfato.
Ası́, la clase Sabueso especializa los métodos de su superclase, Perro.
Extensión
En la extensión se usa herencia para reutilizar el código escrito para
métodos de la superclase, pero luego se agregan nuevos métodos en la subclase
para extender su funcionalidad. Por ejemplo, para la clase Perro, se podrı́a
crear una subclase, Xolo, la cual hereda los métodos de la superclase, paro
luego agregar un metodo nuevo, curiosidad(), ya que estos tienen un instinto
56 Diseño orientado al objeto
Listado 2.1: Programa de ejemplo que muestra el uso de la referencia this para
resolver la ambigüedad 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 numéricas. Una progresión numérica es una secuencia de números, donde
cada uno depende de uno, o de más números previos. En una progresión
aritmética se determina el siguiente número por adición y en una progre-
sión geométrica se determina el siguiente número por multiplicación. En
cualquier caso, una progresión requiere una forma para definir su primer valor
y también para identificar el valor actual.
Se inicia definiendo una clase, Progresion, mostrada en el listado 2.2, la
cual define los campos estándares y los métodos de una progresión numérica.
En particular, se definen los siguientes dos campos entero largo:
Listado 2.3: Clase para progresiones aritméticas que extiende a la clase Progresión
(listado 2.2).
60 Diseño orientado al objeto
1 /* *
2 * Progresi ó n geom é 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 ó n .
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 ó n multiplicando la base con el valor actual .
22 *
23 * @return siguiente valor de la progresi ó n
24 */
25 protected long si guiente Valor () {
26 actual *= base ;
27 return actual ;
28 }
29 // Se heredan los m é todos primerValor () e i m p r i m i r P r o g r e s i o n ( int ).
30 }
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 ó n agregando el valor previo al valor actual .
22 *
23 * @return valor siguiente de la progresi ó n
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 é todos primerValor () e i m p r i m i r P r o g r e s i o n ( int ).
32 }
2.3. Excepciones
Las excepciones son eventos no esperados que ocurren en la ejecución
de un programa. Una excepción se puede deber a una condición de error o
una entrada no anticipada. Las excepciones, en Java, pueden ser vistas como
objetos.
La cláusula throws
Al declarar un método es apropiado indicar las excepciones que podrı́a
lanzar, debido a que tiene un propósito funcional y de cortesı́a. 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 definición en un
método:
Indicando todas las excepciones que podrı́an ser lanzadas por un método,
se prepara a otros para que puedan manejar todos los casos excepcionales que
podrı́an surgir por usar este método. Otro beneficio de declarar excepciones es
que no se necesitan atrapar estas excepciones en el método. En ocasiones esto
es apropiado cuando otro código es el responsable del origen de la excepción.
Se muestra enseguida una excepción que es “pasada”:
Tipos de lanzables
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 excepción.
El ambiente de ejecución Java inicia ejecutando un bloque try...catch
con la ejecución del bloque de sentencias, bloque principal de sentencias.
Si esta ejecución no genera excepciones, entonces el flujo de control sigue
con la primera sentencia después de la última lı́nea 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 excepción, entonces la ejecución en el bloque try...catch termina en
ese punto y la ejecución pasa al bloque catch cuyo tipo excepcion sea más
cercana a la excepción lanzada. La variable para esta sentencia catch refiere
al propio objeto excepción, el cual puede ser usado en el bloque de la sentencia
catch apareado. Una vez que la ejecución de ese bloque catch se completa,
el flujo de control es pasado al bloque finally opcional, si este existe, o a la
primera sentencia después de la última lı́nea del bloque try...catch. De otra
forma, si no hay bloque catch que empate la excepción lanzada, entonces el
2.3 Excepciones 67
catch (ArrayIndexOutOfBoundsException e) {
throw new ExcepciónListaComprasCorta(
"Índice del producto no está en lista de compras");
}
La mejor forma de manejar una excepción, aunque no siempre es posible,
es encontrar el problema, arreglarlo, y continuar la ejecución.
podrı́a querer identificar algunos de los objetos como vendibles, en tal caso
estos deberı́an implementar la interfaz Vendible, listado 2.7.
La clase java.lang.Number
Las clases número Java, cuadro 1.2, especializan la clase abstracta java.-
lang.Number. Cada clase número concreta, tales como java.lang.Integer
y java.lang.Double, extienden la clase java.lang.Number y completan los
detalles para los métodos abstractos de la superclase. Por ejemplo, los métodos
intValue(), floatValue(), longValue(), y doubleValue() son todos abs-
tractos en java.lang.Number. Cada clase número concreta deberá especificar
los detalles de estos métodos.
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. Además,
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 sólo tipo,
ya sea clase o interfaz, la cual determina como la variable es usada y como
ciertos métodos actuarán en esta. De igual forma, un método tiene un único
tipo de regreso. En general, una expresión tiene un tipo único.
La técnica de tipado fuerte de Java, forzar a que todas las variables sean
tipadas y que los métodos declaren el tipo que se espera y que se regresa,
ayuda a prevenir errores. Pero por los requerimientos rı́gidos con tipos, es en
ocasiones necesario cambiar, o convertir, un tipo en otro. Tales conversiones
podrı́an tener que ser especificadas por un operador de conversión explı́cito.
En la sección 1.3.3 se indica como convertir a tipos base. En la siguiente
sección se indica como se realizan las conversiones para variables referencia.
Conversiones anchas
Una conversión 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 automáticamente para guardar el
resultado de una expresión en una variable, sin la necesidad de una conversión
explı́cita. Por lo tanto, se puede asignar directamente el resultado de una
expresión de tipo T en una variable v de tipo U cuando la conversión de T
a U es una conversión ancha. El siguiente fragmento de código muestra una
expresión de tipo Integer, un objeto nuevo construido, asignándose a una
variable de tipo Number.
Integer i = new Integer(3);
Number n = i; // conversión ancha
La correctez de una conversión ancha es revisada por el compilador y su
validez no requiere prueba por el ambiente de ejecución Java al ejecutar el
programa.
Conversiones estrechas
Una conversión 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 conversión estrecha de tipos referencia necesita una
conversión explı́cita. También, la correctez de una conversión estrecha podrı́a
no ser verificada por el compilador. Por lo tanto, su validación deberı́a ser
probada por el ambiente de ejecución en la ejecución del programa.
El siguiente fragmento de código ejemplo muestra como usar una conversión
para realizar una conversión estrecha desde el tipo Number al tipo Integer.
2.5 Conversiones y genéricos 75
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 lanzará la excepción
ClassCastException. Lo anterior se presenta en el siguiente fragmento de
código:
Number n;
Integer i;
n = new Integer(3);
i = (Integer) n; // válido
n = new Double(3.1416);
i = (Integer) n; // no válido
siendo referencia objeto una expresión que evalúa a una referencia objeto
y tipo referencia es el nombre de alguna clase, interfaz, o enumeración exis-
tente. Si referencia objeto es una instancia de tipo referencia entonces
el operador da true, de otra forma da false. Para evitar una excepción
ClassCastException que lanzarı́a el código previo, se deberá modificar de
la siguiente forma:
76 Diseño orientado al objeto
Number n;
Integer i;
n = new Integer(3);
if (n instanceof Integer)
i = (Integer) n; // válido
n = new Double(3.1416);
if (n instanceof Integer)
i = (Integer) n; // no será intentado
Estudiante estudiante2 =
miDirectorio.encontrarOtra(estudiante1);
Estudiante estudiante2 =
(Estudiante)miDirectorio.encontrarOtra(estudiante1);
2.5.2. Genéricos
A partir de Java 5.0 se incluye una estructura genérica para usar
tipos abstractos con la finalidad de evitar conversiones de tipos. Un tipo
genérico es un tipo que no está definido en tiempo de compilación, pero queda
especificado completamente en tiempo de ejecución. La estructura genérica
permite definir una clase en términos de un conjunto de parámetros de
tipo formal, los cuales podrı́an ser usados, por ejemplo, para abstraer los
tipos de algunas variables internas de la clase. Se usan paréntesis angulares
(<>) para encerrar la lista de los parámetros de tipo formal. Aunque cualquier
identificador válido puede ser usado para un parámetro de tipo formal, por
convención son usados los nombres de una sola letra mayúscula. Dada una
clase que ha sido definida con tales tipos parametrizados, se instancia un
objeto de esta clase usando los parámetros de tipo actual para indicar los
tipos concretos que serán usados.
2.5 Conversiones y genéricos 79
1 public class Par <L , V > { // entre < y > los par á 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 }
[altura, 2]
[Estudiante(Matricula: 8403725A, Nombre: Hector, Edad: 14), 9.5]
En el ejemplo, el parámetro del tipo actual puede ser un tipo arbitrario. Pa-
ra restringir el tipo de parámetro actual se puede usar la cláusula extends, co-
mo se muestra enseguida, donde la clase DirectorioParejaGenerico está de-
finida en términos de un parámetro de tipo genérico P, parcialmente especifi-
cado declarando que extiende la clase Persona.
80 Diseño orientado al objeto
DirectorioParejaGenerico<Estudiante> miDirectorioEstudiante;
Listado 2.14: Ejemplo que muestra que un tipo parametrizado sea usado para crear
un nuevo arreglo.
82 Diseño orientado al objeto
Capı́tulo 3
3.2. Arreglos
En esta sección, se exploran unas cuantas aplicaciones con arreglos.
guarden referencias null. Con este diseño se previene tener celdas vacı́as, u
“hoyos”, y los registros están desde el ı́ndice cero hasta antes de 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ño.
El método toString() devuelve una cadena representando las mayores
puntuaciones del arreglo entradas. Puede también ser empleado para propósi-
tos de depuración. La cadena será 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 después de la
primera.
Inserción
Remoción de un objeto
Listado 3.3: Codificación en la clase Puntuciones del método add() para insertar
un objeto Entrada.
Figura 3.3: Remoción en el ı́ndice 3 en el arreglo que guarda las referencias a objetos
entradaJuego.
Listado 3.5: Método Java para ordenar un arreglo de caracteres usando el algoritmo
inserción ordenada
3.2 Arreglos 95
sig = (a*actual + b) % n;
96 Arreglos, listas enlazadas y recurrencia
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ún después de que num sea ordenado, es decir, si num ya
está 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.
new String(A)
98 Arreglos, listas enlazadas y recurrencia
S.toCharArray()
El cifrador César
Una área donde se requiere poder cambiar de una cadena a un arreglo
de caracteres y de regreso es útil en criptografı́a, la ciencia de los mensajes
secretos y sus aplicaciones. Esta área 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 criptografı́a también 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
César, el cual es nombrado después de que Julio César usó este esquema
para proteger mensajes militares importantes. El cifrador de César 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 está tres letras después de esta en el alfabeto para ese lenguaje. Por lo
tanto, en un mensaje del español, se podrı́a reemplazar cada A con D, cada B
con E, cada C con F, y ası́ sucesivamente. Se continúa de esta forma hasta la
letra W, la cual es reemplazada con Z. Entonces, se permite que la sustitución
del patrón dé la vuelta, por lo que se reemplaza la X con A, Y con B, y Z
con C.
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
Listado 3.7: Una clase simple y completa de Java para el cifrador de César
3.2 Arreglos 101
0 1 2 3 4 5 6
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
Y[i][i+1] = Y[i][i] + 3;
i = a.length; // i es 5
j = Y[4].length; // j es 7
El gato
Es un juego que se practica en un tablero de tres por tres. Dos jugadores—
X y O—alternan en colocar sus respectivas marcas en las celdas del tablero,
iniciando con el jugador X. Si algún jugador logra obtener tres marcas suyas
en un renglón, columna o diagonal, entonces ese jugador gana.
La idea básica es usar un arreglo bidimensional, tablero, para mantener
el tablero de juego. Las celdas en este arreglo guardan valores para indicar
si la celda está vacı́a, o guarda una X, o una O. Entonces, el tablero es
una matriz de tres por tres, donde por ejemplo, el renglón central son las
celdas tablero[1][0], tablero[1][1], tablero[1][2]. Para este caso, se
decidió que las celdas en el arreglo tablero sean enteros, un cero indica celda
vacı́a, un uno indica una X, y un menos uno indica O. Esta codificación
permite tener una forma simple de probar si en una configuración del tablero
es una victoria para X o para O, sabiendo si los valores de un renglón, columna
o diagonal suman -3 o 3. Se ilustra lo anterior en la figura 3.5.
Figura 3.5: Una ilustración del juego del gato a la izquierda, y su representación
empleando el arreglo tablero.
3.2 Arreglos 103
La clase Gato, listado 3.8, modela el tablero del gato para dos jugadores.
El código sólo mantiene el tablero del gato y registra los movimientos; no
realiza ninguna estrategia, ni permite jugar al gato contra la computadora.
1 /* * Simulaci ó 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 á estar vac ı́ a
14 jugador = X ; // el primer jugador es ’X ’
15 }
16 /* * Poner una marca X u O en la posici ó 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 ó 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 ó 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 ó 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 ;
104 Arreglos, listas enlazadas y recurrencia
Listado 3.8: Clase Gato para simular el tablero del juego del gato.
O|X|O
-----
O|X|X
-----
X|O|X
Empate
Figura 3.7: Ejemplo de una lista simple enlazada cuyos elementos son cadenas
indicando códigos de aeropuertos. El apuntador sig de cada nodo se muestra como
una flecha. El objeto null es denotado por Ø.
Podrı́a parecer extraño tener un nodo que referencia a otro nodo, pero tal
esquema trabaja fácilmente. 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 último son usualmente llamados la
cabeza y la cola de la lista, respectivamente. Ası́, se puede saltar a través 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 está 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ño fijo predeterminado, y usa espacio
proporcional al número de sus elementos. Asimismo, no se emplean números
ı́ndices para los nodos en una lista enlazada. Por lo tanto, no se puede decir
sólo por examinar un nodo si este es el segundo, quinto u otro nodo en la
lista.
3.3 Listas simples enlazadas 107
Figura 3.8: Inserción de un elemento en la cabeza de una lista simple enlazada: (a)
antes de la inserción; (b) creación de un nuevo nodo; (c) después de la inserción.
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 método trabaja aún si la lista
está vacı́a. 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 109
Figura 3.9: Inserción en la cola de una lista simple enlazada: (a) antes de la inserción;
(b) creación de un nuevo nodo; (c) después de la inserción. Observar que se puso el
enlace sig para cola en (b) antes de asignar a la variable cola 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 método también trabaja si la lista está vacı́a. 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 apuntará al nuevo nodo}
cola ← v { hacer que la variable cola apunte al nuevo nodo }
tam ← tam + 1 {incrementar la cuenta de nodos }
110 Arreglos, listas enlazadas y recurrencia
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 113
Figura 3.12: Remoción del nodo al final de una lista doblemente enlazada con
centinelas: (a) antes de borrar en la cola; (b) borrando en la cola; (c) despueś del
borrado.
elementos en la lista. Este algoritmo también trabaja con una lista vacı́a.
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
Figura 3.14: Inserción de un nuevo nodo después del nodo que guarda JFK: (a)
creación de un nuevo nodo con el elemento BWI y enlace de este; (b) después de la
inserción.
Figura 3.15: Borrado del nodo que guarda PVD: (a) antes del borrado; (b) desenlace
del viejo nodo; (c) después de la remoción.
Algoritmo remover(v):
u ← v.getP rev() { nodo predecesor a v }
w ← v.getSig() { nodo sucesor a v }
w.setP rev(u) { desenlazar v }
u.setSig(w)
v.setP rev(null) { anular los campos de v }
v.setSig(null)
tam ← tam − 1 { decrementar el contador de nodos }
Los objetos de la clase NodoD, los cuales guardan elementos String, son
usados para todos los nodos de la lista, incluyendo los nodos centinela
cabeza y cola.
solamente. Para construir una lista de otros tipos, se puede usar una
declaración genérica.
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 á 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 ó 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 */
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 ; }
3.5 Listas circulares y ordenamiento 119
Listado 3.13: Una clase de lista circular enlazada con nodos simples.
122 Arreglos, listas enlazadas y recurrencia
Simular este juego es una aplicación ideal de una lista circular enlazada.
Los nodos pueden representar a los niños que están sentados en cı́rculo. El
niño “elegido” puede ser identificado como el niño que está sentado después
del cursor, y puede ser removido del cı́rculo para simular el recorrido alrededor.
Se puede avanzar el cursor con cada “Pato” que el elegido identifique, lo
cual se puede simular con una decisión aleatoria. Una vez que un “Ganso”
es identificado, se puede remover este nodo de la lista, hacer una selección
aleatoria para simular si el “Ganso” alcanzó 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 última partida del juego.
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ó!
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!
¡El ganso perdió!
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ó!
El circulo final es [...Pedro, Pepe, Eli, Paco, Gabi, Alex, Luis, To~no...]
Observar que cada iteración en esta ejecución del programa genera una
salida diferente, debido a las configuraciones iniciales diferentes y el uso de
opciones aleatorias para identificar patos y gansos. Además, la decisión de
que el ganso alcance al jugador parado es una decisión aleatoria.
3.5 Listas circulares y ordenamiento 123
Listado 3.14: El método main() de una aplicación que usa una lista circular enlazada
para simular el juego de niños Pato, Pato, Ganso.
124 Arreglos, listas enlazadas y recurrencia
3.6. Recurrencia
La repetición se puede lograr escribiendo ciclos, como por ejemplo con
for, o con while. Otra forma de lograr la repetición es usando recurrencia,
la cual ocurre cuando una función se llama a sı́ misma. Se han visto ejemplos
de métodos que llaman a otros métodos, por lo que no deberı́a ser extraño que
muchos de los lenguajes de programación actual, incluyendo Java, permitan
que un método se llame a sı́ mismo. En esta sección, se verá porque esta
capacidad da una alternativa poderosa para realizar tareas repetitivas.
La función factorial
Para ilustrar la recurrencia, se inicia con un ejemplo sencillo para calcular
el valor de la función factorial. El factorial de un entero positivo n, denotada
3.6 Recurrencia 125
Listado 3.15: Ordenamiento por inserción para una lista doblemente enlazada
representada por la clase ListaDoble.
por n!, está definida como el producto de los enteros desde 1 hasta n. Si n = 0,
entonces n! está definida como uno por convención. Mas formalmente, para
cualquier entero n ≥ 0,
1 si n = 0
n! =
1 · 2 · · · (n − 2) · (n − 1) · n si n ≥ 1
Para denotar n! se empleará el siguiente formato usado en los métodos de
Java, factorial(n).
La función factorial puede ser definida de una forma que sugiera una
formulación 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 términos 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ón recursiva.
1 si n = 0
factorial(n) = (3.1)
n · factorial(n − 1) si n ≥ 1
Esta definición es tı́pica de varias definiciones recursivas. Primero, esta
126 Arreglos, listas enlazadas y recurrencia
contiene uno, o más caso base, los cuales no están definidos recursivamente,
si no en términos de cantidades fijas. En este caso, n = 0 es el caso base.
Esta definición también contiene uno o más casos recursivos, los cuales
están definidos empleando la definición de la función que está siendo definida.
Observar que no hay circularidad en esta definición, porque cada vez que la
función es invocada, su argumento es más pequeño en uno.
Figura 3.16: Ejemplos de la función 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.
ejemplos concretos pequeños para ver como los subproblemas deberı́an estar
definidos.
Recurrencia de cola
Usar la recurrencia puede ser con frecuencia una herramienta útil para
diseñar algoritmos que tienen definiciones cortas y elegantes. Pero esta utilidad
viene acompañado 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 más útil 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 conversión más fácil y eficiente. Especı́ficamente, se puede
fácilmente convertir algoritmos que usan recurrencia de cola. Un algorit-
mo usa recurrencia de cola si este usa recursión lineal y el algoritmo hace
una llamada recursiva como su última operación. Por ejemplo, el algoritmo
InvertirArreglo usa recurrencia de cola.
Sin embargo, no es suficiente que la última sentencia en el método definición
incluya una llamada recurrente. Para que un método use recurrencia de cola, la
llamada recursiva deberá ser absolutamente la última parte que el método haga,
a menos que se esté en el caso base. Por ejemplo, el algoritmo SumaLineal no
usa recurrencia de cola, aún cuando su última sentencia incluye una llamada
recursiva. Esta llamada recursiva no es actualmente la última tarea que el
método realiza. Después de recibir el valor regresado de la llamada recursiva,
le agrega el valor de A[n − 1] y regresa su suma. Esto es, la última 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 través de las llamadas
recurrentes en vez de llamarlas a ellas explı́citamente. Se ilustra este tipo de
conversión revisitando el problema de invertir los elementos de un arreglo.
El siguiente algoritmo no recursivo realiza la tarea de invertir los elementos
iterando a través de llamadas recurrentes del algoritmo InvertirArreglo.
Inicialmente se llama a este algoritmo como InvertirArregloIterati-
vo(A, 0, n − 1).
132 Arreglos, listas enlazadas y recurrencia
F0 = 0
F1 = 1
Fi = Fi−1 + Fi−2 para i > 1.
Aplicando directamente la definición anterior, el algoritmo FibBinario,
mostrado a continuación, encuentra la secuencia de los números de Fibonacci
empleando recurrencia binaria.
Algoritmo FibBinario(k):
Entrada: Un entero k no negativo.
Salida: El k-ésimo número de Fibonacci Fk .
si k ≤ 1 entonces
regresar k
si no
regresar FibBinario(k − 1)+FibBinario(k − 2)
Desafortunadamente, a pesar de que la definición de Fibonacci parece una
recursión binaria, esta técnica es ineficiente en este caso. De hecho, toma un
número de llamadas exponencial para calcular el k-ésimo número de Fibonacci
de esta forma. Especı́ficamente, sea nk el número de llamadas hechas en la
134 Arreglos, listas enlazadas y recurrencia
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
Algoritmo FibLineal(k):
Entrada: Un entero k no negativo.
Salida: El par de números Fibonacci (Fk , Fk−1 ).
si k ≤ 1 entonces
regresar (k, 0)
si no
(i, j) ← FibLineal(k − 1)
regresar (i + j, i)
El algoritmo dado muestra que usando recurrencia lineal para encontrar
los números de Fibonacci es mucho más eficiente que usando recurrencia
binaria. Ya que cada llama recurrente a FibLineal decrementa el argumento
k en 1, la llamada original FibLineal(k) resulta en una serie de k − 1
llamadas adicionales. Esto es, calcular el k-ésimo número de Fibonacci usando
recurrencia lineal requiere k llamadas al método. Este funcionamiento es
significativamente más rápido que el tiempo exponencial necesitado para
el algoritmo basado en recurrencia binaria. Por lo tanto, cuando se usa
recurrencia binaria, se debe primero tratar de particionar completamente el
problema en dos, o se deberı́a estar seguro que las llamadas recursivas que se
traslapan son realmente necesarias.
Usualmente, se puede eliminar el traslape de llamadas recursivas usando
más memoria para conservar los valores previos. De hecho, esta aproximación
es una parte central de una técnica llamada programación dinámica, la cual
está relacionada con la recursión.
Algoritmo ResolverAcertijo(k, S, U ):
Entrada: Un entero k, secuencia S, y conjunto U .
Salida: Una enumeración 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 está ahora siendo usado }
Agregar e al final de S
si k = 1 entonces
Probar si S es una configuración que resuelva el acertijo
si S resuelve el acertijo entonces
regresar “Solución encontrada: ” S
si no
ResolverAcertijo(k − 1, S, U )
Agregar e de regreso a U { e está ahora sin uso }
Remover e del final de S
138 Arreglos, listas enlazadas y recurrencia
Capı́tulo 4
Herramientas de análisis
4.1. Funciones
Se revisan brevemente, en esta sección, las siete funciones más importantes
usadas en el análisis de algoritmos.
f (n) = c,
log n = log2 n.
Hay algunas reglas importantes para logaritmos, parecidas a las reglas de
exponentes.
Proposición 4.1: Reglas de logaritmos dados números 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
Como notación abreviada, se usa logc n para denotar la función (log n)c .
4.1 Funciones 141
f (n) = n.
Esto es, dado un valor de entrada n, la función lineal f asigna el propio
valor n.
Esta función surge en el análisis de algoritmos cuando se tiene que ha-
cer una operación básica para cada uno de los n elementos. Por ejemplo,
comparar un número x con cada uno de los elementos de una arreglo de
tamaño n requerirá n comparaciones. La función lineal también representa el
mejor tiempo de ejecución que se espera lograr para cualquier algoritmo que
procese una colección de n objetos que no estén todavı́a en la memoria de la
computadora, ya que leer los n objetos requiere n operaciones.
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 función crece un poco más rápido que la función lineal, pero es
más rápida que la función cuadrática. Por lo tanto, si se logra mejorar el
tiempo de ejecución de algún problema desde un tiempo cuadrático a n-log-n,
se tendrá un algoritmo que corra más rápido en general.
f (n) = n2 .
Para un valor de entrada n, la función f asigna el producto de n con ella
misma, es decir, “n cuadrada”.
La razón principal por la que la función cuadrática aparece en el análisis
de algoritmos es porque hay muchos algoritmos que tienen ciclos anidados,
donde el ciclo interno realiza un número lineal de operaciones y el ciclo
142 Herramientas de análisis
1 + 2 + 3 + · · · + (n − 2) + (n − 1) + n.
En otras palabras, este es el número total de operaciones que serán hechas
por el ciclo anidado, si el número de operaciones realizadas dentro del ciclo
se incrementa por uno con cada iteración del ciclo exterior.
Se cree que Carl Gauss usó la siguiente identidad para resolver el problema
que le habı́an dejado de encontrar la suma desde 1 hasta 100, cuando tenı́a
aproximadamente 10 años.
Proposición 4.2: Para cualquier entero n ≥ 1, se tiene
n(n + 1)
1 + 2 + 3 + · · · + (n − 2) + (n − 1) + n = .
2
En la figura 4.1 se da la justificación visual de la proposición 4.2.
Lo que se desprende de este resultado es que si se ejecuta un algoritmo
con ciclos anidados tal que las operaciones del ciclo anidado se incrementan
en uno cada vez, entonces el número total de operaciones es cuadrático en el
número de veces, n, que se hace el ciclo exterior. En particular, el número de
operaciones es n2 /2 + n/2, en este caso, lo cual es un poco más que un factor
constante (1/2) veces la función cuadrática n2 .
f (n) = n3 ,
la cual asigna a un valor de entrada n el producto de n con el mismo tres
veces. Esta función aparece con menor frecuencia en el contexto del análisis
4.1 Funciones 143
Polinomiales
f (n) = a0 + a1 n + a2 n2 + a3 n3 + · · · + ad nd ,
Sumatorias
Una notación que aparece varias veces en el análisis de las estructuras de
datos y algoritmos es la sumatoria, la cual está definida como sigue:
b
X
f (i) = f (a) + f (a + 1) + f (a + 2) + · · · + f (b),
i=a
f (n) = bn ,
donde b es una constante positiva, llamada la base, y el argumento n es el
exponente, es decir, la función f (n) asigna al argumento de entrada n el
valor obtenido de multiplicar la base b por sı́ misma n veces. En análisis de
algoritmos, la base más común para la función exponencial es b = 2. Por
ejemplo, si se tiene un ciclo que inicia realizando una operación y entonces
dobla el número de operaciones hechas con cada iteración, entonces el número
de operaciones realizadas en la n-ésima iteración es 2n . Además, una palabra
entera conteniendo n bits puede representar todos los enteros no negativos
4.1 Funciones 145
1. (ba )c = bac
2. ba bc = ba+c
3. ba /bc = ba−c
Sumas geométricas
Cuando se tiene un ciclo donde cada iteración toma un factor multipli-
cativo mayor que la previa, se puede analizar este ciclo usando la siguiente
proposición.
Proposición 4.4: Para cualquier entero n ≥ 0 y cualquier número real a tal
que a > 0 y a 6= 1, considerar la sumatoria
n
X
ai = 1 + a + a2 + · · · + an
i=0
an+1 − 1
.
a−1
146 Herramientas de análisis
1 + 2 + 4 + 8 + · · · + 2n−1 = 2n − 1,
se calcula el entero más grande que puede ser representado en notación binaria
usando n bits.
Figura 4.2: Relaciones de crecimiento para las siete funciones fundamentales usadas
en análisis de algoritmos.
Puede ser realizado estudiando una descripción de alto nivel del algorit-
mo sin tenerlo que implementar o ejecutar experimentos en este.
Esta metodologı́a permite asociar, con cada algoritmo, una función f (n)
que caracteriza el tiempo de ejecución del algoritmo como una función del
150 Herramientas de análisis
Llamar un método.
Indexar en un arreglo.
ya que logrando que el algoritmo se comporte bien para el peor caso, entonces
también lo hara para cualquier entrada.
La notación “O-grande”
Sean f (n) y g(n) funciones que mapean enteros no negativos a números
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) = a0 + a1 n + · · · ad nd ,
y ad > 0, entonces f (n) es O(nd ).
Justificación: observar que, para n ≥ 1, se tiene 1 ≤ n ≤ n2 ≤ · · · ≤ nd por
lo tanto
a0 + a1 n + · · · + ad nd ≤ (a0 + a1 + · · · + ad )nd .
4.2 Análisis de algoritmos 155
Ω-grande
La notación O-grande proporciona una forma asintótica de decir que una
función es “menor que o igual a” otra función, la siguiente notación da una
forma asintótica para indicar que una función crece a un ritmo que es “mayor
que o igual a” otra función.
Sean f (n) y g(n) funciones que mapean enteros no negativos a números
reales. Se dice que f (n) es Ω(g(n)) (pronunciada “f(n) es notación 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
Θ (Teta-grande)
Además, hay una notación 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
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
f (n)esO(g(n)).
Una expresión más matemática para lo anterior es,
f (n) ∈ O(g(n)),
ya que la notación O-grande está indicando una colección completa de
funciones.
Advertencia
El uso de la O-grande y las notaciones relacionadas puede ser engañoso
debido a que los factores constantes que “ocultan” pueden ser muy grandes.
Por ejemplo, es cierto que la función 10100 n es O(n), si este es el tiempo de
ejecución de un algoritmo siendo comparado con otro cuyo tiempo de ejecución
4.2 Análisis de algoritmos 159
es 10n log n, se podrı́a preferir el algoritmo de tiempo O(n log n), a pesar de
que el algoritmo lineal es asintóticamente más rápido. La decisión es por el
factor constante, 10100 , “un googol”, considerado por muchos astrónomos
como una cota superior de la cantidad de átomos en el universo observable.
Serı́a improbable tener un problema del mundo real que tenga esa cantidad
como tamaño de entrada. Por lo tanto, incluso cuando se usa la notación
O-grande, se debe ser consciente de los factores constantes y los términos de
orden inferior que se están “ocultando”.
En general, cualquier algoritmo ejecutándose en tiempo O(n log n), con
un factor constante razonable, deberá ser considerado eficiente. Un método
con tiempo O(n2 ) podrı́a ser lo suficente rápido en algunos casos, por ejemplo,
cuando n es pequeño. Pero un algoritmo ejecutándose en tiempo O(2n ) casi
nunca deberı́a ser considerado eficiente.
Para los dos ciclos anidados para, el cuerpo del ciclo externo, controlado
por el contador i, es ejecutado n veces para i = 0, . . . , n − 1. Por lo
que sentencias como a ← 0 y A[i] ← a/(i + 1) son ejecutadas n veces
cada una. Estas dos sentencias, además del incremento y la prueba
del contador i, contribuyen una cantidad de operaciones primitivas
proporcional a n, o sea, tiempo O(n).
Para mostrar como esta definición trabaja, se dan los siguientes ejemplos:
2
24 = 2(4/2) = (24/2 )2 = (22 )2 = 42 = 16
2
25 = 21+(4/2) = 2(24/2 )2 = 2(22 )2 = 2(42 ) = 32
2
26 = 2(6/2) = (26/2 )2 = (23 )2 = 82 = 64
2
27 = 21+(6/2) = 2(26/2 )2 = 2(23 )2 = 2(82 ) = 128
Pilas y colas
5.1. Pilas
Una pila (stack ) es una colección de objetos que son insertados y removidos
de acuerdo al principio ú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 más reciente insertado, es decir, el “último” objeto puede ser
removido en cualquier momento. Una analogı́a de la pila es el dispensador de
platos que se encuentra en el mobiliario de alguna cafeterı́a 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 está 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 dirección del sitio es empujada en la pila de
direcciones. El navegador, mediante el botón Volver atrás, permite al usuario
sacar los sitios previamente visitados.
estructura que guarda objetos genéricos Java e incluye, entre otros, los
métodos push(), pop(), peek() (equivalente a top()), size(), y empty()
(equivalente a isEmpty()). Los métodos pop() y peek() lanzan la excep-
ción EmptyStackException del paquete java.util si son llamados con pilas
vacı́as. Mientras sea conveniente sólo usar la clase incorporada java.util.-
Stack, es instructivo aprender como diseñar e implementar una pila “desde
cero”.
Implementar un tipo de dato abstracto en Java involucra dos pasos. El
primer paso es la definición de una interfaz de programación de aplica-
ciones o API, o interfaz, la cual describe los nombres de los métodos que
la estructura abstracta de datos soporta y como tienen que ser declarados y
usados.
Además, se deben definir excepciones para cualquier condición de error
que pueda originarse. Por ejemplo, la condición de error que ocurre cuando se
llama al método pop() o top() en una cola vacı́a es señalado lanzando una
excepción de tipo EmptyStackException, la cual está definida en el listado
5.1
1 /* *
2 * Excepci ó n Runtime lanzada cuando se intenta hacer una operaci ó n
3 * top o pop con una cola vac ı́ a .
4 */
5 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 {
6 public E m p t y S t a c k E x c e p t i o n ( String err ) {
7 super ( err );
8 }
9 }
Listado 5.1: Excepción lanzada por los métodos pop() y top() de la interfaz pila
cuando son llamados con una pila vacı́a.
Una interfaz completa para el ADT pila está 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 genéricos (sección 2.5.2).
Para que un ADT dado sea para cualquier uso, se necesita dar una clase
concreta que implemente los métodos de la interfaz asociada con ese ADT. Se
da una implementación simple de la interfaz Stack en la siguiente subsección.
168 Pilas y colas
1 /* *
2 * Interfaz para una pila : una colecci ó n de objetos que son insertados
3 * y removidos de acuerdo al principio ú ltimo en entrar , primero en salir .
4 * Esta interfaz incluye los m é 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 public interface Stack <E > {
9 /* *
10 * Regresa el n ú mero de elementos en la pila .
11 * @return n ú mero de elementos en la pila .
12 */
13 public int size ();
14 /* *
15 * Indica si la pila est á vacia .
16 * @return true si la pila est á vac ı́a , de otra manera false .
17 */
18 public boolean isEmpty ();
19 /* *
20 * Explorar el elemento en la cima de la pila .
21 * @return el elemento cima en la pila .
22 * @exception E m p t y S t a c k E x c e p t i o n si la pila est á vac ı́ a .
23 */
24 public E top ()
25 throws E m p t y S t a c k E x c e p t i o n ;
26 /* *
27 * Insertar un elemento en la cima de la pila .
28 * @param elemento a ser insertado .
29 */
30 public void push ( E elemento );
31 /* *
32 * Quitar el elemento de la cima de la pila .
33 * @return elemento removido .
34 * @exception E m p t y S t a c k E x c e p t i o n si la pila est á vac ı́ a .
35 */
36 public E pop ()
37 throws E m p t y S t a c k E x c e p t i o n ;
38 }
Listado 5.2: Interfaz Stack con documentación en estilo Javadoc. Se usa el tipo
parametrizado genérico, E, para que la pila pueda contener elementos de cualquier
clase.
con -1, y se usa este valor de t para identificar una pila vacı́a. Asimismo,
se pueda usar t para determinar el número de elementos (t+1). Se agrega
también una nueva excepción, llamada FullStackException, para señalar
el error que surge si se intenta insertar un nuevo elemento en una pila llena.
La excepción FullStackException es particular a esta implementación y no
está definida en la ADT pila. Se dan los detalles de la implementación de la
pila usando un arreglo en los siguientes algoritmos.
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
t←t+1
S[t] ← e
Algoritmo pop():
si isEmpty() entonces
lanzar una EmptyStackException
e ← S[t]
S[t] ← null
t←t−1
regresar e
170 Pilas y colas
Método Tiempo
size O(1)
isEmpty O(1)
top O(1)
push O(1)
pop O(1)
1 /* *
2 * Implementaci ó n de la ADT pila usando un arreglo de longitud fija .
3 * Una excepci ó n es lanzada si la operaci ó 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 é todos principales de la clase agregada
6 * java . util . Stack .
7 *
8 */
9 public class ArregloStack <E > implements Stack <E > {
10 protected int capacidad ; // capacidad actual del arreglo del stack
11 public static final int CAPACIDAD = 1000; // capacidad por defecto
12 protected E [] S ; // arreglo ègenrico usado para implementar el stack
13 protected int t = -1; // ı́ ndice para la cima del stack
14 public ArregloStack () {
15 this ( CAPACIDAD ); // capacidad por defecto
16 }
17 public ArregloStack ( int cap ) {
18 capacidad = cap ;
19 S = ( E []) new Object [ capacidad ]; // el compilador podr ı́ a dar advertencia
20 // pero est á bien
21 }
22 public int size () {
23 return ( t + 1);
24 }
25 public boolean isEmpty () {
26 return ( t < 0);
27 }
28 public void push ( E elemento ) throws F u l l S t a c k E x c e p t i o n {
29 if ( size () == capacidad )
30 throw new F u l l S t a c k E x c e p t i on ( " La pila est á llena . " );
31 S [++ t ] = elemento ;
32 }
33 public E top () throws E m p t y S t a c k E x c e p t i o n {
34 if ( isEmpty ())
35 throw new E m p t y S t a c k E x c e p t i o n ( " La pila est á vac ı́ a . " );
36 return S [ t ];
37 }
38 public E pop () throws E m p t y S t a c k E x c e p t i o n {
39 if ( isEmpty ())
40 throw new E m p t y S t a c k E x c e p t i o n ( " La pila est á vac ı́ a . " );
41 E elemento = S [ t ];
42 S [t - -] = null ; // des referenciar S [ t ] para el colector de basura .
43 return elemento ;
44 }
45 public String toString () {
46 String s = " [ " ;
47 if ( size () > 0) s += S [0];
48 for ( int i = 1; i <= size () -1; i ++)
49 s += " , " + S [ i ];
50 return s + " ] " ;
51 }
52 // Imprime informaci ó n del estado de la pila y de la ú ltima operaci ó n
53 public void estado ( String op , Object elemento ) {
54 System . out . print ( " ----- -> " + op ); // imprime esta operaci ó n
55 System . out . println ( " , regresa " + elemento ); // que fue regresado
56 System . out . print ( " resultado : num . elems . = " + size ());
172 Pilas y colas
Salida ejemplo
Se muestra enseguida la salida del programa ArregloStack. Con el uso
de tipos genéricos, se puede crear un ArregloStack A para guardar enteros
y otro ArregloStack B para guardar String.
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 é todos accesores :
20 public E getElemento () {
21 return elemento ;
22 }
23 public Nodo <E > getSig () {
24 return sig ;
25 }
26 // M é 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 genérico para una lista simple
enlazada.
5.1 Pilas 175
Listado 5.5: Clase NodoStack implementando la interfaz Stack usando una lista
simple enlazada con los nodos genéricos del listado 5.4.
Listado 5.6: Método genérico para invertir los elementos en un arreglo genérico
mediante una pila de la interfaz Stack<E>
[(5 + x) − (y + z)].
Los siguientes ejemplos ilustran este concepto:
Correcto: ()(()){([()])}
Correcto: ((()(()){([()])}))
Incorrecto: )(()){([()])}
Incorrecto: (
p: párrafo
Listado 5.7: Clase HTML para revisar el apareamiento de las etiquetas en un docu-
mento HTML.
5.2. Colas
Otra estructura de datos fundamental es la cola (queue). Una cola es una
colección de objetos que son insertados y removidos de acuerdo al principio
primero en entrar, primero en salir, o FIFO (first-in first-out). Esto es, los
elementos pueden ser insertados en cualquier momento, pero sólo el elemento
que ha estado en la cola por más tiempo es el siguiente en ser removido.
Se dice que los elementos entran a una cola por la parte de atrás y son
removidos del frente. La metáfora para esta terminologı́a es una fila de gente
esperando para subir a un juego mecánico. 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
lı́nea.
182 Pilas y colas
El tipo de dato abstracto cola define una colección que guarda los objetos
en una secuencia, donde el acceso al elemento y borrado están restringidos
al primer elemento en la secuencia, el cual es llamado el frente de la cola, y
la inserción del elemento está restringida al final de la secuencia, la cual es
llamada la parte posterior de la cola. Esta restricción forza la regla de que
los elementos son insertados y borrados en una cola de acuerdo al principio
primero en entrar, primero en salir, FIFO.
El ADT cola soporta los siguientes dos métodos fundamentales:
Adicionalmente, de igual modo como que con el ADT pila, el ADT cola
incluye los siguientes métodos de apoyo:
Interfaz java.util.Queue
El API de Java proporciona una interfaz cola, java.util.Queue, la cual
tiene funcionalidad similar al ADT cola, dado previamente, pero la documen-
tación para esta interfaz no indica que sólo soporte el principio FIFO. Cuando
se soporta FIFO, los métodos de las dos interfaces son equivalentes.
ADT Queue Interfaz java.util.Queue
size() size()
isEmpty() isEmpty()
enqueue(e) add(e) o offer(e)
dequeue() remove() o poll()
front() peek() o element()
Hay clases concretas del paquete java.util.concurrent de Java que
implementan la interfaz java.util.Queue con el principio FIFO como:
ArrayBlockingQueue, ConcurrentLinkedQueue y LinkedBlockingQueue
184 Pilas y colas
Interfaz cola
Una interfaz Java para el ADT cola se proporciona en el listado 5.8. Esta
interfaz genérica indica que objetos de tipo arbitrario pueden ser insertados
en la cola. Por lo tanto, no se tiene que usar conversión explı́cita cuando se
remuevan elementos.
Los métodos size() e isEmpty() tienen el mismo significado como sus
contrapartes en el ADT pila. Estos dos métodos, al igual que el método
front(), son conocidos como métodos accesores, por su valor regresado y no
cambian el contenido de la estructura de datos.
1 /* *
2 * Interfaz para una cola : una colecci ó 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 public interface Queue <E > {
9 /* *
10 * Regresa el numero de elementos en la cola .
11 * @return n ú mero de elementos en la cola .
12 */
13 public int size ();
14 /* *
15 * Indica si la cola est á vac ı́ a
16 * @return true si la cola est á vac ı́a , false de otro modo .
17 */
18 public boolean isEmpty ();
19 /* *
20 * Regresa el elemento en el frente de la cola .
21 * @return elemento en el frente de la cola .
22 * @exception E m p t y Q u e u e E x c e p t i o n si la cola est á vac ı́ a .
23 */
24 public E front () throws E m p t y Q u e u e E x c e p t i o n ;
25 /* *
26 * Insertar un elemento en la zaga de la cola .
27 * @param elemento nuevo elemento a ser insertado .
28 */
29 public void enqueue ( E element );
30 /* *
31 * Quitar el elemento del frente de la cola .
32 * @return elemento quitado .
33 * @exception E m p t y Q u e u e E x c e p t i o n si la cola est á vac ı́ a .
34 */
35 public E dequeue () throws E m p t y Q u e u e E x c e p t i o n ;
36 }
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.
Algoritmo isEmpty():
return (f = r)
Algoritmo front():
si isEmpty() entonces
lanzar una EmptyQueueException
return Q[f ]
5.2 Colas 187
Algoritmo enqueue(e):
si size()= N − 1 entonces
lanzar una FullQueueException
Q[r] ← e
r ← (r + 1) mód N
Algoritmo dequeue():
si isEmpty() entonces
lanzar una EmptyQueueException
e ← Q[f ]
Q[f ] ← null
f ← (f + 1) mód N
return e
La implementación anterior contiene un detalle importante, la cual podrı́a
ser ignorada al principio. Considerar la situación que ocurre si se agregan N
objetos en Q sin quitar ninguno de ellos. Se tendrı́a f = r, la cual es la misma
condición que ocurre cuando la cola está vacı́a. Por lo tanto, no se podrı́a
decir la diferencia entre una cola llena y una vacı́a en este caso. Existen varias
formas de manejarlo.
La solución que se describe es considerando que Q no puede tener más de
N − 1 objetos. Esta regla simple para manejar una cola llena se considera
en el algoritmo enqueue dado previamente. Para señalar que no se pueden
insertar más elementos en la cola, se emplea una excepción especı́fica, llamada
FullQueueException. Para determinar el tamaño de la cola se hace con
la expresión (N − f + r) mód N , la cual da el resultado correcto en una
configuración “normal”, cuando f ≤ r, y en la configuración “envuelta”
cuando r < f . La implementación Java de una cola por medio de un arreglo
es similar a la de la pila, y se deja como ejercicio.
Al igual que con la implementación de la pila con arreglo, la única desven-
taja de la implementación de la cola con arreglo es que artificialmente se pone
la capacidad de la cola a algún valor fijo. En una aplicación real, se podrı́a
actualmente necesitar más o menos capacidad que esta, pero si se tiene una
buena estimación de la capacidad, entonces la implementación con arreglo es
bastante eficiente.
La siguiente tabla muestra los tiempos de ejecución de los métodos de
una cola empleando un arreglo. Cada uno de los métodos en la realización
arreglo ejecuta un número constante de sentencias involucrando operaciones
aritméticas, comparaciones, y asignaciones. Por lo tanto, cada método en esta
188 Pilas y colas
Método Tiempo
size O(1)
isEmpty O(1)
front O(1)
enqueue O(1)
dequeue O(1)
Listado 5.9: Implementación del método enqueue() del ADT cola usando de una
lista simple ligada con nodos de la clase Nodo.
Listado 5.10: Implementación del método dequeue() del ADT cola usando una
lista simple ligada con nodos de la clase Nodo.
5.2 Colas 189
Cada uno de los métodos de la implementación del ADT cola, con una
implementación de lista ligada simple, corren en tiempo O(1). Se evita la
necesidad de indicar un tamaño máximo para la cola, como se hizo en la
implementación 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 métodos en la implementación de la cola con la lista ligada son
más complicados que lo que se desearı́a, se debe tener cuidado especial de el
manejo de los casos especiales, como cuando la cola está vacı́a y se hace la
operación enqueue, o cuando la cola se vacı́a por la operación dequeue.
1. e ← Q.dequeue()
2. Servir elemento e
3. Q.enqueue(e)
El problema de Josefo
En el juego de niños de la “Papa Caliente”, un grupo de n niños sentados
en cı́rculo se pasan un objeto, llamado la “papa” alrededor del cı́rculo. El
juego se inicia con el niño que tenga la papa pasándola al siguiente en el
cı́rculo, y estos continúan pasando la papa hasta que un coordinador suena
una campana, en donde el niño que tenga la papa deberá dejar el juego y pasar
la papa al siguiente niño en el cı́rculo. Después de que el niño seleccionado
deja el juego, los otros niños estrechan el cı́rculo. Este proceso es entonces
continuado hasta que quede solamente un niño, que es declarado el ganador.
190 Pilas y colas
Operación 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 ()
1 /* *
2 * Interfaz para una deque : una colecci ó n de objetos que pueden ser
3 * insertados y quitados en ambos extremos ; un subconjunto de los
4 * m é todos de java . util . LinkedList .
5 *
6 */
7 public interface Deque <E > {
8 /* *
9 * Regresa el n ú mero de elementos en la deque .
10 */
11 public int size ();
12 /* *
13 * Determina si la deque est á vac ı́ a .
14 */
15 public boolean isEmpty ();
16 /* *
17 * Regresa el primer elemento ; una excepci ó n es lanzada si la deque
18 * est á 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 ú ltimo elemento ; una excepci ó n es lanzada si la deque
23 * est á 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 ú ltimo en la deque .
32 */
33 public void addLast ( E elemento );
34 /* *
35 * Quita el primer elemento ; una excepci ó n es lanzada si la deque
36 * est á 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 ú ltimo elemento ; una excepci ó n es lanzada si la deque
41 * est á 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 con documentación estilo Javadoc. Se usa el tipo
parametrizado genérico E con lo cual una deque puede contener elementos de
cualquier clase especificada.
194 Pilas y colas
1 /* *
2 * Implementaci ó 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 NodoDeque <E > implements Deque <E > {
7 protected NodoDL <E > cabeza , cola ; // nodos centinelas
8 protected int tam ; // cantidad de elementos
9 /* * Constructor sin par á metros para crear una deque vac ı́ a . */
10 public NodoDeque () { // inicializar una deque vac ı́ a
11 cabeza = new NodoDL <E >();
12 cola = new NodoDL <E >();
13 cabeza . setSig ( cola ); // hacer que cabeza apunte a cola
14 cola . setPrev ( cabeza ); // hacer que cola apunte a cabeza
15 tam = 0;
16 }
17 public int size () {
18 return tam ;
19 }
20 public boolean isEmpty () {
21 return tam == 0;
22 }
23 public E getFirst () throws E m p t y D e q u e E x c e p t i o n {
24 if ( isEmpty ())
25 throw new E m p t y D e q u e E x c e p t i o n ( " Deque est á vac ı́ a . " );
26 return cabeza . getSig (). getElemento ();
27 }
28 public E getLast () throws E m p t y D e q u e E x c e p t i o n {
29 if ( isEmpty ())
30 throw new E m p t y D e q u e E x c e p t i o n ( " Deque est á vac ı́ a . " );
31 return cola . getPrev (). getElemento ();
32 }
33 public void addFirst ( E o ) {
34 NodoDL <E > segundo = cabeza . getSig ();
35 NodoDL <E > primero = new NodoDL <E >( o , cabeza , segundo );
36 segundo . setPrev ( primero );
37 cabeza . setSig ( primero );
38 tam ++;
39 }
40 public void addLast ( E o ) {
41 NodoDL <E > penultimo = cola . getPrev ();
42 NodoDL <E > ultimo = new NodoDL <E >( o , penultimo , cola );
43 penultimo . setSig ( ultimo );
44 cola . setPrev ( ultimo );
45 tam ++;
46 }
47 public E removeFirst () throws E m p t y D e q u e E x c e p t i o n {
48 if ( isEmpty ())
49 throw new E m p t y D e q u e E x c e p t i o n ( " Deque est á vac ı́ a . " );
50 NodoDL <E > primero = cabeza . getSig ();
51 E e = primero . getElemento ();
52 NodoDL <E > segundo = primero . getSig ();
53 cabeza . setSig ( segundo );
54 segundo . setPrev ( cabeza );
55 tam - -;
56 return e ;
5.3 Colas con doble terminación 195
57 }
58 public E removeLast () throws E m p t y D e q u e E x c e p t i o n {
59 if ( isEmpty ())
60 throw new E m p t y D e q u e E x c e p t i o n ( " Deque est á vac ı́ a . " );
61 NodoDL <E > ultimo = cola . getPrev ();
62 E e = ultimo . getElemento ();
63 NodoDL <E > penultimo = ultimo . getPrev ();
64 cola . setPrev ( penultimo );
65 penultimo . setSig ( cola );
66 tam - -;
67 return e ;
68 }
69 }
Listado 5.13: Clase NodoDeque que implementa la interfaz Deque con la clase NodoDL
(que no se muestra). NodoDL es un nodo de una lista doblemente enlazada genérica.
196 Pilas y colas
Capı́tulo 6
Listas e Iteradores
Operación 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 199
Cuadro 6.1: Realización de una deque por medio de una lista arreglo.
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}
n←n−1
regresar e
Método 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)
Para construir una implementación Java del ADT lista arreglo, se mues-
tra, en el listado 6.1 la interfaz ListaIndice, la cual captura los méto-
dos principales del ADT lista arreglo. En este caso, se usa la excepción
IndexOutOfBoundsException para indicar un argumento con ı́ndice inválido.
1 /* *
2 * Una interfaz para listas arreglo .
3 */
4 public interface ListaIndice <E > {
5 /* * Regresa el n ú mero de elementos en esta lista . */
6 public int size ();
7 /* * Prueba si la lista vac ı́ a . */
8 public boolean isEmpty ();
9 /* * Insertar un elemento e para estar en el ı́ ndice i ,
10 * desplazando todos los elementos despu é s de este . */
11 public void add ( int i , E e )
12 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 ;
13 /* * Regresar el elemento en el ı́ ndice i , sin quitarlo . */
14 public E get ( int i )
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 /* * Quita y regresa el elemento en el ı́ ndice i ,
17 * desplazando los elementos despu é s de este . */
18 public E remove ( int i )
19 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 ;
20 /* * Reemplaza el elemento en el ı́ ndice i con e ,
21 * regresando el elemento previo en i . */
22 public E set ( int i , E e )
23 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 ;
24 }
La clase java.util.ArrayList
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
6.2.2. Posiciones
A fin de ampliar de forma segura el conjunto de operaciones para listas,
se abstrae la noción de “posición” la cual permite usar la eficiencia de
la implementación de las listas simples enlazadas, o dobles, sin violar los
principios del diseño orientado a objetos. En este marco, se ve a la lista como
una colección que guarda a cada elemento en una posición y que mantiene
estas posiciones organizadas en un orden lineal. Una posición es por sı́ misma
un tipo de dato abstracto que soporta el siguiente método simple:
6.2 Listas nodo 209
Figura 6.4: Una lista nodo. Las posiciones en el orden actual son p, q, r, y s.
p = null
Operación Salida S
addFirst(8) – (8)
p1 ← first() [8] (8)
addAfter(p1 , 5) – (8,5)
p2 ← next(p1 ) [5] (8,5)
addBefore(p2 , 3) – (8,3,5)
p3 ← prev(p2 ) [3] (8,3,5)
addFirst(9) – (9,8,3,5)
p2 ← last() [5] (9,8,3,5)
remove(first()) 9 (8,3,5)
set(p3 , 7) 3 (8,7,5)
addAfter(first(), 2) – (8,2,7,5)
Una interfaz, con los métodos requeridos, para el ADT lista nodo, llamada
ListaPosicion, está dada en el listado 6.4. Esta interfaz usa las siguientes
excepciones para indicar condiciones de error.
Cuadro 6.4: Realización de una deque por medio de una lista nodo.
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 podrı́an 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
métodos 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 posición. Esta clase es
similar a la clase NodoD mostrada en el listado 3.11, excepto que ahora los
nodos guardan un elemento genérico 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 ó n
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 é 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 ó n no est á en una lista ! " );
14 return elemento ;
15 }
16 // M é todos accesores
17 public NodoD <E > getNext () { return next ; }
18 public NodoD <E > getPrev () { return prev ; }
19 // M é 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.
Figura 6.6: Quitando el objeto guardado en la posición para “PVD”: (a) antes de
la remoción; (b) desenlazando el nodo viejo; (c) después de la remoción.
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 á s all á del inicio de la lista " );
43 return prev ;
44 }
45 /* * Regresa la posici ó n despu é 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 á s all á del final de la lista " );
53 return next ;
54 }
55 /* * Insertar el elemento dado antes de la posici ó 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 é s de la posici ó 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 );
71 v . setNext ( nodoNuevo );
72 }
73 /* * Insertar el elemento dado al inicio de la lista , regresando
74 * la nueva posici ó n ; tiempo O (1) */
75 public void addFirst ( E elemento ) {
76 tam ++;
77 NodoD <E > nodoNuevo = new NodoD <E >( cabeza , cabeza . getNext () , elemento );
78 cabeza . getNext (). setPrev ( nodoNuevo );
79 cabeza . setNext ( nodoNuevo );
80 }
81 /* * Insertar el elemento dado al final de la lista , regresando
82 * la nueva posici ó n ; tiempo O (1) */
83 public void addLast ( E elemento ) {
84 tam ++;
85 NodoD <E > ultAnterior = cola . getPrev ();
86 NodoD <E > nodoNuevo = new NodoD <E >( ultAnterior , cola , elemento );
87 ultAnterior . setNext ( nodoNuevo );
88 cola . setPrev ( nodoNuevo );
89 }
90 /* * Quitar la posici ó n dada de la lista ; tiempo O (1) */
91 public E remove ( Posicion <E > p )
92 throws I n v a l i d P o s i t i o n E x c e p t i o n {
93 NodoD <E > v = revis aPosicio n ( p );
94 tam - -;
95 NodoD <E > vPrev = v . getPrev ();
96 NodoD <E > vNext = v . getNext ();
6.2 Listas nodo 219
154 }
155 /* * Valida si una posici ó n es la primera ; tiempo O (1). */
156 public boolean isFirst ( Posicion <E > p )
157 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 {
158 NodoD <E > v = revis aPosicio n ( p );
159 return v . getPrev () == cabeza ;
160 }
161 /* * Valida si una posici ó n es la ú ltima ; tiempo O (1). */
162 public boolean isLast ( Posicion <E > p )
163 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 {
164 NodoD <E > v = revis aPosicio n ( p );
165 return v . getNext () == cola ;
166 }
167 /* * Intercambia los elementos de dos posiciones dadas ; tiempo O (1) */
168 public void swapElements ( Posicion <E > a , Posicion <E > b )
169 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 {
170 NodoD <E > pA = rev isaPosic ion ( a );
171 NodoD <E > pB = rev isaPosic ion ( b );
172 E temp = pA . elemento ();
173 pA . setElemento ( pB . elemento ());
174 pB . setElemento ( temp );
175 }
176 /* * Regresa una representaci ó n textual de una lista nodo usando for - each */
177 public static <E > String f or Ea c hT oS tr i ng ( ListaPosicion <E > L ) {
178 String s = " [ " ;
179 int i = L . size ();
180 for ( E elem : L ) {
181 s += elem ; // moldeo impl ı́ cito del elemento a String
182 i - -;
183 if ( i > 0)
184 s += " , " ; // separar elementos con una coma
185 }
186 s += " ] " ;
187 return s ;
188 }
189 /* * Regresar una representaci ó n textual de una lista nodo dada */
190 public static <E > String toString ( ListaPosicion <E > l ) {
191 Iterator <E > it = l . iterator ();
192 String s = " [ " ;
193 while ( it . hasNext ()) {
194 s += it . next (); // moldeo impl ı́ cito del elemento a String
195 if ( it . hasNext ())
196 s += " , " ;
197 }
198 s += " ] " ;
199 return s ;
200 }
201 /* * Regresa una representaci ó n textual de la lista */
202 public String toString () {
203 return toString ( this );
204 }
205 }
6.3. Iteradores
Un cálculo tı́pico en una lista arreglo, lista, o secuencia es recorrer sus
elementos en orden, uno a la vez, por ejemplo, para buscar un elemento
especı́fico.
más vieja que la interfaz iterador y en vez de los nombres anteriores, usa
hasMoreElements() y nextElement().
Listado 6.8: Ejemplo de un iterador Java usado para convertir una lista nodo a
una cadena.
List<Integer> valores;
...sentencias que crean una nueva lista y la llenan con Integer.
int suma = 0;
for (Integer i : valores)
suma += i; // el moldeo implı́cito permite esto
Iteradores posición
Para los ADT que soporten la noción de posición, tal como las ADT lista
y secuencia, se puede también dar el siguiente método:
6.3 Iteradores 225
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 ó n p como el ú 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 }
Cuadro 6.5: Correspondencia entre los métodos de los ADT lista arreglo y lista nodo
(1a columna) contra las interfaces List y ListIterator de java.util. Se usa A y L
como abreviaturas para las clases java.util.ArrayList y java.util.LinkedList
232 Listas e Iteradores
6.4.2. Secuencias
Una secuencia es un ADT que soporta todos los métodos del ADT deque
(sección 5.12), el ADT lista arreglo (sección 6.1), y el ADT lista nodo (sección
6.2). Entonces, este proporciona acceso explı́cito 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
métodos “puente” que proporcionan conexión entre ı́ndices y posiciones:
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 ó n .
5 */
6 public interface Secuencia <E >
7 extends Deque <E > , ListaIndice <E > , ListaPosicion <E > {
8 /* * Regresar la posici ó 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 ó 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últiple por las interfaces
Deque, ListaIndice, y ListaPosicion y agregando los dos métodos puente.
Como una lista enlazada no permite acceso indizado a sus elementos, reali-
zar la operación 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 optimización, se puede iniciar saltando del extremo
más cercano de la lista, logrando ası́ el siguiente tiempo de ejecución
O(mı́n(i + 1, n − i))
r = bn/2c
O(mı́n(i + 1, n − i + 1))
o para llenar el espacio creado por la remoción de una vieja posición, como
en los métodos insertar y remover basados en ı́ndice. Todos los otros métodos
basados en la posición toman tiempo O(1).
Listado 6.14: La clase ListaFavoritos que incluye una clase anidada, Entrada, la
cual representa los elementos y su contador de accesos
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 heurı́stica, o regla de oro, que intenta tomar ventaja de la localidad
de referencia que está presente en una secuencia de acceso es la heurı́stica
mover al frente. Para aplicar esta huerı́stica, 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-ésimo 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:
...
lo cual es O(n3 ).
Por otra parte, si se emplea la heurı́stica mover al frente, insertando cada
elemento la primera vez, entonces
...
Por lo que el tiempo de ejecución para realizar todos los accesos en este
caso es O(n2 ). Ası́, la implementación mover al frente tiene tiempos de acceso
más rápidos para este escenario. Sin embargo este beneficio vien a un costo.
6.5 Ejemplo: Heurı́stica mover al frente 239
El listado 6.16 muestra una aplicación que crea dos listas de favoritos,
una usa la clase ListaFavoritos y la otra ListaFavoritosMF. Las listas de
favoritos son construidas usando un arreglo de direcciones Web, y después se
generan 20 números pseudoaleatorios que están en el rango de 0 al tamaño
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ño de la
lista. Con el sitio más popular de la 1a lista se abre en una ventana.
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 aFavorit os 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 ó 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 á s popular de L1
31 try {
32 String popular = L1 . top (1). iterator (). next (); // el m á s popular
33 JEditorPane jep = new JEditorPane ( popular );
34 jep . setEditable ( false );
35 JFrame frame = new JFrame ( " URL m á s popular en L1 : " + popular );
36 frame . getCo ntentPa ne (). 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 }
3. Se regresa la lista A.
Árboles
nes entre padres e hijos con lı́neas rectas, ver figura 7.1. Se llama al elemento
cima la raı́z del árbol, pero este es dibujado como el elemento más alto, con
los otros elementos estando conectados abajo, justo lo opuesto a un árbol
vivo.
Figura 7.1: Árbol con 17 nodos para representar la organización de una corporación.
Una arista del árbol 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
árbol de la figura 7.2 contiene el camino cs016/programs/pr2.
Ejemplo 7.2. La relación de herencia entre clases en un programa Java
forman un árbol. La raı́z, java.lang.Object, es un ancestro de todas las otras
clases. Cada clase, C, es una descendiente de esta raı́z y es la raı́z de un
subárbol de las clases que extienden a C. Ası́, hay un camino de C a la raı́z,
java.lang.Object, en este árbol de herencia.
Árboles ordenados
Estos métodos hacen la programación con árboles más fácil y más 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 también un número de métodos genéricos que un árbol probablemente
podrı́a soportar y que no están necesariamente relacionados con la estructura
del árbol, incluyendo los siguientes:
Listado 7.1: Interfaz Java Tree para representar el ADT árbol. Métodos adicionales
de actualización podrı́an ser agregados dependiendo de la aplicación.
Figura 7.4: La estructura enlazada para un árbol general: (a) el objeto posición
asociado con un nodo; (b) la porción de la estructura de dato asociada con un nodo
y sus hijos.
Operación 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)
el peor caso. Aunque tal tiempo de ejecución es una función del tamaño de
entrada, es más preciso caracterizar el tiempo de ejecución en términos del
parámetro dv , debido a que este parámetro puede ser mucho menor que n.
Altura
La altura de un nodo v en un árbol A se puede definir también recursiva-
mente:
Listado 7.3: Método height1(), que usa el método max() de la clase java.lang.-
Math
7.2 Algoritmos de recorrido para árboles 253
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 }
Figura 7.5: Recorrido en preorden de un árbol ordenado, donde los hijos de cada
nodo están ordenados de izquierda a derecha.
P (A) = v.elemento().toString().
256 Árboles
De otra forma,
P (A) = v.elemento().toString() + “(” + P (A1 ) + “,” + · · · + “,” + P (Ak ) + “)”,
donde v es la raı́z de A y A1 , A2 , . . . , Ak son los subárboles enraizados de los
hijos de v, los cuales están dados en orden si A es un árbol ordenado.
La definición de P (A) es recursiva y el operador “+” indica la concatena-
ción de cadenas. La representación parentética del árbol de la figura 7.1 se
muestra enseguida, donde el sangrado y los espacios han sido agregados para
claridad, y además se han retirado las comas.
Electronics R’Us (
R&D
Sales (
Domestic
International (
Canada
S. America
Overseas (
Africa
Europe
Asia
Australia
)
)
)
Purchasing
Manufacturing (
TV
CD
Tuner
)
)
El método Java representacionParentetica, listado 7.6, es una varia-
ción del método toStringPreorden, listado 7.5. Está implementado por la
definición anterior para obtener una representaión parentética de cadena de
un árbol A. El método representacionParentetica hace uso del método
toString que está definido para cada objeto Java. Se puede ver a este método
como un tipo de método toString() para objetos árbol.
7.2 Algoritmos de recorrido para árboles 257
Figura 7.7: El árbol de la figura 7.2 del sistema de archivos con nombre, tamaño
asociado para cada archivo o directorio dentro de cada nodo, y el espacio usado
por cada 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 }
nodo tiene cero o dos hijos. Algunos autores también se refieren a tales árboles
como árboles binarios árbol binario completo. Ası́, en un árbol binario
propio, cada nodo interno tiene exactamente dos hijos. Un árbol binario que
no es propio es árbol impropio.
Ejemplo 7.6. Una clase de árboles binarios se emplean en los contextos donde
se desea representar un número de diferentes salidas que pueden resultar de
contestar una serie de preguntas si o no. Cada nodo interno está asociado con
una pregunta. Iniciando en la raı́z, 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 decisión, se sigue una arista desde un padre a un hijo, eventualmente
trazando un camino en el árbol desde la raı́z a un nodo externo. Tales árboles
binarios son conocidos como árboles de decisión, porque cada nodo externo
V en tal árbol representa una decisión de que hacer si la pregunta asociada con
los ancestros de v fueron contestadas en una forma que llevan a v. Un árbol de
decisión es un árbol binario propio. La figura 7.8 muestra un árbol de decisión
para mostrar en orden ascendente tres valores guardados en las variables A,
B y C, donde los nodos internos del árbol representan comparaciones y los
nodos externos representan la secuencia ordenada en forma creciente.
Figura 7.8: Un árbol de decisión para ordenar tres variables en orden creciente
Ejemplo 7.7. Una expresión aritmética puede ser representada con un árbol
binario cuyos nodos externos están asociados con variables o constantes, y
cuyos nodos internos están asociados con los operadores +, −, × y /. Ver
figura 7.9. Cada nodo en tal árbol tiene un valor asociado con este.
constante.
Si un nodo es interno, entonces su valor está definido aplicando la
operación a los valores de sus hijos.
1 /* *
2 * Una interfaz para un á 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 árbol binario que extiende a
Tree (listado 7.1).
264 Árboles
Se observa que el número máximo de nodos en los niveles del árbol binario
crece exponencialmente conforme se baja en el árbol. De esta observación, se
pueden derivar las siguientes propiedades de relación entre la altura de un
árbol binario A con el número de nodos.
Proposición 7.2: Sea A un árbol binario no vacı́o, y sean n, nE , nI y h el
número de nodos, número 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
7.3 Árboles Binarios 265
2. 1 ≤ nE ≤ 2h
3. h ≤ nI ≤ 2h − 1
4. log(n + 1) − 1 ≤ h ≤ n − 1
1. 2h + 1 ≤ n ≤ 2h+1 − 1
2. h + 1 ≤ nE ≤ 2h
3. h ≤ nI ≤ 2h − 1
4. log(n + 1) − 1 ≤ h ≤ (n − 1)/2
nE = nI + 1.
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 vacı́o. Las pilas están inicialmente vacı́as. Al final, la
pila de nodos externos tendrá un nodo más que la pila de nodos internos,
considerar dos casos:
Figura 7.11: Operación para quitar un nodo externo y su nodo padre, usado en la
demostración
Listado 7.10: Interfaz Java PosicionAB para definir los métodos que permiten
acceder a los elementos de un nodo del árbol binario.
7.3 Árboles Binarios 267
Figura 7.12: Representación de un árbol binario con estructura enlazada. (a) Diseño
del nodo; (b) Ejemplo.
268 Árboles
Listado 7.11: Clase auxiliar NodoAB para implementar los nodos del árbol binario.
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 . */
71 public Posicion <E > parent ( Posicion <E > v )
72 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 {
73 PosicionAB <E > vv = revisaP osicion ( v );
74 Posicion <E > posPadre = vv . getPadre ();
75 if ( posPadre == null )
76 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 padre " );
77 return posPadre ;
78 }
79 /* * Regresa una colecci ó n iterable de los hijos de un nodo . */
80 public Iterable < Posicion <E > > children ( Posicion <E > v )
81 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 {
82 ListaPosicion < Posicion <E > > children = new ListaNodoPosicion < Posicion <E > >();
83 if ( hasLeft ( v ))
84 children . addLast ( left ( v ));
85 if ( hasRight ( v ))
86 children . addLast ( right ( v ));
87 return children ;
88 }
89 /* * Regresa una colecci ó n iterable de los nodos de un á rbol . */
90 public Iterable < Posicion <E > > positions () {
91 ListaPosicion < Posicion <E > > posiciones = new ListaNodoPosicion < Posicion <E > >();
92 if ( tam != 0)
93 p r e o r d e n P o s i c i o n e s ( root () , posiciones ); // asignar posiciones en preorden
94 return posiciones ;
95 }
96 /* * Regresar un iterado de los elementos guardados en los nodos . */
97 public Iterator <E > iterator () {
98 Iterable < Posicion <E > > posiciones = positions ();
99 ListaPosicion <E > elementos = new ListaNodoPosicion <E >();
100 for ( Posicion <E > pos : posiciones )
101 elementos . addLast ( pos . elemento ());
102 return elementos . iterator (); // Un iterador de elementos
103 }
104 /* * Reemplaza el elemento en un nodo . */
105 public E replace ( Posicion <E > v , E o )
106 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 {
107 PosicionAB <E > vv = revisaP osicion ( v );
108 E temp = v . elemento ();
109 vv . setElemento ( o );
110 return temp ;
111 }
112 // M é todos adicionales accesores
113 /* * Regresa el hermano de un nodo . */
272 Árboles
228 }
229 /* * Quitar un nodo externo v y reemplazar su padre con el hermano de v */
230 public void r e m o v e A b o v e E x t e r n a l ( Posicion <E > v )
231 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 {
232 if (! isExternal ( v ))
233 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 ( " El nodo no es externo " );
234 if ( isRoot ( v ))
235 remove ( v );
236 else {
237 Posicion <E > u = parent ( v );
238 remove ( v );
239 remove ( u );
240 }
241 }
242 // M é todos auxiliares
243 /* * Si v es un nodo de un á rbol binario , convertir a PosicionAB ,
244 * si no lanzar una excepci ó n */
245 protected PosicionAB <E > re visaPosi cion ( Posicion <E > v )
246 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 {
247 if ( v == null || !( v instanceof PosicionAB ))
248 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 ó n no es v á lida " );
249 return ( PosicionAB <E >) v ;
250 }
251 /* * Crear un nuevo nodo de un á rbol binario */
252 protected PosicionAB <E > creaNodo ( E elemento , PosicionAB <E > parent ,
253 PosicionAB <E > left , PosicionAB <E > right ) {
254 return new NodoAB <E >( elemento , parent , left , right ); }
255 /* * Crear una lista que guarda los nodos en el sub á rbol de un nodo ,
256 * ordenada de acuerdo al recorrido en preordel del sub á rbol . */
257 protected void p r e o r d en P o s i c i o n e s ( Posicion <E > v , ListaPosicion < Posicion <E > > pos )
258 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 {
259 pos . addLast ( v );
260 if ( hasLeft ( v ))
261 p r e o r d e n P o s i c i o n e s ( left ( v ) , pos ); // recursividad en el hijo izquierdo
262 if ( hasRight ( v ))
263 p r e o r d e n P o s i c i o n e s ( right ( v ) , pos ); // recursividad en el hijo derecho
264 }
265 /* * Crear una lista que guarda los nodos del sub á rbol de un nodo ,
266 ordenados de acuerdo al recorrido en orden del sub á rbol . */
267 protected void p o s i c i o n e s E n o r d e n ( Posicion <E > v , ListaPosicion < Posicion <E > > pos )
268 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 {
269 if ( hasLeft ( v ))
270 p o s i c i o n e s E n o r d e n ( left ( v ) , pos ); // recursividad en el hijo izquierdo
271 pos . addLast ( v );
272 if ( hasRight ( v ))
273 p o s i c i o n e s E n o r d e n ( right ( v ) , pos ); // recursividad en el hijo derecho
274 }
275 }
Rendimiento de ArbolBinarioEnlazado
Los tiempos de ejecución de los métodos de la clase ArbolBinarioEnlazado,
el cual usa una representación de estructura enlazada, se muestran enseguida:
Figura 7.13: Numeración por nivel de un árbol binario: (a) esquema general; (b)
un ejemplo.
7.3 Árboles Binarios 277
Figura 7.14: Representación de un árbol binario A por medio de una lista arreglo
S.
si no { ei =0 )0 }
A2 ← P.pop() { el árbol que representa a E2 }
A ← P.pop() { el árbol que representa a ◦ }
A1 ← P.pop() { el árbol que representa a E1 }
A.attach(A.root(), A1 , A2 )
P.push(A)
regresar P.pop()
Como cualquier árbol binario puede ser visto como un árbol general, el
recorrido en preorden para árboles generales, sección 7.2.2, puede ser aplicado
a cualquier árbol binario y simplificado como se da a continuación.
Algoritmo preordenBinario(A, v):
Realizar la acción “visita” para el nodo v
si v tiene un hijo izquierdo u en A entonces
preordenBinario(A, u) { recursivamente recorrer el subárbol izquierdo }
si v tiene un hijo derecho w en A entonces
preordenBinario(A, w) { recursivamente recorrer el subárbol derecho }
Como en el caso de los árboles generales, hay varias aplicaciones de
recorrido en preorden para árboles binarios.
Figura 7.16: Un árbol binario de búsqueda ordenando enteros. El camino azul sólido
es recorrido cuando se busca exitosamente el 36. El camino azul discontinuo es
recorrido cuando se busca sin éxito el 70.
y(v) es la profundidad de v en A.
Regresar r.salida.
Implementación Java
La clase RecorridoEuler, mostrada en el listado 7.13, implementa un
recorrido transversal de Euler usando la plantilla método patrón. El recorrido
transversal es hecha por el método RecorridoEuler(). Los métodos auxiliares
llamados por RecorridoEuler son lugares vacı́os, tienen cuerpos vacı́os o
solo regresan null. La clase RecorridoEuler es abstracta y por lo tanto no
puede ser instanciada. Contiene un método abstracto, llamado ejecutar, el
cual necesita ser especificado en las subclases concretas de RecorridoEuler.
1 /* *
2 * Plantilla para algoritmos que recorren un á rbol binario usando un
3 * recorrido euleriano . Las subclases de esta clase redefinir á n
4 * algunos de los m é 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 ó n del recorrido . Este m é todo abstracto deber á ser
10 * especificado en las subclases concretas . */
11 public abstract R ejecutar ( ArbolBinario <E > T );
12 /* * Inicializaci ó n del recorrido */
13 protected void inicializar ( ArbolBinario <E > T ) { arbol = T ; }
14 /* * M é 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 é todos auxiliares que pueden ser redefinidos por las subclases
27 /* * M é 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 é todo llamado para visitar abajo */
30 protected void visitarAbajo ( Posicion <E > v , ResultadoRecorrido <R > r ) {}
31 /* * M é todo llamado para visitar a la derecha */
32 protected void vi sitarDe recha ( Posicion <E > v , ResultadoRecorrido <R > r ) {}
288 Árboles
33
34 /* Clase interna para modelar el resultado del recorrido */
35 public class ResultadoRecorrido <R > {
36 public R izq ;
37 public R der ;
38 public R salida ;
39 }
40 }
[1] R. Lafore, Data Structures & Algorithms, Second Edition, Sams, 2003.