ISBN: 978-607-02-2703-5
9 786070 227035
AMPARO LÓPEZ GAONA
ESTRUCTURAS
DE DATOS CON
JAVA:
UN ENFOQUE PRÁCTICO
FACULTAD DE CIENCIAS
21
01
López Gaona, Amparo
Estructuras de datos con Java : un enfoque práctico / Amparo López Gaona.
-- México : UNAM, Facultad de Ciencias, 2011.
xi, 222 p. ; 23.5 cm. -- (Las prensas de ciencias) (Temas de computación)
Incluye índice
Bibliografía: p. 219
ISBN 978-607-02-2703-5
1. Estructuras de datos (Ciencia de la computación). 2. Algoritmos de
computadora. 3. Java (Lenguaje de programación). I. Universidad Nacional
Autónoma de México. Facultad de Ciencias. II. t. III.
Ser. IV. Ser.
005.73-scdd21
Biblioteca Nacional de México
Estructuras de datos con Java:
un enfoque práctico
1º edición, 20 de octubre de 2011
1a reimpresión, 2017
© D.R. 2011. Universidad Nacional Autónoma de México.
Facultad de Ciencias.
Ciudad Universitaria. Delegación Coyoacán,
C. P. 04510, México, Distrito Federal.
editoriales@[Link]
ISBN: 978-607-02-2703-5
Diseño de portada: Laura Uribe Hernández
Composición tipográfica: Salvador López Mendoza
Prohibida la reproducción parcial o total de la obra por cualquier medio, sin
la autorización por escrito del titular de los derechos patrimoniales.
Impreso y hecho en México.
A Andrea y Salvador
Agradecimientos
Agradezco a todas las personas que colaboraron, de una o de otra forma, en
la materializa-ción de este proyecto. Especialmente a Salvador por el realizar
el trabajo relacionado con la tipograf´ıa del libro y por su invaluable ayuda
durante la etapa final del desarrollo del mismo en que me v´ı forzada a ser
zurda y él se convirtió en mucho más que “mi mano derecha”.
También agradezco a Andrea por su ayuda, apoyo, compa˜
n´ıa y ser mi chofer durante mi
zurdera, además de animarme durante mi rehabilitación.
La imagen que aparece en esta página es una “colección de objetos”
realizada por Andrea en cuarto a˜
no de primaria.
Indice general
Introducci´
on
ix
Tipos abstractos de datos
1.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2. El TAD Conjunto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3. Ejemplo de uso del TAD Conjunto
.......................
1.4. Implementación de conjuntos
..........................
5
1.5. Iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.5.1. Ejemplos de uso de iteradores . . . . . . . . . . . . . . . . . . . . . .
13
1.6. Análisis de algoritmos
..............................
15
1.7. Mejora al TAD Conjunto . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
1.8. Ejercicios
.....................................
26
Listas
33
2.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
2.2. El TAD Lista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
34
2.3. Aplicaciones de listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
2.3.1. Lista de identificadores . . . . . . . . . . . . . . . . . . . . . . . . . .
36
2.3.2. Orden en recipientes . . . . . . . . . . . . . . . . . . . . . . . . . . .
39
2.3.3. Problema de José . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
2.4. Implementación de listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
43
2.4.1. Complejidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
50
2.4.2. Listas ligadas con una referencia al nodo final . . . . . . . . . . . . .
51
2.4.3. Complejidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
54
2.5. Ejercicios
.....................................
55
3
Pilas
59
3.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
59
3.2. El TAD Pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
60
3.3. Aplicaciones de pilas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
61
ii
INDICE GENERAL
3.3.1. Torres de Hanoi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
61
3.3.2. Análisis y evaluación de expresiones . . . . . . . . . . . . . . . . . . .
64
3.3.3. Salida de laberintos . . . . . . . . . . . . . . . . . . . . . . . . . . . .
71
3.4. Implementación del TAD Pila . . . . . . . . . . . . . . . . . . . . . . . . . .
76
3.5. Ejercicios
.....................................
80
Colas
83
4.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
83
4.2. El TAD Cola
...................................
84
4.3. Aplicaciones de colas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
85
4.3.1. Uso en sistemas operativos . . . . . . . . . . . . . . . . . . . . . . . .
85
4.3.2. Acomodo de trenes . . . . . . . . . . . . . . . . . . . . . . . . . . . .
88
4.3.3. Salida rápida de laberintos . . . . . . . . . . . . . . . . . . . . . . . .
92
4.4. Implementación del TAD Cola . . . . . . . . . . . . . . . . . . . . . . . . . .
97
4.5. Ejercicios
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Arboles
105
5.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
5.2. Árboles binarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
5.3. Implementación de árboles binarios . . . . . . . . . . . . . . . . . . . . . . . 108
5.3.1. Recorridos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
5.4. Aplicaciones de árboles binarios . . . . . . . . . . . . . . . . . . . . . . . . . 112
5.4.1.
Árbol para expresiones aritméticas . . . . . . . . . . . . . . . . . . . 112
5.4.2. Juego de adivinar animales . . . . . . . . . . . . . . . . . . . . . . . . 115
5.5. Árboles binarios de búsqueda . . . . . . . . . . . . . . . . . . . . . . . . . . 119
5.5.1. Implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
5.5.2. Aplicación de árboles binarios de búsqueda . . . . . . . . . . . . . . . 127
5.6. Árboles balanceados
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
5.7. Ejercicios
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
Colas de prioridad
143
6.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
6.2. El TAD Cola de prioridad . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
6.3. Aplicaciones de colas de prioridad . . . . . . . . . . . . . . . . . . . . . . . . 144
6.3.1. Ordenamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
6.3.2. Planeación de trabajos . . . . . . . . . . . . . . . . . . . . . . . . . . 145
6.3.3. Simulación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
6.4. Implementación del TAD Cola de prioridad . . . . . . . . . . . . . . . . . . . 157
6.5. Ejercicios
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
INDICE GENERAL
iii
Tablas de dispersi´
on
169
7.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
7.2. El TAD Tabla de dispersión . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
7.3. Funciones de dispersión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
7.4. Manejo de colisiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
7.5. Aplicaciones de tablas de dispersión . . . . . . . . . . . . . . . . . . . . . . . 177
7.5.1. Radix sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
7.5.2. Cálculo de peso molecular . . . . . . . . . . . . . . . . . . . . . . . . 179
7.5.3. Información de puntos . . . . . . . . . . . . . . . . . . . . . . . . . . 183
7.6. Implementación de tablas de dispersión . . . . . . . . . . . . . . . . . . . . . 186
7.6.1. Utilizando cubetas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
7.6.2. Direccionamiento abierto . . . . . . . . . . . . . . . . . . . . . . . . . 192
7.7. Ejercicios
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
A Recursi´
on
199
A.1. Búsqueda con retroceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
B Algoritmos de ordenamiento
207
B.1. Burbuja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
B.2. Inserción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
B.3. Quick sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
C Normas de estilo en Java
211
D El programa javadoc
215
E La clase StreamTokenizer
217
Bibliograf´ıa
219
Indice alfab´
etico
221
Indice de figuras
2.1. Lista ligada con cinco nodos. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
44
2.2. Lista vac´ıa. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
2.3. Inserción del elemento e1 en una lista. . . . . . . . . . . . . . . . . . . . . . . .
48
2.4. Supresión de un nodo de una lista. . . . . . . . . . . . . . . . . . . . . . . . . .
49
2.5. Lista ligada con una referencia al inicio y otra al final. . . . . . . . . . . . . . .
.
51
2.6. Conversión de base 10 a base 2.
..........................
58
3.1. Pila con entrada y salida por arriba. . . . . . . . . . . . . . . . . . . . . . . . .
59
3.2. Torres de Hanoi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
61
3.3. Evaluación de la expresión (3+4)*9. . . . . . . . . . . . . . . . . . . . . . . . .
69
3.4. Evaluación de la expresión 3+(4*9). . . . . . . . . . . . . . . . . . . . . . . . .
70
3.5. Laberinto y su camino de salida. . . . . . . . . . . . . . . . . . . . . . . . . . .
71
3.6. Código para las celdas del laberinto. . . . . . . . . . . . . . . . . . . . . . . . .
73
3.7. Laberinto y su representación en decimal. . . . . . . . . . . . . . . . . . . . . .
73
4.1. Cola de datos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
83
4.2. Cola de procesos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
85
4.3. Planificación round-robin.
.............................
86
4.4. Marcado de celdas en un laberinto. . . . . . . . . . . . . . . . . . . . . . . . . .
93
4.5. Cola circular. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
5.1. Ejemplo de un árbol. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
106
5.2. Árbol sintáctico. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
107
5.3. Árbol binario para la expresión ((A-B)+(X/2))/(B*(C+D)).
. . . . . . . . . . . 107
5.4. Conversión de árbol no binario a binario.
. . . . . . . . . . . . . . . . . . . . . 108
5.5. Árbol binario creado a partir de un árbol no binario.
. . . . . . . . . . . . . . . 109
5.6. Árbol de animales. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
115
5.7. Inserción de un nuevo animal en el árbol.
. . . . . . . . . . . . . . . . . . . . . 117
5.8. Árbol de búsqueda. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
120
5.9. Supresión de un nodo en un árbol binario de búsqueda. . . . . . . . . . . . . . .
125
vi
INDICE DE FIGURAS
5.10. Árbol no balanceado. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
133
5.11. Árbol no balanceado con los números del 1, 2, 3, 5 y 6. . . . . . . . . . . . . .
.
134
5.12. Inserción en un árbol balanceado.
. . . . . . . . . . . . . . . . . . . . . . . . . 134
5.13. Inserción del 5 y del 6 en el árbol balanceado. . . . . . . . . . . . . . . . . . . .
134
5.14. Doble rotación para dejar un árbol balanceado. . . . . . . . . . . . . . . . . . .
135
5.15. Árbol de letras.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
5.16. Poda de un árbol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
140
6.1. Planeación para tres máquinas.
. . . . . . . . . . . . . . . . . . . . . . . . . . 146
6.2. Planeación para tres máquinas usando una cola de prioridad. . . . . . . . . .
..
146
6.3. Ejemplos de árboles binarios incompletos. . . . . . . . . . . . . . . . . . . . . .
158
6.4. Ejemplo de árbol binario completo.
. . . . . . . . . . . . . . . . . . . . . . . . 158
6.5. Representación de un árbol completo en un arreglo. . . . . . . . . . . . . . . . .
159
6.6. Ejemplo de dos heaps con los mismos datos. . . . . . . . . . . . . . . . . . . . .
159
6.7. Inserción de un elemento en un heap.
. . . . . . . . . . . . . . . . . . . . . . . 163
6.8. Supresión del elemento menor en un heap. . . . . . . . . . . . . . . . . . . . . .
164
7.1. Mapeo de llaves a ´ındices de una tabla. . . . . . . . . . . . . . . . . . . . . . . 171
Indice de tablas
1.1. Tiempo de ejecución de los métodos de la clase Conjunto. . . . . . . . . . . .
..
20
1.2. Tiempo de ejecución de los métodos de la segunda versión de la clase
Conjunto. .
25
2.1. Tiempo de ejecución de los métodos de la clase Lista.
..............
50
2.2. Tiempo de ejecución de los métodos de la clase Lista con doble
referencia. . . . .
55
3.1. Tiempo de ejecución de los métodos de la clase Pila. . . . . . . . . . . . . . . .
79
4.1. Tiempo de ejecución de los métodos de la clase Cola. . . . . . . . . . . . . . . .
100
5.1. Tiempo de ejecución de los métodos de la clase ArbolBinarioBusqueda.
. . . . . 127
5.2. Complejidad de los métodos de la clase ArbolBinarioBusqueda si están
balanceados.137
6.1. Tiempo de ejecución de los métodos de la clase Heap. . . . . . . . . . . . . . . .
166
7.1. Datos registrados de 8 personas. . . . . . . . . . . . . . . . . . . . . . . . . . . 170
7.2. Datos registrados de 8 personas con el resultado de una función.
. . . . . . . . . 170
7.3. Resultado de la función de dispersión sobre cadenas. . . . . . . . . . . . . . . . .
171
7.4. Tabla de dispersión.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
7.5. Resultado del radix sort, paso a paso. . . . . . . . . . . . . . . . . . . . . . . . 178
7.6. Tiempo de ejecución de los métodos de la clase TablaDeDispersion. . . .
. . . . 191
7.7. Tiempo de ejecución de los métodos de la clase TablaDeDispersionA.
. . . . . . 195
vii
Introducci´
on
Los programadores frecuentemente se encuentran con la necesidad de
escribir programas que manipulan una colección de datos del mismo tipo.
Con los conocimientos básicos de programación inmediatamente se piensa
que la solución para trabajar con estos datos es utilizar un arreglo, con las
ventajas y desventajas de éstos. Si bien es cierto que as´ı podr´ıa ser, no
siempre es conveniente utilizar arreglos debido a que en general el acceso a
los datos no es por posición, as´ı que se debe buscar otra organización
adecuada para los datos en términos de facilidad de manejo por parte del
programador y en términos de rapidez de ejecución de las tareas relativas a
dicho manejo.
Las estructuras de datos tienen como objetivo facilitar la organización de
grandes cantidades de datos requeridos en memoria para cualquier
aplicación, con el propósito de que su manipulación sea eficiente. Por
eficiencia se entiende la habilidad de encontrar y manipular los datos con el
m´ınimo de recursos, como tiempo de proceso y espacio en memoria. No es
lo mismo hacer un programa para manipular decenas de datos que para miles
de ellos.
Conocer, y sobre todo utilizar las estructuras de datos, es esencial para
escribir programas que utilicen eficientemente los recursos de la
computadora. Existen diversos tipos de estructuras de datos; las hay desde
las muy generales y ampliamente utilizadas hasta otras muy especializadas
para problemas particulares. La selección de la estructura de datos apropiada
permite utilizar la más eficiente para el problema espec´ıfico que se desea
resolver, con lo cual se optimiza el rendimiento del programa.
En este libro se consideran las estructuras de datos básicas: listas, pilas,
colas, árboles, colas de prioridad y tablas de dispersión. A pesar de ser tan
importantes no son dif´ıciles de comprender ni de implementar. Sin
embargo, no es suficiente saber cómo se implementa cierta estructura de
datos particular, también es necesario ser capaz de determinar, dado un
problema, cuál es la más apropiada. Se asume que el lector tiene
conocimientos básicos de programación orientada a objetos utilizando el
lenguaje de programación Java, en caso de no tenerlos se recomienda el libro
Introducci´
on al desarrollo de programas con JAVA (Amparo López Gaona, UNAM,
2011).
Aunque la mayor´ıa de las estructuras de datos presentadas se encuentran
como clases de biblioteca de Java es conveniente conocer cómo están
implementadas para presentar algunos otros temas relacionados con ellas,
como son el manejo de referencias y análisis de algoritmos.
Cabe aclarar que la interfaz de las estructuras presentadas en este texto no
coinciden total-ix
CAP´
ITULO 0. INTRODUCCI ´
ON
mente con las proporcionadas por Java.
En este texto cada estructura de datos se presenta como un tipo abstracto de
datos con el objeto de ilustrar la independencia entre interfaz e
implementaciones, y as´ı poder utilizarlas en diversas aplicaciones sin
importar la implementación de la misma, sólo conociendo la forma de
utilizarla. En cada cap´ıtulo se presenta una estructura de datos como un tipo
abstracto de datos (TAD); para cada uno se plantean problemas cuya
solución requiere este TAD. Finalmente se presenta una implementación del
TAD analizando para cada método la complejidad del mismo.
El orden en que se presentan los diferentes estructuras de datos es el
utilizado en los cursos de estructuras de datos. Sin embargo, si el lector está
interesado en una estructura particular puede estudiarla sin necesidad del
material anterior, excepto quizá los dos primeros cap´ıtulos, en los que se
presentan las bases de los siguientes.
Las estructuras de datos que se presentan en este documento y los cap´ıtulos
en los que se desarrolla se enumeran a continuación:
Cap´ıtulo 1. Tipos abstractos de datos. En este cap´ıtulo se introduce el
concepto de tipo abstracto de dato mediante una implementación del tipo de
datos Conjunto.
También se presentan los conceptos básicos del análisis de algoritmos; para
ello se va afinando la implementación del tipo de datos.
Cap´ıtulo 2. Listas. Las listas son estructuras de datos sencillas que no
imponen restricciones al contenido ni a la forma de acceder a los elementos
que contienen, es decir pueden contener datos de cualquier tipo, con
repetición o no, y no necesariamente ordenados.
Cap´ıtulo 3. Pilas. Una pila es una estructura de datos cuya única restricción
está en la forma de acceder a los elementos almacenados en ella, ya que
tanto la entrada como la salida de los datos es por un sólo lugar. Aunque
parece ser una restricción muy fuerte, esta estructura es muy sencilla de
implementar y sumamente útil en aplicaciones en las que se requiere que los
datos se recuperen en orden inverso a como se almacenan.
Cap´ıtulo 4. Colas. Una cola es una estructura de datos que almacena una
colección de elementos sin restricción en cuanto al valor de los mismos, con
o sin repetición. Sin embargo, el acceso a los datos está restringido al
permitir que se inserten siempre por la parte trasera de la cola y se eliminen
por el frente de la misma. Con ello se consigue que los datos salgan en el
mismo orden en que entran.
Cap´ıtulo 5. ´
Arboles. Un árbol es una estructura de datos que impone un orden
jerárquico a los datos. Existen diferentes tipos de árboles en este cap´ıtulo se
presentan los árboles en general, los árboles binarios, y dentro de éstos, los
árboles de búsqueda y los balanceados. Algunas de las aplicaciones
presentadas en este cap´ıtulo no sólo requieren de árboles, sino de otras
estructuras de datos de las presentadas en cap´ıtulos anteriores.
xi
Cap´ıtulo 6. Colas de prioridad. Una cola de prioridad es una estructura de
datos en la cual es muy eficiente la localización del elemento con valor
menor (mayor) para su proceso, en caso de empate la selección del elemento
por atender se hace de acuerdo al orden de llegada. En esta estructura de
datos no se requiere que todos los elementos estén ordenados. La
implementación de este TAD es de las más elegantes debido a su sencillez.
Cap´ıtulo 7. Tablas de dispersi´
on. En una tabla de dispersión o de hash, el tiempo de acceso a los
elementos es constante, sin embargo, no existe ningún tipo de orden en los
elementos ah´ı almacenados. En este cap´ıtulo se describe cómo crearlas y
los principales problemas con ellas. Las aplicaciones que se presentan
permiten apreciar la importancia de esta estructura para gran cantidad de
situaciones.
Se incluyen los siguientes apéndices:
Ap´
endice A. Recursi´
on. En este apéndice se presenta brevemente el tema de recursión y búsquda
con retroseso o backtracking. Incluye un programa para realizar búsqueda
binaria y un programa para colocar reinas en un tablero de ajedrez sin que se
agredan.
Ap´
endice B. Ordenamiento. Se presentan algunos métodos de ordenamiento,
como el de selección, burbuja y el quicksort.
Ap´
endice C. Normas de estilo de Java. Este apéndice contiene las principales
gu´ıas de estilo para organizar y dar formato al código fuente de los
programas en Java.
Ap´
endice D. El programa javadoc. Se describen las marcas permitidas en los
comentarios para javadoc y utilizadas en este libro, además del resultado de
este programa al tomar como entrada uno de los programas desarrollados en
el libro.
Ap´
endice E. La clase StreamTokenizer. Se presentan los métodos de esta clase
utilizados en el libro.
Los programas de ejemplo desarrollados a lo largo del libro se encuentran en
la dirección:
DPNQVUBDJPOGDJFODJBTVOBNNY_BMHFTUSVDUVSBT Los
identificadores que se utilizan en los programas no tienen acentos para que
los programas sean portables a cualquier plataforma y puedan correr sin
ningún tipo de configuración especial. En el libro, los comentarios en los
programas s´ı tienen acentos y e˜
nes, aunque en el sitio electrónico no tienen acentos, esto con el fin de
imprimirlos sin ningún problema, si se desea hacerlo.
Cap´ıtulo 1
Tipos abstractos de datos
En este cap´ıtulo se presenta el concepto de tipo abstracto de datos y se
ilustra mediante la implementación de un tipo de datos para trabajar con
conjuntos. Tal implementación se irá mejorando a lo largo del cap´ıtulo.
También se presenta el concepto de análisis de algoritmos y se ejemplifica
mediante el análisis de la complejidad de los métodos que forman la
implementación del tipo abstracto de datos Conjunto.
1.1.
Introducci´
on
Los lenguajes de programación proporcionan tipos de datos llamados
primitivos, los cuales pueden ser utilizados por los programadores para
definir las variables requeridas en la solución de su problema. Cada tipo de
dato es una abstracción que determina el conjunto de valores que deben tener
los datos, as´ı como las operaciones que pueden realizarse con ellos y las
restricciones o propiedades de las operaciones.
Los tipos de datos primitivos proporcionados en Java son boolean, byte,
short, int, long, float, double y char. A manera de ejemplo se tiene que el tipo
de datos boolean sólo tiene dos posibles valores: true y false, y define para él
las operaciones de conjunción, disyunción y negación. Una caracter´ıstica o
restricción de estas operaciones es que el resultado es también un valor
booleano. Java es un lenguaje en el cual todo dato debe tener un tipo
asociado antes de poder usarse; este aspecto tiene la ventaja de que es
posible detectar alguna inconsistencia entre el tipo de los operandos
utilizados en las operaciones para cada tipo de datos.
Aunque los datos de tipo primitivo son ampliamente utilizados es frecuente
que en las aplicaciones se requiera trabajar con datos de tipos particulares.
Por ejemplo, se podr´ıa estar desarrollando un programa que manipula
figuras geométricas y por lo tanto se requiera constantemente trabajar con
puntos en el plano cartesiano, entonces resulta más cómodo para el
programador, y más legible, trabajar con datos del tipo Punto y no con
números.
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
El concepto de tipo abstracto de datos (TAD) generaliza la noción de tipos
de datos proporcionados por el lenguaje de programación al permitir que el
programador cree sus propios tipos de datos, especificando los componentes
del dato y las operaciones sobre datos de ese nuevo tipo. Se denominan TAD
porque nada en la definición indica cómo se implementan las operaciones,
por tanto, puede haber más de una implementación para cada TAD.
Ejemplos de TAD pueden ser el tipo Punto, Racional, Alumno, Fecha.
Una estructura de datos es una forma de organizar conjuntos de datos,
generalmente del mismo tipo, con el objetivo de facilitar la manipulación de
tales datos en forma individual o colectiva. En este documento se presentan
las estructuras de datos como tipos abstractos de datos debido a que se puede
definir cada uno v´ıa una interfaz e implementarse en una clase
independiente. El usuario sólo sabe qué operaciones puede realizar con cada
TAD y cómo usarlas, sin importar la implementación de las mismas.
1.2.
El TAD Conjunto
Un conjunto es una colección de elementos no repetidos y no
necesariamente ordenados. En esta sección se presenta el desarrollo de una
primera versión de un tipo de datos llamado Conjunto. Las operaciones
básicas sobre conjuntos son: almacenar un dato en el conjunto, eliminar un
dato del conjunto, verificar si un dato particular es parte del conjunto,
determinar si el conjunto está vac´ıo, as´ı como su tama˜
no, además de las operaciones clásicas de
intersección, unión y diferencia.
En Java se puede tener una separación entre definición e implementación de
un TAD
mediante el uso de una interfaz y una clase. En la interfaz se definen las
operaciones que se pueden realizar con los datos del tipo y la descripción de
las propiedades de tales operaciones.
En una clase aparte de la interfaz se programa la estructura de los datos del
TAD y las operaciones definidas en la interfaz.
Para el TAD Conjunto se puede tener la siguiente interfaz: public interface
Conjuntable {
public void agregar(Object elemento);
public void eliminar(Object elemento);
public boolean contiene (Object elemento);
public boolean estaVacio();
public int tamanio();
public void vaciar();
public Conjuntable union(Conjuntable c1);
public Conjuntable interseccion(Conjuntable c1);
public Conjuntable diferencia(Conjuntable c1);
public boolean subconjunto(Conjuntable c1);
Las siguientes son las propiedades que se esperan de los métodos de un
conjunto:
1.2. EL TAD CONJUNTO
• agregar. Si el elemento no existe en el conjunto, lo agrega y el tama˜no del
conjunto crece en una unidad.
• eliminar. Si el elemento está en el conjunto lo elimina y se reduce el
tama˜no del conjunto en una unidad.
• contiene. Regresa true si el elemento está en el conjunto y false en otro
caso. Este método no cambia el estado del conjunto.
• estaVacio. Devuelve true si el conjunto está vac´ıo y false en otro caso. No
cambia el estado del conjunto.
• tamanio. Devuelve un entero mayor o igual a cero que indica la cantidad de
elementos en el conjunto.
• vaciar. Elimina todos los elementos del conjunto. El tama˜no del conjunto
después de ejecutar este método es cero.
• union. Devuelve el conjunto formado por los elementos contenidos en el
conjunto que llama al método más los elementos del conjunto tomado como
parámetro, desde luego sin duplicar elementos. El tama˜
no máximo del conjunto resultante es la suma de los
tama˜
nos de ambos conjuntos. El tama˜
no m´ınimo se tiene cuando uno de los conjuntos
está contenido en otro y este tama˜
no es el del conjunto contenedor.
• interseccion. Devuelve el conjunto formado por los elementos que están en
el conjunto que llama al método y que también están en el conjunto tomado
como parámetro.
El tama˜
no máximo que puede tener el conjunto resultado es el tama˜
no del menor de
los conjuntos tomados como parámetros. Si ambos conjuntos no contienen
elementos en común devuelve el conjunto vac´ıo, por lo tanto el tama˜
no m´ınimo del conjunto
resultante es cero.
• diferencia. Devuelve el conjunto formado por los elementos que están en el
primer conjunto y no están en el segundo. En caso que ambos conjuntos
tengan exactamente los mismos elementos, el resultado es el conjunto vac
´ıo. El tama˜
no máximo del conjunto
resultante es el tama˜
no del primer conjunto.
• subconjunto. Devuelve true si todos los elementos del conjunto tomado
como parámetro están en el conjunto con el que se llama al método.
Devuelve false en otro caso.
Como se sabe, la interfaz es independiente de la implementación, as´ı que
podr´ıan tenerse diferentes implementaciones de ella y el usuario ignorar
estos detalles. En las siguientes secciones se presenta una implementación
usando arreglos, esta implementación sirve como repaso de los principales
conceptos de Java y como introducción a otros temas importantes
relacionados con la programación en general.
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
1.3.
Ejemplo de uso del TAD Conjunto
En esta sección se presenta un ejemplo sencillo del uso de los conjuntos para
resolver un problema. El problema consiste en escribir un programa que
permite detectar palabras “mal escritas” en un texto. Para ello se tiene un
conjunto con las palabras que están bien escritas (las palabras del
diccionario). El algoritmo consiste en crear un conjunto con las palabras del
texto, al obtener la diferencia de ambos conjuntos se tienen las palabras que
están en el texto pero no en el diccionario. No necesariamente son palabras
mal escritas, porque podr´ıan ser palabras como software, que está bien pero
no aparece en el diccionario o conjugaciones de verbos, o alguna otra
variación.
Ejemplo 1.1. Una solución, en Java, al problema planteado utilizando la
clase Conjunto que contiene la implementación de los métodos descritos en
la interfaz Conjuntable de la sección anterior.
import [Link].*;
import [Link].*;
/**
* Programa que determina las palabras que están en un texto dado y no
* están en un diccionario.
* @author Amparo López Gaona
* @version
Abril 2011
*/
public class VerificadorSintaxis {
private static Conjunto leerPalabras(Reader in, int n) throws IOException{
Conjunto resultado = new Conjunto(n); // Conjunto para n palabras máximo
StreamTokenizer tok = new StreamTokenizer(in);
[Link](’.’);
[Link](true);
int c = [Link]();
while (c != StreamTokenizer.TT_EOF) {
if (c == StreamTokenizer.TT_WORD)
[Link]([Link]);
c = [Link]();
return resultado;
public static void main (String [] pps) {
try {
// lee los dos conjuntos de palabras
1.4. IMPLEMENTACI ´
ON DE CONJUNTOS
Conjunto dicc = leerPalabras(new FileReader("[Link]"),90000); Conjunto
texto = leerPalabras(new FileReader("[Link]"),100); [Link](dicc);
// elimina las palabras bien escritas
// imprime las palabras mal escritas
[Link]("\nLas palabras mal escritas son \n"); Iterator e =
[Link]();
int cont = 0;
// Escribe 10 palabras por l´
ınea
while ([Link]()) {
[Link]([Link]()+ " ");
if (cont == 10) {[Link](); cont = 0;}
else cont++;
} catch (IOException e) { [Link]("Excepción:"+e); }
En este programa se utiliza la clase StreamTokenizer que permite extraer
tokens de un flujo de datos (en este caso de un Reader). El método
nextToken extrae el siguiente elemento del flujo y devuelve una constante
que especifica el tipo de elemento extra´ıdo: TT EOF indica que se ha
llegado al final del archivo, TT WORD indica que el token es una palabra,
etc. Si se trata de una palabra, la deja en el atributo sval. En el método
leerPalabras se especifica con lowerCaseMode(true) que se desea que el
texto se trabaje con minúsculas.
Como se puede observar la solución es sencilla y se utiliza el TAD Conjunto
sin conocer la implementación del mismo, sólo conociendo la interfaz que
implementa.
1.4.
Implementaci´
on de conjuntos
En esta sección se presenta una implementación de los métodos contenidos
en la interfaz Conjuntable definida en la sección 1.2..
Clase 1.1. Clase que implementa la interfaz Conjuntable utilizando un
arreglo de objetos de la clase Object, esto para permitir que los conjuntos
puedan contener objetos de cualquier clase.
/**
* Programa que implementa el tipo abstracto de datos conjunto.
* @author Amparo López Gaona
* @version 1.
*/
public class Conjunto implements Conjuntable {
private Object[] datos;
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
El arreglo de datos contiene a los elementos del conjunto; es importante
notar que no se ha definido el tama˜
no del arreglo, pues es desconocido hasta su creación. En la implementación
se presentan tres constructores: uno por omisión, otro de copia y otro que
recibe el tama˜
no
del conjunto.
M´
etodo 1.1. Constructor por omisión. Este constructor crea un conjunto con
capacidad para 20 elementos. Es preciso recordar que la instrucción this
llama al constructor de esta clase cuya firma coincida con el tipo y cantidad
de parámetros en this.
/**
Constructor de un conjunto con capacidad para 20 elementos.
*/
public Conjunto() {
this(20);
M´
etodo 1.2. Constructor que recibe un número entero que especifica la
cantidad de datos permitidos en el conjunto. Si la cantidad que recibe el
constructor no es válida crea un conjunto con capacidad para 20 datos. El
arreglo se llena con el valor null para especificar que en cada localidad no se
tiene almacenado ningún valor válido, con lo cual esta implementación
impone la restricción de que ningún conjunto puede tener a null como
elemento.
/**
* Constructor de un conjunto con capacidad para los elementos
* indicados. Si el tama~
no es negativo o cero, se construye un
* conjunto con capacidad para 20 elementos.
* @param tam capacidad del conjunto.
*/
public Conjunto(int tam) {
datos = new Object[tam <= 0 ? 20 : tam];
for (int i = 0; i < [Link]; i++)
datos[i] = null;
M´
etodo 1.3. Constructor de copia. Como su nombre indica hace una copia del
conjunto que recibe como parámetro.
/**
* Constructor de copia.
* @param Conjunto -- conjunto que se tomará como valor inicial para
* crear el nuevo.
*/
public Conjunto(Conjunto c) {
1.4. IMPLEMENTACI ´
ON DE CONJUNTOS
datos = new Object[[Link]];
for (int i = 0; i < [Link]; i++)
datos[i] = [Link][i];
M´
etodo 1.4. Método para determinar si un conjunto no tiene elementos. Para
ello, en el método se recorre todo el arreglo buscando que todos los
elementos sean igual a null, en cuyo caso devuelve true. Si algún elemento
es null devuelve false.
/**
* Determina si el conjunto tiene elementos o no.
* @return boolean - Devuelve true si el conjunto no tiene elementos y
false en otro caso.
*/
public boolean estaVacio(){
for (int i = 0; i < [Link]; i++)
if (datos[i] != null) return false;
return true;
M´
etodo 1.5. Método para calcular la cantidad de elementos en el conjunto,
para ello lo recorre contando los elementos diferentes de null.
/**
* Devuelve la cantidad de elementos que tiene el conjunto.
* @return int - cantidad de
elementos que tiene el conjunto.
*/
public int tamanio(){
int tam = 0;
for (int i = 0; i < [Link]; i++)
if (datos[i] != null) tam++;
return tam;
M´
etodo 1.6. Método para dejar vac´ıo un conjunto sustituyendo todos sus
elementos por el valor null.
/**
* Elimina los elementos que tiene el conjunto.
*/
public void vaciar(){
for (int i = 0; i < [Link]; i++)
datos[i] = null;
8
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
M´
etodo 1.7. Método para determinar si un elemento dado está en el conjunto.
Para ello recorre el arreglo buscando el elemento mediante el uso del método
equals. Una buena costumbre es la de sobre-escribir en todas las clases el
método equals para poder comparar objetos de la misma clase por su estado
y no por su referencia.
/**
* Determina si un elemento está contenido en el conjunto.
* @param elem - elemento que se desea saber si está en el conjunto.
* @return boolean - devuelve true si el elemento está en el conjunto y
false en otro caso.
*/
public boolean contiene (Object elemento){
if (!estaVacio())
for (int i = 0; i < [Link]; i++)
if ([Link](datos[i]))
return true;
return false;
}
M´
etodo 1.8. Método para eliminar del conjunto el elemento indicado; para
hacerlo lo sustituye por el valor null, que se sabe es un elemento que no está
contenido en el conjunto.
Si el elemento no está en el conjunto no hace nada pues el objetivo es que
ese elemento no esté en el conjunto.
/**
* Elimina del conjunto el elemento especificado.
* @param elem - elemento que se desea eliminar del conjunto.
*/
public void eliminar(Object elemento) {
if (!estaVacio())
for (int i = 0; i < [Link]; i++)
if ([Link](datos[i])) {
datos[i] = null;
return;
M´
etodo 1.9. Método para agregar un elemento al conjunto. Para ello se busca
el primer lugar desocupado y si el elemento no está en el conjunto se inserta.
Si el elemento ya está en el conjunto no hace nada, y si el conjunto ya no
tiene espacio se dispara la excepción IllegalArgumentException.
1.5. ITERADORES
/**
* Agrega un elemento al conjunto, siempre y cuando no exista.
* @param elem - Elemento a insertar.
* @throws IllegalArgumentException - cuando el conjunto está lleno.
*/
public void agregar(Object elemento){
if (! contiene(elemento)) {
for (int i=0; i < [Link]; i++)
if (datos[i] == null) {
datos[i] = elemento;
return;
throw new IllegalArgumentException("El conjunto está lleno.");
Es necesario notar que al agregar un nuevo elemento no se asigna una copia
de él, debido a que se desconoce la clase del mismo. Esto puede ser un
problema, por ejemplo, si se tiene un conjunto con las personas que viven en
una ciudad y por cualquier causa se actualiza el domicilio de alguna persona
a través de los métodos que se tengan para ello en la clase Persona, de
manera automática e impl´ıcita se actualiza en el conjunto y podr´ıa darse el
caso de que esta actualización dé lugar a que el objeto modificado ya no
cumpla con algún criterio que le permitió ser parte del conjunto, pero como
ya es parte de él la situación pasa inadvertida. Para evitar este problema es
conveniente que al llamar al método agregar en el parámetro se asigne una
copia del objeto que se desea agregar no el objeto mismo. Para ello se puede
utilizar el constructor de copia o bien el método clone especificados en la
clase del objeto.
En la siguiente sección se presenta un concepto de gran ayuda en la
programación en general, pero muy útil en la programación de los métodos
restantes (unión, intersección, etcétera) de la interfaz Conjuntable.
1.5.
Iteradores
Una tarea frecuente al trabajar con una colección o estructura de datos es
recorrerla para obtener todos los elementos que la componen, ya sea para
imprimirlos o bien realizar una operación con todos ellos. Por ejemplo,
incrementar el salario de todos los empleados, dar de alta un conjunto de
alumnos en un curso particular, etcétera.
Para ejemplificar esta situación se puede suponer que se requiere escribir un
programa en el que es necesario imprimir el contenido de un conjunto.
Ejemplo 1.2. Primera versión de un programa que imprime el contenido de
un conjunto.
10
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
public class PruebaConjuntos {
public static void main(String [] pps) {
Conjunto grupo = new Conjunto();
...
// Instrucciones para llenar el conjunto
for (int i = 0; i < [Link](); i++) {
[Link]([Link](i));
Esta es una forma sencilla de imprimir los elementos del conjunto, sin
embargo, en el caso de la implementación para conjuntos que se presentó en
la sección anterior no es posible hacerlo porque no se tiene el método
obtenerElemento. Aun si se tuviera no ser´ıa apropiado especificar la
posición del elemento, porque eso va contra la definición de conjunto, en
tanto que los datos no se guardan por posición.
Una posibilidad es incluir en la clase Conjunto un método obtenerElementos
(método 1.10) que devuelva el arreglo en donde está almacenado el
conjunto.
M´
etodo 1.10. Método que devuelve el arreglo en donde se guarda un conjunto.
public Object[] obtenerElementos() {
return datos;
Ejemplo 1.3. Segunda versión de un programa que imprime el contenido de
un conjunto; aqu´ı se utiliza el método obtenerElementos.
public class PruebaConjuntos {
public static void main(String [] pps) {
Conjunto grupo = new Conjunto();
...
// Instrucciones para llenar el conjunto
Object [] obj = [Link]() {
for (int i = 0; i < [Link]; i++) {
[Link](obj[i]);
Sólo que esta forma no es genérica, pues es particular de la implementación
de la colección.
Es decir, si cambia la implementación del conjunto se debe cambiar la
aplicación que lo imprime.
1.5. ITERADORES
11
Una forma genérica de realizar esta función es a través de un iterador. Un
iterador es un objeto que permite recorrer una colección de objetos. Para
crear un iterador se debe implementar la interfaz Iterator del paquete
[Link] que tiene la siguiente forma: public interface Iterator {
public boolean hasNext();
public Object next() throws NoSuchElementException;
public void remove() throws UnsupportedOperationException,
IllegalStateException;
}
Los métodos de la interfaz Iterator tienen el siguiente propósito:
• hasNext devuelve true si en la colección aún hay al menos un elemento. En
caso de no haber más elementos devuelve false.
• next devuelve el siguiente elemento de la colección. La primera vez que se
llama a este método devuelve el primer elemento de la colección. Si la
colección no tiene elementos y se llama a este método, se dispara la
excepción NoSuchElementException.
• remove permite eliminar de la colección el último elemento devuelto por el
iterador.
Este método puede ser llamado sólo una vez por cada llamada al método
next. Esta operación es opcional; si se decide no permitir esta operación es
necesario disparar la excepción UnsupportedOperationException.
Normalmente las clases que implementan o tienen colecciones incluyen un
método que devuelve un iterador. En general, el iterador se implementa en
una clase interna privada debido a que ésta tiene acceso, sin calificativo, a
todos los métodos y estructura del objeto que la encierra,
independientemente de la visibilidad de éstos afuera de la clase.
Clase 1.2. Clase Conjunto que incluye la implementación de un iterador.1
import [Link];
public class Conjunto implements Conjuntable {
// Estructura y métodos igual que antes.
// Se agregan los métodos siguientes:
/**
* Método que devuelve
un iterador sobre el conjunto
* @return Iterator -- iterador sobre el conjunto
1En la interfaz Conjuntable no se especificó el iterador por cuestiones
didácticas, sin embargo es conveniente incluir la firma del método que
devuelve el iterador.
12
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
public Iterator iterador () {
return new miIterador();
/**
Implementación del iterador **/
private class miIterador implements Iterator {
private int pos;
//Índice para recorrer el conjunto
public miIterador(){
// Constructor por omisión
pos = 0;
/*
Determina si aún hay elementos en el conjunto, moviendo el
´
ındice hasta la posición del primer elemento diferente de null, si es que lo
hay */
public boolean hasNext() {
while (pos < [Link] && datos[pos] == null) pos++;
return (pos < [Link]);
/* Devuelve el siguiente elemento en el conjunto y avanza el
ındice una posición */
public Object next() throws NoSuchElementException {
if (hasNext())
return datos[pos++];
throw new NoSuchElementException("No hay elementos en el conjunto");
/* Operación no implementada */
public void remove() throws UnsupportedOperationException,
IllegalStateException {
throw new UnsupportedOperationException();
}
}
Como puede observarse, la implementación del iterador depende por
completo de la implementación del conjunto, sin embargo para su utilización
ya no se requiere conocer la implementación de éste, sólo basta conocer los
métodos del iterador.
M´
etodo 1.11. En el siguiente ejemplo se muestra la forma de utilizar el
iterador para imprimir todos los elementos del conjunto. Para que el método
muestre adecuadamente los elementos del conjunto, en la clase de estos
elementos se debe tener el método toString.
1.5. ITERADORES
13
/**
* Método para imprimir los elementos de un conjunto
* @param Conjunto - Conjunto que se desea imprimir
*/
public void imprimir(Conjunto c) {
Iterator it = [Link]();
while ([Link]())
[Link]([Link]() + "
");
1.5.1.
Ejemplos de uso de iteradores
En esta sección se presenta la programación de los métodos unión,
intersección, diferencia y contención para trabajar con conjuntos como
ejemplos de uso del iterador construido en la clase Conjunto.
M´
etodo 1.12. El método union forma un conjunto que tiene los elementos del
conjunto con que se llama al método más los elementos que tiene el conjunto
parámetro y que aún no están en él. Se utiliza un iterador sobre el conjunto
parámetro y cada elemento en tal conjunto se agrega al conjunto que llama al
método. Recordar que en el método agregar se verifica que el elemento no
esté en el conjunto antes de agregarlo.
/**
* Método que devuelve la unión de dos conjuntos
* @param Conjunto -- conjunto que unirá al que llama a este método
* @return Conjunto -- conjunto con la unión
*/
public Conjunto union(Conjuntable c) {
Conjunto cUnion = new Conjunto(this);
Iterator it = [Link]();
while([Link]())
[Link]([Link]());
return cUnion;
}
M´
etodo 1.13. El método que calcula la intersección de dos conjuntos utiliza un
iterador sobre el conjunto que llama al método y para cada elemento se
verifica que no esté en el conjunto parámetro, si es el caso, lo elimina del
conjunto que llama al método.
/**
* Método que devuelve la intersección de dos conjuntos
* @param Conjunto -- conjunto que intersecta con el conjunto que
14
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
* llama a este método
* @return Conjunto -- conjunto con la intersección
*/
public Conjunto interseccion(Conjuntable c) {
Conjunto cInterseccion = new Conjunto(this);
Iterator it = iterador();
while ([Link]()) {
Object elemento = [Link]();
if ()
[Link](elemento);
}
return cInterseccion;
M´
etodo 1.14. El método que calcula la diferencia de dos conjuntos es similar
en su implementación al método anterior, excepto que en este caso se
eliminan los elementos que están en ambos conjuntos.
/**
* Método que devuelve la diferencia de dos conjuntos
* @param Conjunto -- conjunto con el que se hará la diferencia
* @return Conjunto -- conjunto con la diferencia
*/
public Conjunto diferencia(Conjuntable c) {
Conjunto cMenos = new Conjunto(this);
Iterator it = iterador();
while ([Link]()) {
Object elemento = [Link]();
if ([Link](elemento))
[Link](elemento);
return cMenos;
}
M´
etodo 1.15. El método para determinar si un conjunto es subconjunto de otro
recorre el primer conjunto, y si encuentra un elemento que no esté en el
segundo conjunto entonces no es su subconjunto. Si termina la revisión y
todos los elementos están en el segundo conjunto entonces si es subconjunto.
/**
* Método para determinar si un conjunto es subconjunto de otro
1.6. AN ´
ALISIS DE ALGORITMOS
15
* @param Conjunto -- conjunto en el que se busca el subconjunto
* @return boolean -- devuelve true si el conjunto que llama a este método
* es subconjunto del parámetro y false en otro caso
*/
public boolean subconjunto(Conjuntable c) {
Iterator it = iterador();
while ([Link]())
if ())
return false;
return true;
}
1.6.
An´
alisis de algoritmos
Es común encontrar más de un algoritmo para resolver un problema
particular. Si lo único que interesa es que el problema sea resuelto, se puede
elegir cualquiera. En caso de que se requiera que el programa se ejecute lo
más rápido posible, entonces es necesario comparar los algoritmos
encontrados para determinar el mejor en términos de rapidez de ejecución.
El análisis de algoritmos es un área importante de la computación teórica, en
esta sección se proporciona una introducción al tema debido a la importancia
que tiene realizar implementaciones eficientes de los algoritmos, sobre todo
en el caso de la implementación de estructuras de datos, puesto que éstas son
utilizadas frecuentemente para trabajar con grandes cantidades de datos y es
necesario determinar cuál es la más adecuada para una tarea particular.
La eficiencia de un algoritmo medida en términos del tiempo requerido para
su ejecución puede parecer tarea fácil, pues de manera inmediata puede
pensarse que basta con medir el tiempo que tarda en ejecutarse el programa
que lo implementa, ya sea con un reloj en mano o bien desde el propio
programa.2Sin embargo, esto no es conveniente porque depende de muchos
factores, por ejemplo, la cantidad de procesos que esté ejecutando el sistema
operativo o la disponibilidad de los recursos que requiera el programa. Otra
posibilidad de medir un programa es contar las l´ıneas de código de los
programas, aunque tampoco es una forma aceptada porque puede depender
del lenguaje usado y de la habilidad del programador.
Una tercera opción es aislar las operaciones fundamentales del algoritmo y
contar la cantidad de veces que se ejecutan. Por ejemplo, si se desean sumar
los 100 elementos de una colección de datos. La operación importante es la
de sumar y se hace 99 veces. Este número puede generalizarse como una
función f ( n) = n − 1, para un conjunto de n elementos a sumar. De manera
que ahora se pueden comparar algoritmos para el caso general, no sólo para
uno particular. Más adelante se especifica cuáles son las operaciones
fundamentales.
2 En Java puede hacerse utilizando el método [Link](),
que devuelve la hora en milisegundos.
16
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
El análisis de algoritmos es un técnica que permite obtener una medida de la
eficiencia de un algoritmo, independientemente de la plataforma o lenguaje
en que se ejecute éste. Debido a que un algoritmo por lo general se programa
en varios métodos, la medida se basa en examinar el tiempo de ejecución de
cada método. Al agilizar al menos un método se puede mejorar la velocidad
de ejecución del programa entero. Generalmente se calcula el tiempo de
ejecución en el peor caso, porque de esa manera se tiene una estimación de
lo más que podr´ıa tardarse la ejecución del mismo.
La importancia de conocer la complejidad de un algoritmo está en que se
puede predecir el crecimiento de tiempo en la ejecución de un programa
conforme se incremente la cantidad de datos; de esta forma se puede
comparar la complejidad en términos de tiempo de dos programas que
realizan la misma tarea.
Para representar la complejidad de los algoritmos se utiliza una notación
conocida como O grande que permite evaluar el ritmo de crecimiento en el
tiempo de ejecución como una función dependiente de la cantidad de datos
del problema; esta función no indica la cantidad de tiempo que tarda en
ejecutarse un programa. Definida de manera formal una función f( n) es de
orden g( n) : if( n) = O( g( n)) ⇐⇒ ∃ constantes positivas c y no tales que f(
n) ≤ c( g( n)) ∀c, n ≥ n 0
Esta función es muy fácil de calcular para programas sencillos; para poder
estimar este tipo de funciones se ha desarrollado un álgebra que permite
calcularla para programas complejos.
Para ello se deben tener en cuenta las propiedades de este tipo de funciones.
Suponer que se tienen dos algoritmos A1 y A2 con tiempos de ejecución O( f
1( n)) y O( f 2( n)) respectivamente, se tienen las siguientes reglas:
1. Regla de la suma. Se aplica cuando se desea evaluar la complejidad del
algoritmo resultante de ejecutar A1 seguido de A2, en este caso la
complejidad está dada por O(max( f 1( n) , f 2( n))).
2. Regla de la multiplicación. Se aplica cuando se desea evaluar la
complejidad del algoritmo resultante de ejecutar A2 dentro del algoritmo
A1, en este caso la complejidad está dada por O( f 1( n) f 2( n)).
Tomando en cuenta estas reglas se puede proceder a evaluar la complejidad
de cualquier algoritmo como sigue:
1. Asignaciones, declaraciones. Las asignaciones tienen complejidad
dependiente de la complejidad de la expresión que determina el valor a
asignar. Para efectos de cálculo se asume que todas las operaciones sobre
tipos primitivos, las asignaciones y las secuencias de asignaciones toman un
tiempo constante, expresado como O(1).
2. Proposiciones consecutivas. Se evalúa la complejidad de cada una de las
proposi-ciones y se aplica la regla de la suma.
1.6. AN ´
ALISIS DE ALGORITMOS
17
3. Condicionales. La complejidad de las instrucciones condicionales está
determinada por la complejidad de la evaluación de la condición y del
cuerpo de la instrucción condicional. La condición comúnmente tiene
complejidad O(1). La complejidad del cuerpo se calcula como la mayor de
los bloques de instrucciones de cada alternativa de la condicional. Por
ejemplo:
if (a > 0) {
suma += a;
datos++;
} else {
a*= (-1);
Éste tomar´ıa una unidad de tiempo para la condición más dos para las
asignaciones de la instrucción if (tiene más que el else), es decir, tardar´ıa
tres unidades de tiempo, por lo tanto es de orden O(1).
4. Ciclos. La complejidad de un ciclo se calcula utilizando la regla de la
multiplicación.
Para ello se toma el tiempo de ejecución de su cuerpo multiplicado por el
número de veces que se realiza. El caso más sencillo es cuando están fijos el
valor inicial y el de la condicional y el cuerpo toma un tiempo constante.
Ejemplo: for (int i=0; i < n; i++)
if (valores[i] < m´
ınimo)
m´
ınimo = valores[i];
Este ciclo se realiza n veces y el cuerpo toma un tiempo constante cada vez,
por tanto, el tiempo de ejecución de este ciclo es O(1 ∗ n) = O( n).
Un análisis un poco más complejo es el requerido para el ciclo presentado en
siguiente método:
public boolean esPrimo (int n) {
for (int i=2; i*i <= n; i++)
if (0 == n %i) return false;
return true;
Aqu´ı el ciclo se hace mientras la i sea menor o igual a la ra´ız cuadrada de
n, es decir,
el número máximo de iteraciones es
n, como el cuerpo del ciclo toma un tiempo
constante, se tiene que el algoritmo es O( n).
Si se tiene un ciclo anidado, el tiempo de ejecución, o complejidad, también
se calcula multiplicando el tiempo de ejecución del cuerpo de la iteración
por el número de veces que se realiza el ciclo, por ejemplo:
18
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
for (int i = 0; i < n ; i++)
for (int j = 0; j < n ; j++)
k++;
Este algoritmo tiene complejidad n 2, porque para cada uno de los n valores
de i se realiza la instrucción for interna, que es de complejidad n debido a
que realiza n veces su cuerpo, que tarda un tiempo constante en ejecutarse.
El siguiente ejemplo también es fácil de analizar:
for (int i = 0; i <n ; i++)
for (int j=0; j < n ; j++) {
c[i][j] = 0;
for (int k=0; k < n ; k++)
c[i][j] += a[i][k] * b[k][j];
El ciclo más interno tiene complejidad n debido a que se realiza n veces y su
cuerpo es de complejidad 1, el ciclo de en medio es de complejidad n 2
porque se realiza n veces pero, como se explicó, el ciclo que es su cuerpo
tiene complejidad n. Finalmente, el ciclo externo se realiza n veces y la
complejidad de su cuerpo es n 2, lo que al multiplicarse da n 3 como
complejidad del ciclo externo.
Cuando los l´ımites de las iteraciones están relacionados con los ´ındices
externos, el análisis se vuelve más complejo.
for (int i = n-1; i > 0; i--)
for (int j = 0; j < i; j++)
if (v[j] > v[j+1]) {
double temp = v[j];
v[j] = v[j+1];
v[j+1] = temp;
En este caso la instrucción if tiene complejidad constante. La iteración
interna se realiza, en el peor de los casos, n − 1 veces, con lo cual su
complejidad es de n − 1, que multiplicada por n − 1, que es el número de
veces que realiza el ciclo externo, es igual a n 2 − 2 n + 2 = n 2. En seguida
se explica la eliminación del valor constante.
5. Llamadas a m´
etodos. La complejidad de las llamadas a métodos está determinada por la
complejidad del método. Por ejemplo:
1.6. AN ´
ALISIS DE ALGORITMOS
19
public void imprimirPrimos(int n) {
for (int i =2; i <=n; i++)
if (esPrimo(i))
[Link]("valor "+ i + "es primo."); else
[Link]("valor "+ i + "no es primo.");
El método esPrimo tiene una complejidad
n, como se explicó anteriormente. Este
valor se multiplica por n que es el número de veces en que se realiza la
instrucción for,
por tanto el método imprimirPrimo tiene complejidad n n Los órdenes de
magnitud o complejidad de los algoritmos más comunes son:
• Constante. O(1) es el tiempo ideal pues no depende de la cantidad de datos.
• Logar´ıtmico. O(log2( n)) algoritmos que sucesivamente descartan una
cantidad de datos, generalmente la mitad, para ser procesados son los que t
´ıpicamente caen en es-ta categor´ıa. Estos algoritmos se consideran
eficientes. Ejemplo, la búsqueda binaria (véase apéndice A).
• Casi lineal, llamados de orden n logaritmo de n. O( n log( n)), algoritmos
con esta complejidad aparecen en ciclos donde el cuerpo es de orden logar
´ıtmico. Se consideran algoritmos eficientes.
• Lineal. O( n) algoritmos con esta complejidad son aquellos que trabajan
con todos los datos. Ejemplo, búsqueda de un elemento en un conjunto
desordenado.
• Cuadrático. O( n 2) algoritmos con este tipo de complejidad generalmente
involucran aplicar un algoritmo lineal n veces.
• Exponencial. O(2 n), estos algoritmos son extremadamente costosos y no
deseables. Esta complejidad aparece en algoritmos que se basan en el ensayo
reiterado de soluciones.
Ejemplo 1.4. Análisis del método tamanio de la clase Conjunto visto en la
sección 1.4.
cuyo cuerpo se reproduce a continuación:
public int tamanio(){
int tam = 0;
for (int i = 0; i < [Link]; i++)
if (datos[i] != null) tam++;
return tam;
20
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
El peor caso para calcular el tama˜
no de un conjunto con el método anterior se presenta cuando el conjunto está
lleno, debido a que es necesario recorrer todo el arreglo. Cada instrucción de
asignación se realiza en un tiempo constante c, y si el arreglo tiene n
localidades, cuando el conjunto está lleno es necesario hacer n veces la
instrucción if. Si cada una se realiza en un tiempo constante c, entonces el
método toma c + ( c × n) + c unidades de tiempo en realizarse.
Expresado de manera formal se dice que el algoritmo tiene complejidad O( c
∗ ( n + 2)). En el análisis de algoritmos se busca una cota superior, por eso
no se toman en cuenta las constantes, es decir, no se dice O( c ∗ ( n + 2) la
forma correcta es O( n). Si cada comparación toma 1 segundo y se tiene un
arreglo de 1000 datos, el método tardará 1002 segundos en terminar. Si n =
2000 entonces el tiempo es 2002, casi el doble de 1000. Si n = 500 entonces
el tiempo es 502, casi la mitad de 1000. Si se tienen 3000 datos, el tiempo
que toma es 3002, prácticamente el triple. Se puede observar que el tiempo
de ejecución del método depende de la cantidad de los datos de entrada y las
constantes son irrelevantes. Se puede hacer la demostración formal de este
resultado, pero para los fines con que se usa el análisis de algoritmos en este
texto basta con la explicación anterior.
1.7.
Mejora al TAD Conjunto
De acuerdo con lo explicado en la sección anterior se tiene que el tiempo de
ejecución, en el peor de los casos, de cada método del TAD Conjunto es:
Operación
Tiempo de ejecución
constructores
O(n)
estaVacio
O(n)
tamanio
O(n)
contiene
O(n)
eliminar
O(n)
agregar
O(n)
iterador
O(1)
hasNext
O(n)
next
O(n)
union
O(n2)
interseccion
O(n3)
diferencia
O(n3)
subconjunto
O(n2)
Tabla 1.1. Tiempo de ejecución de los métodos de la clase Conjunto.
Como puede observarse la mayor´ıa de los métodos son O(n). Se puede tener
una implementación más eficiente de este TAD con sólo utilizar una variable
que indique cuál es la
1.7. MEJORA AL TAD CONJUNTO
21
cantidad de elementos en el conjunto y colocando los datos en localidades
consecutivas. Por lo tanto, cada vez que se modifique el conjunto se debe
actualizar tal variable. En los siguientes párrafos se presenta tal
implementación empezando por los constructores.
Ejemplo 1.5. Segunda versión de la clase que implementa la interfaz
Conjunto. En esta versión además del arreglo con los datos se tiene un
contador que indica la cantidad de datos almacenados.
/**
* Clase para el tipo abstracto de datos Conjunto.
* @author Amparo López Gaona
* @version 2.
*/
public class Conjunto implements Conjuntable {
private Object[] datos;
// Almacenamiento para el conjunto
private int nDatos;
// Cantidad de datos almacenados
/**
Constructor de un conjunto con capacidad para 20 elementos.
*/
public Conjunto() {
this(20);
/**
* Constructor de un conjunto con capacidad para los elementos indicados.
* @param tam capacidad del conjunto.
*/
public Conjunto(int tam) {
datos = new Object[(tam <= 0) ? 20: tam];
nDatos = 0;
/**
* Constructor de copia.
* @param Conjunto -- conjunto que se tomará como valor inicial para
* crear el nuevo.
*/
public Conjunto(Conjunto c) {
datos = new Object[[Link]];
for (int i = 0; i < [Link]; i++)
datos[i] = [Link][i];
nDatos = [Link];
22
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
Como puede observarse, los constructores son prácticamente iguales en esta
versión y en la anterior con la excepción de que ahora se actualiza la variable
nDatos que lleve registro de la cantidad de datos en el conjunto.
M´
etodo 1.16. El método para determinar si un conjunto está vac´ıo
simplemente verifica que el valor de la variable nDatos sea igual a cero, en
caso contrario el conjunto no está vac´ıo.
/**
* Determina si el conjunto tiene elementos o no.
* @return boolean - Devuelve true si el conjunto no tiene elementos y
*
false en otro caso.
*/
public boolean estaVacio(){
return (nDatos == 0);
M´
etodo 1.17. El método para dejar conjunto vac´ıo simplemente asigna a la
variable nDatos el valor cero.
/**
* Vac´
ıa el contenido de un conjunto.
* @return boolean - Devuelve true si el conjunto no tiene elementos y
false en otro caso.
*/
public void vaciar(){
nDatos = 0;
M´
etodo 1.18. El método para dejar vac´ıo un conjunto simplemente asigna a la
variable nDatos el valor cero.
/**
* Vac´
ıa el contenido de un conjunto
*/
public void vaciar(){
nDatos = 0;
M´
etodo 1.19. El método tamanio devuelve el valor de la variable nDatos, que
es la cantidad de elementos del conjunto.
/**
* Devuelve la cantidad de elementos que tiene el conjunto.
* @return int - cantidad de elementos que tiene el conjunto.
1.7. MEJORA AL TAD CONJUNTO
23
*/
public int tamanio(){
return nDatos;
}
M´
etodo 1.20. El método contiene determina si un elemento está en el conjunto
revisando las localidades del arreglo en donde se almacena el conjunto hasta
encontrar el elemento o saber que ya se agotó el conjunto.
/**
* Determina si un elemento está contenido en el conjunto.
* @param elemento - elemento que se desea saber si está en el conjunto.
* @return boolean - devuelve true si el elemento está en el conjunto y
false en otro caso.
*/
public boolean contiene (Object elemento){
if (!estaVacio())
for (int i = 0; i < nDatos; i++)
if ([Link](datos[i])) {
return true;
return false;
M´
etodo 1.21. El método eliminar quita un elemento del conjunto
sustituyéndolo por el que está al final del mismo.
/**
* Elimina el elemento especificado del conjunto.
* @param elemento - elemento que se desea eliminar del conjunto.
*/
public void eliminar(Object elemento){
for (int i = 0; i < nDatos; i++)
if ([Link](datos[i])){
datos[j] = datos[nDatos--];
return;
M´
etodo 1.22. El método agregar inserta un nuevo elemento en el conjunto,
para ello debe verificar primero que no exista este elemento.
/**
24
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
* Agrega un elemento al conjunto, siempre y cuando no exista.
* @param elemento - Elemento a insertar.
* @throws IllegalArgumentException -- cuando el conjunto está lleno.
*/
public void agregar(Object elemento){
if (nDatos >= [Link])
throw new IllegalArgumentException ("El conjunto está lleno"); if
(!contiene(elemento))
datos[nDatos++] = elemento;
Clase 1.3. Clase interna que implementa el funcionamiento del iterador.
/**
* Devuelve un iterador sobre los elementos del conjunto.
* @return Iterador
*/
public Iterator iterador () {
return new miIterador();
private class miIterador implements Iterator {
private int pos;
//Índice para recorrer el conjunto
public miIterador(){
// Constructor por omisión
pos = 0;
}
//Determina si aún hay elementos en el conjunto
public boolean hasNext() {
return (pos < nDatos);
// Devuelve el siguiente elemento en el conjunto
public Object next() throws NoSuchElementException {
if (hasNext())
return datos[pos++];
throw new NoSuchElementException("No hay elementos en el conjunto");
public void remove() throws IllegalStateException,
NoSuchElementException {}
1.7. MEJORA AL TAD CONJUNTO
25
Es importante notar que cambia la implementación del iterador, pero la
forma de uso permanece intacta. El usuario no se entera que cambió la
implementación ni de la clase, ni del iterador porque se siguen
implementando las mismas interfaces.
La implementación de los métodos que utilizan el iterador no cambia, pues
como se menciona en el párrafo anterior, éste se sigue utilizando igual en
tanto la interfaz no cambia, sólo la implementación. El resultado de hacer los
cálculos correspondientes de la complejidad de los métodos de la segunda
versión del TAD Conjunto es el presentado en la tabla 1.2.: Operación
Tiempo de ejecución
constructor por omisión
O(1)
constructor de copia
O(n)
estaVacio
O(1)
tamanio
O(1)
contiene
O(n)
eliminar
O(n)
agregar
O(n)
iterador
O(1)
hasNext
O(1)
next
O(1)
Tabla 1.2. Tiempo de ejecución de los métodos de la segunda versión de la
clase Conjunto.
Los métodos contiene, eliminar y agregar siguen teniendo la misma
complejidad porque en el peor caso es necesario recorrer todo el arreglo, sin
embargo, en esta versión los otros métodos tienen orden constante, lo cual es
inmejorable. Cabe resaltar que el cambio fue m´ınimo, agregar una variable
que sirve de contador, sin embargo, el beneficio fue muy grande al reducir la
complejidad de la mayor´ıa de los métodos.
Otra mejora que puede hacerse a la clase Conjunto en cuanto a funcionalidad
aunque no en cuanto a velocidad consiste en modificar el método agregar
para que cuando se requiera agregar un elemento al conjunto y éste se
encuentre lleno, el arreglo “crezca” y no lance una excepción.
Es sabido que arreglo es la única estructura de datos que proporciona Java y
que su gran ventaja es que acceder a cualquier elemento de él toma siempre
el mismo tiempo, sin embargo una vez definido su tama˜
no éste no puede cambiar, por lo tanto los arreglos no pueden crecer.
En esta sección se presenta una forma de hacerlo.
M´
etodo 1.23. Método para agregar un elemento a un conjunto sin preocuparse
por el tama˜
no de éste. Si el arreglo está lleno, se llama al método privado crecerArreglo.
/**
* Agrega un elemento al conjunto, siempre y cuando no exista.
26
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
* @param elemento - Elemento a insertar.
*/
public void agregar(Object elemento){
if (nDatos >= [Link]) {
crecerArreglo();
// Si el conjunto está lleno aumenta su capacidad
if (!contiene(elemento)) {
datos[nDatos++] = elemento;
M´
etodo 1.24. Método privado para aumentar la capacidad de un conjunto, para
ello crea un arreglo más grande que el original, luego copia todos los datos
del arreglo original al nuevo y finalmente copia la referencia del nuevo
arreglo en el original, con lo cual a partir de ese momento el original tiene un
tama˜
no mayor.
/*
* Aumenta la capacidad del arreglo.
* @param - nueva capacidad para el arreglo
*/
private void crecerArreglo() {
int tamanoNuevo = [Link] +10;
Object [] arregloNuevo = new Object[tamanoNuevo];
//Copia los elementos del arreglo anterior
for (int i = 0; i < nDatos; i++) {
arregloNuevo[i] = datos[i];
datos = arregloNuevo;
1.8.
Ejercicios
1. Implementar un método para la clase Conjunto que calcule el conjunto
potencia del conjunto dado. El conjunto potencia es el conjunto de todos sus
subconjuntos. Especificar la complejidad del método.
2. Llenar la siguiente tabla en la que se especifica el tiempo de ejecución de
un método que trabaja con n = 105 y luego con n = 1010 datos de entrada, y
se realizan 106
operaciones por segundo.
1.8. EJERCICIOS
27
Función
Nombre
Tiempo con n 5
Tiempo con n 10
log n
Logar´ıtmica
Lineal
n log n
n2
Cuadrática
n3
Cúbica
2n
Exponencial
3. Desarrollar un programa que lea un texto y muestre como salida las
palabras que aparecen en él, indicando cuántas veces aparece cada una.
4. Implementar un tipo abstracto de datos para trabajar con una colección de
datos en la cual los elementos se almacenan sin importar si se repiten y no
tienen un orden definido. La interfaz que se debe implementar es la
siguiente: public interface Coleccionable {
boolean estaVacia();
void limpiar();
int tamanio();
boolean eliminar (Object);
boolean eliminar(Object, int);
boolean eliminarTodos(Coleccionable);
int cardinalidad(Object);
boolean contiene(Object);
boolean contieneTodos (Coleccionable);
boolean agregar(Object);
boolean agregar(Object, int);
boolean agregarTodos(Coleccionable);
Coleccionable conjuntoUnico();
boolean equals (Object);
[Link] iterador();
En la interfaz Coleccionable el método:
• estaVacia devuelve true si no hay elementos en la colección y false en otro
caso.
• limpiar limpia la colección, es decir, la deja sin elementos. Este método
debe ser de orden O(1).
• tamanio devuelve el número de elementos en la colección.
28
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
• eliminar elimina de la colección el elemento proporcionado como
parámetro, todas las veces que está en la colección. Si el elemento no se
encuentra en ella devuelve false, en otro caso devuelve true.
• eliminar elimina de la colección el elemento proporcionado como
parámetro, las veces indicadas en el segundo parámetro. Este método debe
devolver true sólo si fue posible eliminar los elementos, las veces
especificadas. Si el elemento no se encuentra en ella devuelve false. Si el
número de veces es incorrecto dispara la excepción
IllegalArgumentException.
• eliminarTodos elimina de la colección los elementos incluidos en la
colección proporcionada como parámetro las veces que éste está en ella. Este
método devuelve true si la operación fue exitosa y false en otro caso.
• cardinalidad devuelve la cantidad de veces (cardinalidad) que un objeto
dado se repite en la colección.
• contiene devuelve true si la colección contiene al menos una vez el
elemento especificado.
• contieneTodos devuelve true si la colección contiene todos los elementos
en la colección especificada en su parámetro, respetando el número de veces
que lo tiene.
• agregar agrega a la colección el elemento proporcionado como parámetro.
Devuelve true si se pudo realizar exitosamente la operación y false en otro
caso
• agregar agrega a la colección el elemento proporcionado como parámetro,
el número de veces especificadas. Devuelve true si se pudo realizar
exitosamente la operación y false en otro caso. En caso que el número de
veces no sea positivo dispara la excepción IllegalArgumentException.
• agregarTodos agrega a la colección cada elemento contenido en la
colección que se le proporciona como parámetro respetando el número de
veces que aparece en el parámetro. Devuelve true si se pudo realizar
exitosamente la operación y false en otro caso.
• conjuntoUnico regresa la colección resultante de eliminar elementos
repetidos.
• equals devuelve true si ambas colecciones tienen igual cantidad de
elementos y cada uno tiene igual cardinalidad en cada colección. Recordar
que no importa el orden en que estén almacenados. En otro caso devuelve
false.
• iterador devuelve un iterador sobre los elementos de la colección.
5. Escribir una tabla con la complejidad de los métodos del TAD para
colecciones de datos desordenados y repetidos que implementa la interfaz
Coleccionable.
1.8. EJERCICIOS
29
6. Implementar un tipo abstracto de datos para trabajar con una colección de
datos ordenados. La interfaz que se debe implementar es la siguiente: public
interface Ordenable {
boolean estaVacia();
void limpiar();
int tamanio();
void eliminarMin ();
void eliminarMax();
void agregar(Object);
Object obtenerMin();
Object obtenerMax()
Object obtenerKesimo()
Ordenable obtenerSubcoleccion(Object);
Ordenable obtenerSubcoleccion(Object,Object);
boolean equals (Object);
[Link] iteradorASC();
[Link] iteradorDSC();
En la interfaz Ordenable el método:
• estaVacia devuelve true si no hay elementos en la colección y false en otro
caso.
• limpiar limpia la colección, es decir, la deja sin elementos. Este método
debe ser de orden O(1).
• tamanio devuelve el número de elementos en la colección.
• eliminarMin elimina de la colección el menor elemento de la colección.
• eliminarMax elimina de la colección el mayor elemento.
• agregar agrega a la colección el elemento proporcionado como parámetro.
• obtenerMin devuelve el elemento menor de la colección. Ésta permanece
intacta.
• obtenerMax devuelve el elemento mayor de la colección. Ésta permanece
intacta.
• obtenerKesimo devuelve el k-ésimo elemento de la colección. Ésta
permanece intacta.
• obtenerSubcoleccion regresa la colección que contiene desde el objeto
proporcionado como parámetro hasta el elemento mayor de la colección
original. Ésta permanece intacta.
30
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
• obtenerSubcoleccion regresa la colección que contiene a partir del objeto
proporcionado como primer parámetro hasta el elemento proporcionado
como segundo parámetro de la colección original. Ésta permanece intacta. Si
el primero parámetro es mayor que el segundo dispara una excepción.
• equals devuelve true si ambas colecciones tienen los mismos elementos. En
otro caso devuelve false.
• iteradorASC devuelve un iterador sobre los elementos de la colección para
hacer un recorrido del elemento menor al mayor.
• iteradorDSC devuelve un iterador sobre los elementos de la colección para
hacer un recorrido del elemento mayor al menor.
7. Escribir una tabla con la complejidad de los métodos del TAD para
colecciones de datos ordenados y repetidos que implementa la interfaz
Ordenable.
8. Implementar un TAD para trabajar con arreglos bidimensionales,
conocidos como matrices. La interfaz para este tipo de datos es la siguiente:
interface Dimensionable {
public boolean estaVacio ();
public int renglones();
public int columnas();
public void insertar(Object valor, int renglon, int columna); public Object
obtener(int
renglon, int columna);
public void eliminar(int
renglon, int columna);
public void reemplazar(Object valor, int
renglon, int columna);
public Dimensionable transponer ();
public boolean esSimetrica();
public [Link] iterador();
La forma de trabajar los arreglos de dos dimensiones, o matriz, es la
siguiente:
• estaVacio devuelve true si el arreglo de dos dimensiones no tiene
elementos y false en otro caso.
• renglones devuelve un entero que indica la cantidad de renglones que tiene
la matriz.
• columnas devuelve un entero que indica la cantidad de columnas que tiene
la matriz.
• insertar agrega a la matriz el elemento pasado como primer parámetro, en
la posición indicada en los siguientes dos parámetros. Si la posición es
inválida se dispara la excepción ArrayIndexOutOfBoundsException.
1.8. EJERCICIOS
31
• obtener devuelve el elemento almacenado en la posición indicada. Dispara
la excepción ArrayIndexOutOfBoundsException en caso de que la posición
sea inválida. Este método no cambia el estado de la matriz.
• eliminar elimina el elemento almacenado en la posición indicada. Dispara
la excepción ArrayIndexOutOfBoundsException en caso de que la posición
sea inválida. Este método no cambia el estado de la matriz.
• reemplazar sustituye el elemento almacenado en la posición indicada por el
objeto valor pasado como parámetro. Si la posición es inválida se dispara la
excepción ArrayIndexOutOfBoundsException.
• iterador devuelve un iterador sobre los elementos de la matriz.
• transponer devuelve la matriz transpuesta de la que llama al método.
Cambia los renglones por columnas, por lo tanto, si la matriz original tiene n
renglones y m columnas la matriz resultante tiene m renglones y n columnas.
• esSimetrica devuelve true si la matriz es simétrica y false en otro caso. Este
método sólo se aplica a matrices cuadradas, es decir, aquellas en las que el
número de renglones y el de columnas es el mismo. Una matriz es simétrica
si el elemento en la posición [i,j] es igual al de la posición [j,i] para todas las
j y todas las i.
9. Escribir una tabla con la complejidad de los métodos del TAD Matriz que
implementa la interfaz Dimensionable.
10. Implementar un tipo abstracto de datos para trabajar con mapas
ordenados (también llamados tablas, diccionarios o tablas de búsqueda). En
un mapa los elementos se almacenan en parejas, cada una consta de una
llave y un valor. Para recuperar un valor es necesario proporcionar la llave y
se obtiene el valor asociado a ella. Cada llave identifica una entrada, por lo
tanto debe ser única. Sin embargo, es posible que dos llaves diferentes
tengan asociado el mismo valor. La interfaz que se debe implementar es la
siguiente:
public interface Mapeable {
boolean estaVacio();
void limpiar();
int tamanio();
Object agregar(Object llave, Object valor);
Object obtener(Object llave);
void eliminar(Object llave);
[Link] iterador();
public interface Registro {
Object obtenerLlave();
Object obtenerValor();
32
CAP´
ITULO 1. TIPOS ABSTRACTOS DE DATOS
void asignarValor(Object valor);
En interfaz Mapeable, el método:
• estaVacio devuelve true si no hay elementos en el mapa.
• limpiar limpia el mapa. Este método debe ser de orden O(1).
• tamanio devuelve el número de elementos en el mapa.
• agregar agrega al mapa un registro devolviendo el valor previo si la llave
ya exist´ıa en el mapa o bien null si la llave es nueva.
• obtener devuelve el valor asociado con una llave o bien nulo si la llave no
está en el mapa.
• eliminar elimina del mapa el registro con la llave proporcionada, si se
encuentra en el mapa, de no ser asi no hace nada.
• iterador devuelve un objeto que implementa la interfaz [Link].
• Registro es una interfaz pública anidada, su tipo es [Link].
Este objeto almacena la pareja llave/valor, permitiendo accesar a la llave o al
valor y permitiendo también cambiar el valor. El iterador que regresa
elementos trabaja con los objetos [Link].
11. Escribir una tabla con la complejidad de los métodos del TAD para
trabajar con mapas ordenados que implementa la interfaz Mapeable.
Cap´ıtulo 2
Listas
Las estructuras de datos son una forma de almacenar información en la
computadora para que pueda ser utilizada en manera eficiente, donde por
eficiencia se entiende la habilidad de encontrar y manipular los datos con
rapidez y con el m´ınimo de recursos, como tiempo de proceso y espacio en
memoria. En las estructuras de datos, las menos restrictivas son las listas,
porque no imponen restricciones al contenido ni a la forma de acceder a sus
elementos. En este cap´ıtulo se presenta la definición del TAD Lista, varias
implementaciones y aplicaciones del mismo.
2.1.
Introducci´
on
Las listas aparecen frecuentemente en la vida real, a manera de ejemplo se
tienen; lista de compras, lista de útiles escolares, lista de invitados, lista de
pendientes, lista de tareas, lista de discos, etc. Se puede definir una lista
como una estructura de datos en la cual es irrelevante el orden y manejo de
los elementos que contiene, es decir las inserciones y supresiones pueden ser
en cualquier lugar.
Ejemplo 2.1. Como ejemplo de uso de listas en computación se tiene el
siguiente: suponer que se requiere escribir una lista con los identificadores
contenidos en un programa escrito en Java. Cada identificador en esta lista
debe estar acompa˜
nado de una lista con el número de
cada una de las l´ıneas en las que aparece. Por ejemplo, al proporcionar el
siguiente archivo como entrada al programa mencionado:
import [Link];
class PruebaLista {
public static void main(String [] pps) {
Lista lis = new Lista();
for (int i = 0; i < 10; i++)
33
34
CAP´
ITULO 2. LISTAS
[Link](new Integer(i));
Iterator i = [Link]();
while([Link]()) {
Integer entero = (Integer) [Link]();
if (([Link]()) %2 == 0)
[Link](entero);
i = [Link]();
while([Link]())
[Link]([Link]());
se debe producir el siguiente resultado:
java: 1,
util: 1,
Iterator: 10, 1,
PruebaLista: 3,
String: 4,
pps: 4,
Lista: 5, 5,
lis: 16, 14, 10, 8, 5,
i: 18, 17, 16, 12, 11, 10, 8, 7, 7, 7,
agregar: 8,
Integer: 12, 12, 8,
elementos: 16, 10,
hasNext: 17, 11,
entero: 14, 13, 12,
next: 18, 12,
intValue: 13,
eliminar: 14,
System: 18,
out: 18,
println: 18,
2.2.
El TAD Lista
La interfaz para el TAD Lista que se programará en este cap´ıtulo es la
siguiente: interface Listable {
public boolean estaVacia();
2.3. APLICACIONES DE LISTAS
35
public void vaciar();
public void agregar(Object elem);
public boolean contiene(Object elem);
public Object primerElemento();
public void eliminar(Object elem);
public void sustituir(Object orig, Object nuevo);
public [Link] iterador();
Para cada una de estas operaciones se espera el siguiente comportamiento:
• agregar. Agrega a la lista el objeto especificado como parámetro, sin
importar la posición en que lo haga y sin importar si ya existe un objeto
igual a él en la lista. El tama˜
no de la lista crece en una unidad.
• contiene. Regresa true si el elemento está en la lista y false en otro caso.
Este método no cambia el estado de la lista.
• iterador. Devuelve un iterador sobre la lista.
• eliminar. Si el elemento existe en la lista lo elimina, en caso contrario no
hace nada.
Si la operación es exitosa se reduce el tama˜
no de la lista en una unidad.
• estaVacia. Devuelve true si la lista está vac´ıa y false en otro caso.
• primerElemento. Si la lista no está vac´ıa, devuelve el valor almacenado al
inicio de ella. El estado de la lista no cambia. En el caso de solicitar el
primer elemento de una lista vac´ıa se dispara la excepción
NoSuchElementException.
• sustituir. Si el elemento a sustituir se encuentra en la lista lo sustituye por el
segundo parámetro, en caso contrario no hace nada. Esta operación no altera
el tama˜
no de la
lista.
• vaciar. Elimina todos los elementos de la lista. El tama˜no de la lista
después de esta operación es cero.
2.3.
Aplicaciones de listas
En esta sección se presentan algunos ejemplos de uso del TAD Lista, que es
el nombre de una clase que implementa la interfaz Listable definida en la
sección 2.2.
36
CAP´
ITULO 2. LISTAS
2.3.1.
Lista de identificadores
En esta sección se presenta una solución al problema de obtener la lista de
identificadores con su respectivo número de l´ınea planteado en la sección
2.1.
En la definición del problema se puede notar que se requiere una lista cuyos
elementos tienen dos componentes: el nombre de un identificador y una lista
para almacenar los números de l´ınea en que aparece tal identificador en el
archivo fuente. As´ı que se empezará por escribir esta clase, la cual está
formada por los atributos mencionados, as´ı como los constructores y
métodos para obtener cada elemento de la estructura.
Ejemplo 2.2. Clase para almacenar parejas de identificadores con una lista
de l´ıneas en donde aparece.
/**
* Clase para almacenar una pareja constituida por un nombre y una lista.
* @author Amparo López Gaona
* @version Abril 2011
*/
class IdyLista {
private String nombre;
private Lista lista;
/** Constructor que recibe una cadena
* @param id - Cadena que es el nombre del identificador
*/
public IdyLista (String id)
nombre = id;
lista = new Lista();
/** Constructor que recibe una cadena y una lista
* @param n - Cadena que es el nombre del identificador
* @param l - Lista asociada al nombre dado
*/
public IdyLista (String n, Lista l)
nombre = n;
lista = l;
/**
* Método para obtener el nombre del objeto
* @String nombre asociado al objeto
*/
public String obtenerNombre() {
return nombre;
2.3. APLICACIONES DE LISTAS
37
/**
* Método para obtener la lista del objeto
* @Lista -- lista del objeto
*/
public Lista obtenerLista() {
return lista;
Clase 2.1. Clase para crear la lista de identificadores con su respectiva lista
con números de l´ınea.
import [Link];
/**
* Clase para trabajar una lista donde cada elemento contiene a su vez una
lista.
* @author Amparo López Gaona
* @version
Abril 2011
*/
class ListaDeIds {
Lista ids;
/**
* Constructor por omisión
*/
ListaDeIds()
ids = new Lista();
}
M´
etodo 2.1. En el método insertar se verifica que el elemento no esté en la
lista, si es el caso se crea un objeto de la clase IdyLista con el nombre del
identificador y una lista con un elemento, que es el número de l´ınea en que
se encuentra el identificador. Si el identificador está en la lista sólo se agrega
al final de su lista asociada el número de l´ınea.
/**
* Método que permite insertar en una lista, cuyo nombre es dado, el
* elemento especificado
* @param identificador - nombre de la lista en que se hará la inserción
* @param linea - elemento que se insertará
*/
public void insertar(String identificador, Integer linea) {
boolean encontre = false;
Iterator it = [Link]();
38
CAP´
ITULO 2. LISTAS
while ([Link]() && !encontre) {
IdyLista id = (IdyLista) [Link]();
if ([Link]().equals(identificador)) {
Lista lis = [Link]();
[Link](linea);
encontre = true;
if (!encontre) {
Lista lis = new Lista();
[Link](linea);
[Link](new IdyLista(identificador, lis));
M´
etodo 2.2. Método para imprimir la lista. En este método se utilizan dos
iteradores anidados, el primero para moverse a lo largo de la lista de
identificadores; para cada uno se utiliza otro iterador para moverse en la lista
de números de l´ınea.
/**
* Método para imprimir la lista
*/
public void imprimir() {
for(Iterator it = [Link](); [Link](); ) {
IdyLista idLista = (IdyLista) [Link]();
[Link]([Link]() + ": "); Lista lista =
[Link]();
for (Iterator ilista = [Link](); [Link](); ) {
[Link]([Link]()+ ", ");
[Link]();
Clase 2.2. Clase para generar la lista de identificadores con su número de l
´ınea. Su trabajo consiste en crear una lista con las palabras reservas tomada
del primer parámetro del método main, abrir el archivo con el programa
fuente (segundo parámetro del método main), leerlo y mientras haya
palabras en él verificar que no sea una reservada, si es el caso insertarla en la
lista y por último imprimir tal lista.
/**
* Clase para generar un lista de palabras no reservadas cada una con una
lista
* con los números de l´
ınea en que aparece.
2.3. APLICACIONES DE LISTAS
39
* @author Amparo López Gaona
* @version Abril 2011
*/
class ListaPalabrasNoReservadas
public static void main (String [] pps) {
ListaDeIds lisId = new ListaDeIds();
int tipoTok;
if ([Link] != 2) {
[Link]("Forma de uso: ListaPalabrasReservadas
archivoFuente"); [Link](0);
leerReservadas(pps[0]);
try {
BufferedReader in = new BufferedReader (new FileReader(pps[0]));
StreamTokenizer tok = new StreamTokenizer(in);
[Link](’.’);
tipoTok = [Link]();
while (tipoTok != StreamTokenizer.TT_EOF) {
if (tipoTok == StreamTokenizer.TT_WORD)
if (! palRes([Link]))
[Link]([Link], new Integer([Link]())); tipoTok = [Link]();
[Link]();
} catch(IOException e) {
[Link]("Se genero IOException "+e);
En el cap´ıtulo anterior se utilizó la clase StreamTokenizar y se explicó su
uso. En esta ocasión basta decir que en esta clase se tiene el método lineno()
que devuelve el número de l´ınea en que se encuentra el elemento extra´ıdo
del flujo. Para conocer la documentación completa de esta clase véase el
apéndice E.
2.3.2.
Orden en recipientes
A continuación se plantea otro problema, se trata de un problema común en
la labor académi-ca. Suponer que se tiene registrada cierta cantidad de
alumnos que presentaron un examen.
40
CAP´
ITULO 2. LISTAS
Dicho examen cuenta con 100 preguntas. Conforme los alumnos terminan el
examen se califica y se registra en una lista el nombre del alumno, su
identificación y el número de aciertos obtenidos. Obviamente esta lista no
tiene un orden que facilite las búsquedas en ella. Puede haber muchos usos
para esta lista, en particular, se requiere determinar cuáles alumnos tuvieron
la mejor calificación.
Una solución al problema es ordenar la lista de alumnos por el número de
aciertos obtenidos. Para la implementación de la solución se puede usar
cualquier algoritmo de ordenamiento. Sin embargo, en esta sección se
presenta un algoritmo muy particular para cuando los elementos a ordenar
están en un rango de valores numéricos peque˜
no; este algoritmo utiliza
listas. El algoritmo utilizado es el llamado ordenamiento en recipientes ( bin-
sort ).
Si se sabe que el atributo por el cual se va a ordenar una serie de datos tiene
un rango de valores peque˜
no entonces se puede ordenar con el algoritmo siguiente:
• Crear un arreglo de recipientes ( bins), uno para cada valor del atributo por
el cual se va a ordenar. Lo recomendable es que cada recipiente se
implemente como una lista.
• Colocar cada elemento en el recipiente adecuado de acuerdo al valor por el
cual se va ordenar. Por ejemplo, colocar al alumno que obtuvo 100 aciertos
en la localidad 100
del arreglo, al que tuvo 48 en la 48, etc., con lo cual en cada localidad del
arreglo se tiene una lista con los datos de los alumnos que obtuvieron esos
aciertos.
• Recorrer nuevamente el arreglo de recipientes y extraer cada elemento de
la lista de cada localidad del arreglo y colocarlo en la lista final. Con esto se
tiene una lista ordenada por el atributo numérico especificado.
Este algoritmo es bastante eficiente sólo que está restringido a datos con las
caracter´ısticas mencionadas anteriormente. Si la lista original tiene n
elementos y hay m recipientes, el algoritmo es de orden O( n + m), si m < =
n se tiene O( n), pero si m > n entonces es de O( m) por lo cual la
complejidad del algoritmo es lineal.
A diferencia de otros algoritmos de ordenamiento, éste requiere memoria
adicional para cada recipiente y por lo tanto es buen ejemplo de equilibrio
entre espacio y rendimiento.
Ejemplo 2.3. A continuación se presenta una implementación del algoritmo
de ordenamiento en recipientes. Se asume que se tiene una clase Alumno
con los métodos necesarios para manipular sus atributos y en particular hay
uno obtenerAciertos que, como su nombre indica, permite conocer la
cantidad de aciertos que obtuvo el alumno en el examen.
/**
* Método para ordenar datos en un rango espec´
ıfico usando recipientes (binsort)
* @param datos -- lista con los datos a ordenar
* @param rango -- l´
ımite superior del rango de valores a tomar en cuenta.
*/
2.3. APLICACIONES DE LISTAS
41
public static void ordenar (Lista datos, int rango) {
if (rango <= 0) return;
Lista[] recipiente = new Lista[rango + 1];
for (int i = 0; i < rango; i++)
recipiente[i] = new Lista();
//Distribuye la información en los recipientes
[Link] it = [Link]();
while ([Link]()) {
Alumno alum = (Alumno) [Link]();
recipiente[[Link]()].agregar(alum);
[Link]();
//Recoge la información de los recipientes
for (int i = 0; i < rango; i++){
it = recipiente[i].iterador();
while ([Link]())
[Link]([Link]());
Al entrar al método ordenar los datos en la lista están desordenados y al salir
ya se encuentran ordenados, de acuerdo con los aciertos del alumno.
2.3.3.
Problema de Jos´
Se tiene a un grupo de soldados rodeado por el enemigo. Los soldados saben
que no pueden salvarse sin ayuda. Afortunadamente tienen un caballo, el
cual pueden usar para solicitar la ayuda necesaria (o escapar). El mensajero
tiene muchas posibilidades de salvarse, por lo que la elección queda a la
suerte. Para elegir al soldado que irá en el caballo a solicitar ayuda todos los
soldados forman un c´ırculo, colocan en un sombrero papeles cada uno con
el nombre de un soldado y otros con un número entero entre 1 y la cantidad
de soldados. Se toma del sombrero un número n y un nombre. Contando a
partir del soldado cuyo nombre se tomó del sombrero, el n-ésimo soldado
sale del c´ırculo. Se cierra nuevamente el c´ırculo y se empieza a contar
desde la persona que segu´ıa a la que salió, al llegar al n-ésimo lugar se saca
al soldado correspondiente. El proceso se repite hasta que quede un solo
soldado, quién es el que buscará ayuda.
Por ejemplo, si el número de soldados es 8 y son nombres son: Francisco,
Jorge, Luis, Sergio, Ricardo, Salvador, Diego y Mario y el número n es 4 y
se empieza a contar a partir de Francisco, el orden de eliminación de
soldados es Sergio, Mario, Ricardo, Jorge, Francisco, Luis, Diego y
Salvador. Es decir, el que irá a pedir ayuda es Salvador.
42
CAP´
ITULO 2. LISTAS
El problema consiste en determinar el orden en que los hombres son
eliminados y quién se salva; para hacerlo se recibe un número n, la cantidad
de soldados y el lugar que ocupa el soldado a partir de donde se empezará la
cuenta.
Este problema es una adaptación a una leyenda sobre el historiador Flavius
Josephus en el primer siglo de nuestra era. En la literatura existen varias
versiones de este problema además existe un algoritmo para determinar tanto
el número n, como la posición inicial para asegurar que un hombre particular
se salve.
Ejemplo 2.4. Clase que contiene un método para resolver el problema de
José. El método recibe una lista con el nombre de cada soldado, el tama˜
no de la lista y el número n para ir
eliminando a los soldados. En esta versión la posición inicial es siempre la
primera y muestra el orden en que se van descartando los soldados.
/**
* Método para resolver el problema de José
* @param listaPersonas -- Lista con los datos de las personas
* @param nPersonas -- entero con la cantidad de personas en la lista
* @param numero -- entero para saber cada cuántos se elimina una persona
*/
public void jose(Lista listaPersonas, int nPersonas, int numero) {
Iterator itr = [Link]();
Object soldadoFuera = null;
[Link]("Los soldados se descartaron en el siguiente orden");
while (nPersonas-- != 1) {
for (int i = 0; i < numero; i++) {
if (![Link]())
itr = [Link]();
soldadoFuera = [Link]();
[Link](soldadoFuera+ ", ");
[Link](soldadoFuera);
Ejemplo 2.5. Clase para probar la solución al problema de José. Este
programa recibe dos parámetros: el primero es la cantidad de soldados y el
segundo cada cuántos soldados se elimina alguno.
/** Clase para probar la solución al problema de José
* @author Amparo López Gaona
* @version Abril 2011
*/
2.4. IMPLEMENTACI ´
ON DE LISTAS
43
public class PruebaJose {
public static void main(String [] pps) {
int p1 = 0;
int p2 = 0;
Jose pp = new Jose();
try {
if([Link] != 2) {
[Link]("Usar: Jose NPersonas numero");
} else {
p1 = [Link](pps[0]);
p2 = [Link](pps[1]);
Lista listaPersonas = new Lista();
[Link] ("Se construirá la lista"); for(int i = 1; i <= p1; i++)
[Link](new String("soldado"+i)); for (Iterator itr =
[Link](); [Link](); ) [Link]([Link]()+" ");
[Link]();
jose(listaPersonas, p1, p2);
[Link]("\n\n y el ganador es " +
[Link]());
} catch(NumberFormatException e) {
[Link]("Forma de uso: Jose NPersonas numero");
En este programa se valida que haya dos parámetros y que sean enteros, si es
es el caso se crea un objeto de la clase Jose, se generan cadenas simulando
los nombres de los soldados, se llama a ejecutar el método jose y se muestra
el nombre del soldado elegido para ir a pedir ayuda.
2.4.
Implementaci´
on de listas
En la sección anterior se utilizó el TAD Lista desconociendo su
implementación, como debe ser. Es decir, los programas mostrados
funcionan correctamente siempre y cuando se cuente con una
implementación completa y funcional de la interfaz Listable.
44
CAP´
ITULO 2. LISTAS
La forma más sencilla de implementar una lista es mediante un arreglo, sin
embargo, es sabido que con un arreglo se debe conocer de antemano la
cantidad máxima de datos que se pueden almacenar en él. Para salvar esta
restricción se puede usar una lista ligada (figura 2.1.). Una lista ligada es una
agrupación de objetos denominados nodos, no necesariamente almacenados
en forma adyacente. Cada nodo contiene el elemento (o dato) y un enlace o
liga a su sucesor. El último elemento tendrá un enlace con valor null para
indicar que no tiene sucesor.
lista
e1
e2
e3
e4
e5
Figura 2.1. Lista ligada con cinco nodos.
Se dice que una lista ligada es una estructura dinámica, pues no se determina
su capacidad máxima al momento de su creación. Cada vez que se inserta un
elemento en una lista ligada, crece y si se eliminan elementos decrece.
Una lista ligada suele tener un principio y un fin, pero no se puede llegar en
tiempo constante a cualquier elemento por su posición. El acceso a los
elementos en la lista es secuencial, es decir, para acceder a cualquier
elemento es necesario recorrer secuencialmente los elementos en la lista
hasta llegar al elemento deseado.
Las listas ligadas, como se han descrito, pueden variar al implementarse, si
se decide tener un apuntador al inicio de la lista, un apuntador al final de la
lista, si la lista terminará con el valor null, como se mostró en la figura, o
bien con un nodo especial, si se usará una sola liga o bien dos. En esta
sección se presentan sólo un par de implementaciones; cada una de ellas se
puede usar para resolver los problemas presentados en la sección 2.3.
Clase 2.3. Implementación de la clase para los nodos de una lista ligada. La
estructura de cada nodo consta de un objeto de la clase Object, que es el dato
que almacena, esto con el propósito de hacerla genérica. El otro atributo de
los nodos es una referencia al siguiente nodo.
/**
* Clase para manejar los nodos de la lista.
* @author Amparo López Gaona
* @version abril 2011
*/
public class Nodo {
public Object elemento;
public Nodo sgte;
/**
2.4. IMPLEMENTACI ´
ON DE LISTAS
45
* Crea un nodo con elemento igual a valor y apuntando al vac´
ıo.
* @param valor el Objeto que es el valor de nodo
*/
Nodo(Object valor) {
this (valor, null);
/**
* Crea un nodo después del indicado, con elemento igual a valor.
* @param valor el Objeto que es el valor de nodo
* @param n el nodo anterior al recién creado
*/
Nodo(Object valor, Nodo n) {
elemento = valor;
sgte = n;
En esta clase se tienen varias cosas curiosas. La primera es que los atributos
no son privados.
En este caso se justifica tal decisión para facilitar la asignación de valor a los
atributos por la forma en que se trabajarán los mismos, como se verá más
adelante en la programación de algunos métodos.
La segunda es que se tiene una referencia a un objeto del tipo que se está
definiendo, pero es válido hacer esto. Al declarar un entero, por ejemplo int
entero; se está especificando que en la variable entero se tendrá cualquier
valor entero válido. Si la declaración es Object elemento; se está
especificando que en elemento se tendrá la referencia a un objeto de la clase
Object, es decir, la dirección de un objeto. Al declarar Nodo sgte se
especifica que en este atributo se tiene la referencia a un objeto de la clase
Nodo.
La tercera es que se asigna el objeto que se pasa como parámetro, no una
copia de él, y como se mencionó, esto puede traer problemas. No se puede
evitar este problema porque la clase Object no tiene implementado un
constructor de copia ni el método clone, pero es importante que el usuario
del TAD pase como parámetro una copia del objeto con el que se desea
trabajar no el objeto mismo para evitar posibles sorpresas desagradables.
Clase 2.4. Implementación de la interfaz Listable presentada al inicio de este
cap´ıtulo en la cual se tiene una referencia al inicio de la lista y el valor null
en el atributo sgte de un nodo especifica el final de la lista.
/** Implementación de la interfaz Listable.
* @author Amparo López Gaona
* @version abril 2011
*/
public class Lista implements Listable{
protected Nodo inicio;
46
CAP´
ITULO 2. LISTAS
M´
etodo 2.3. El constructor por omisión de la lista asigna el valor de null al
nodo inicial, denominado cabecera, es decir, se crea la lista vac´ıa, la cual se
representa gráficamente como se muestra en la figura 2.2.
/**
Construye la lista
*/
public Lista() {
inicio = null;
inicio
Figura 2.2. Lista vac´ıa.
M´
etodo 2.4. El método estaVacia determina si una lista está vac´ıa verificando
que el nodo inicial tenga como valor null.
/**
* Prueba que la lista esté vac´
ıa.
* @return true si está vac´
ıa y false en otro caso.
*/
public boolean estaVacia() {
return inicio == null;
M´
etodo 2.5. El método vaciar eliminar todos los elementos de la lista. Para
ello basta con asignar al nodo inicial el valor null con lo cual los nodos que
formaban la lista quedan inaccesibles y por lo tanto a disposición del
recolector de basura.
/** Crea una lista vac´
ıa.
*/
public void vaciar() {
inicio = null;
M´
etodo 2.6. El método primerElemento permite obtener el primer elemento de
una lista no vac´ıa. Para hacerlo toma el valor del atributo elemento del nodo
inicial.
/**
* Devuelve el primer elemento de la lista si no está vac´
ıa
* @throws NoSuchElementException -- en caso de no existir el elemento
* en la lista
**/
2.4. IMPLEMENTACI ´
ON DE LISTAS
47
public Object primerElemento() {
if (!estaVacia())
return [Link];
throw new NoSuchElementException("La lista no tiene elementos");
M´
etodo 2.7. El método contiene se utiliza para determinar si un objeto está en
la lista o no. Para su implementación se hace uso del método interno buscar
que recorre la lista hasta encontrar el dato y devuelve el nodo en que se
encuentra, si no lo encuentra devuelve null.1
/**
* Determina si un elemento está contenido en la lista.
* @param dato el elemento a buscar.
* @return boolean - true si el dato está en la lista y false en otro caso.
*/
public boolean contiene(Object dato) {
return buscar(dato) != null;
/*
* Devuelve la posición del nodo que contiene el dato buscado.
* @param dato el dato a buscar.
* @return un nodo;
null si el dato no se encuentra.
*/
private Nodo buscar(Object dato) {
Nodo pos = inicio;
while(pos != null && ) pos = [Link];
return pos;
M´
etodo 2.8. El método sustituir cambia el valor de un nodo. Este método
busca el primer nodo con el valor original, y si lo encuentra sustituye ese
valor por el nuevo. En caso de no encontrar el elemento dispara la excepción
NoSuchElementException.
/** Método para cambiar el valor de un elemento en la lista
* @param original -- elemento, dentro de la lista, que se desea cambiar
* @param nuevo -- elemento con el que se sustituye
* @throws NoSuchElementException -- en caso de no existir el elemento en
la lista
*/
1 Se utiliza el método equals, que debe estar sobreescrito en cada clase
cuyos objetos formarán la lista.
48
CAP´
ITULO 2. LISTAS
public void sustituir(Object original, Object nuevo) Nodo n = buscar
(original)
if (n != null)
[Link] = nuevo;
else
throw new NoSuchElementException("No se puede sustituir el objeto
"+objeto);
M´
etodo 2.9. El método agregar inserta un elemento al inicio de la lista,
haciendo uso del constructor de la clase Nodo.2 Independientemente de si la
lista está vac´ıa o no basta con actualizar el valor de inicio para que contenga
la referencia al nuevo nodo, y este nuevo nodo tendrá como elemento
siguiente el que hasta antes de su llegada era el nodo inicial (figura 2.3.).
Caso 1: Lista sin elementos
inicio
inicio
e1
Caso 2: Lista con elementos
e1
inicio
e2
e3
inicio
e2
e3
Figura 2.3. Inserción del elemento e1 en una lista.
/**
* Inserta el primer elemento de la lista.
* @param dato el dato a agregar.
*/
public void agregar(Object dato) {
inicio = new Nodo(dato,inicio);
M´
etodo 2.10. El método para eliminar un elemento de la lista busca el
elemento a eliminar y conserva la referencia al nodo anterior, pues ésta se
debe modificar para que apunte al nodo sucesor del nodo que se va a
eliminar, con lo cual el nodo a eliminar queda inaccesible y por lo tanto
eliminado (figura 2.4.).
2 Como es irrelevante el lugar en donde se agreguen los elementos a la lista,
por facilidad se decidió hacerlo al inicio de la lista.
2.4. IMPLEMENTACI ´
ON DE LISTAS
49
Antes de borrar e2
inicio
e1
e2
e3
e4
de borrar e2
Después
inicio
e1
e2
e3
e4
Figura 2.4. Supresión de un nodo de una lista.
/**
* Elimina la primera ocurrencia de un dato.
* @param dato -- el dato a eliminar.
*/
public void eliminar(Object dato) {
Nodo pos = inicio, anterior = null;
while(pos != null && ) {
anterior = pos;
pos = [Link];
if (pos != null) {
// Encontró el elemento
if(pos == inicio)
// Es el inicio de la lista
inicio = [Link]
else
[Link] = [Link];
Para terminar la implementación del TAD Lista para la interfaz Listable se
presenta la implementación del iterador.
M´
etodo 2.11. Método iterador, con la clase interna que implementa su
funcionamiento.
public [Link] iterador() {
return new MiIterador();
/**
* Clase que implementa el iterador
*/
50
CAP´
ITULO 2. LISTAS
private class MiIterador implements [Link] {
private Nodo posicion = inicio;
public boolean hasNext() {
return posicion != null;
public Object next() throws [Link] {
if (hasNext()) {
Object o = [Link];
posicion = [Link];
return o;
throw new [Link]();
public void remove() {
throw new IllegalStateException();
}
2.4.1.
Complejidad
En esta sección se presenta una tabla con la complejidad de cada método de
la implementación del TAD Lista que se presentó en la sección anterior.
Operación
Tiempo de ejecución
constructor
O(1)
agregar
O(1)
contiene
O( n)
eliminar
O( n)
estaVacia
O(1)
iterador
O(1)
hasNext
O(1)
next
O(1)
primerElemento
O(1)
sustituir
O( n)
vaciar
O(1)
Tabla 2.1. Tiempo de ejecución de los métodos de la clase Lista.
2.4. IMPLEMENTACI ´
ON DE LISTAS
51
Puede observarse que la complejidad de la mayor´ıa de los métodos es
constante, incluyendo la de agregar un elemento. Los métodos contiene,
eliminar y sustituir tienen complejidad lineal debido a que, en el peor caso,
se requiere recorrer toda la lista para poder hacer la tarea encomendada.
2.4.2.
Listas ligadas con una referencia al nodo final
Con la implementación antes vista se tiene que la operación de agregar, al
inicio de la lista, es de O(1), lo cual es inmejorable. Sin embargo, si lo
común es agregar elementos al final de la lista, con la implementación
presentada esta operación ser´ıa de orden n porque es necesario recorrer toda
la lista antes de llegar al final.
M´
etodo 2.12. Método para agregar un elemento al final de la lista.
/**
* Método para agregar un elemento al final de la lista.
* @param dato -- dato que se desea agregar
*/
public void agregarFinal(Object dato) {
if (estaVac´
ıa())
agregar(dato);
else {
for (Nodo p = inicio; [Link] != null; p = [Link])
//Encuentra el último nodo
[Link] = new Nodo(dato);
// Agrega el nuevo nodo
Para que este método también sea de orden constante es necesario incluir
una nueva referencia que esté siempre apuntando al final de la lista (véase
figura 2.5.).
inicio
fin
e1
e2
e3
e4
e5
Figura 2.5. Lista ligada con una referencia al inicio y otra al final.
Clase 2.5. Clase que implementa una lista ligada con dos referencias: una al
inicio y otra al final de la lista.
/**
* Lista ligada usando dos referencias: una al inicio y otra al final.
52
CAP´
ITULO 2. LISTAS
* @author Amparo López Gaona
* @version abril 2011
*/
public class Lista implements Listable {
private Nodo inicio;
private Nodo fin;
Ahora cambia la implementación de aquellos métodos en los que se requiere
que la referencia al final de la lista se˜
nale al nodo correspondiente. A continuación se muestran sólo los métodos
que cambian.
M´
etodo 2.13. Constructor por omisión. Inicializa tanto el nodo inicial como el
final con valor de null.
/**
Construye la lista
*/
public Lista() {
inicio = fin = null;
M´
etodo 2.14. El método para probar si una lista está vac´ıa. Ahora se debe
verificar que ambas referencias tengan valor null.
/** Prueba que la lista esté vac´
ıa.
* @return true si está vac´
ıa y false en otro caso.
*/
public boolean estaVacia() {
return inicio == null && fin == null;
M´
etodo 2.15. El método para eliminar todos los elementos de una lista asigna
el valor null a ambas referencias.
/** Crea una lista vac´
ıa.
*/
public void vaciar() {
inicio = fin =null;
M´
etodo 2.16. El método ultimoElemento permite encontrar el último elemento
de la lista.
Este método utiliza la referencia a dicho elemento.
/** Devuelve el último elemento de la lista
* @param Object -- último elemento de la lista, o null si está vac´
ıa
**/
public Object ultimoElemento() {
return (estaVacia()) ? null : [Link];
}
2.4. IMPLEMENTACI ´
ON DE LISTAS
53
M´
etodo 2.17. El método agregarAlInicio incluye actualizar la referencia al
último elemento en caso de que la lista esté vac´ıa antes de la operación para
que al final de la misma ambas referencias apunten al nodo insertado.
/**
* Inserta el primer elemento de la lista.
* @param dato el dato a agregar.
*/
public void agregarAlInicio(Object dato) {
if (estaVacia())
inicio = fin = new Nodo(dato);
else
inicio = new Nodo(dato,inicio);
M´
etodo 2.18. El método agregar ahora lo hace al final de la lista, para ello es
necesario actualizar la referencia al último elemento de la misma.
/**
* Inserta el elemento al final de la lista.
* @param dato el dato a agregar.
*/
public void agregar(Object dato) {
if (estaVacia())
inicio = fin = new Nodo(dato);
else
fin = [Link] = new Nodo(dato);
M´
etodo 2.19. El método eliminar funciona igual que en la implementación
anterior, excepto que si se elimina el último elemento es necesario actualizar
la referencia al final de la lista.
/**
* Elimina la primera ocurrencia de un dato.
* @param dato el dato a eliminar.
*/
public void eliminar(Object dato) {
Nodo pos = inicio, anterior = null;
while(pos != null && ! [Link]([Link])) {
anterior = pos;
pos = [Link];
if (pos == null) return;
// No lo encontró
54
CAP´
ITULO 2. LISTAS
if(pos == inicio) {
// Es el inicio de la lista
inicio = [Link];
if (inicio == null)
fin = null;
else {
[Link] = [Link];
if ([Link] == null)
fin = anterior;
M´
etodo 2.20. En esta implementación se incluye el método eliminarPrimero
para eliminar rápidamente el primer elemento de la lista, pues no se hace
ninguna búsqueda.
/**
* Elimina el primer nodo de la lista.
*/
public void eliminarPrimero() {
if (!estaVacia()) {
inicio = [Link];
if (inicio == null)
fin = null;
En esta sección se presentó una segunda implementación para el TAD Lista,
la cual incluye más métodos de los definidos en la interfaz Listable. Esto es
válido, pues la única restricción al implementar una interfaz es implementar
todos los métodos definidos en ella, pero pueden agregarse otros, como se
hizo en esta sección.
2.4.3.
Complejidad
En la tabla 2.2. se muestra la complejidad de los métodos del TAD Lista, en
el caso de tener una referencia al inicio y otra al final de la lista.
Puede observarse que la introducción de la otra referencia permite que la
complejidad de los métodos agregarAlInicio, eliminarPrimero y
ultimoElemento sea de orden 1, además de que reduce la complejidad del
método agregar, porque ahora es orden constante.
Otra posibilidad de implementación de las listas es utilizar en cada nodo dos
referencias: una en el siguiente nodo, como hasta ahora, y la otra en el nodo
anterior. La funcionalidad de las listas es casi la misma, excepto porque se
pueden recorrer en cualquier dirección con la misma facilidad. Se deja como
ejercicio al lector.
2.5. EJERCICIOS
55
Operación
Tiempo de ejecución
constructor
O(1)
agregar
O(1)
agregarAlInicio
O(1)
contiene
O( n)
eliminar
O( n)
eliminarPrimero
O(1)
estaVacia
O(1)
hasNext
O(1)
iterador
O(1)
next
O(1)
primerElemento
O(1)
tamanio
O(1)
sustituir
O( n)
ultimoElemento
O(1)
vaciar
O(1)
Tabla 2.2. Tiempo de ejecución de los métodos de la clase Lista con doble
referencia.
2.5.
Ejercicios
1. Programar una clase ListaExtendida que sea subclase de la clase Lista
presentada en este cap´ıtulo y que, además de los métodos heredados, tenga
los siguientes métodos:
• Un constructor que reciba como parámetro un objeto de la clase que
implementa la interfaz Coleccionable del ejercicio 4 del cap´ıtulo anterior y
cree una nueva lista que contenga todos los elementos de dicha Coleccion.
• Un constructor de copia. Es decir, la nueva lista creada por este constructor
deberá de ser idéntica a la lista pasada como parámetro, pero no deberá ser la
misma.
• intercambiar(ListaExtendida lis) el cual debe intercambiar, en tiempo
constante, todos los elementos de la lista que lo llame por los elementos de
la lista lis pasada como parámetro.
• concatenar(ListaExtendida lis) para agregar todos los elementos de la lista
pasada como parámetro al final de la lista que llama al método. Después de
haber ejecutado la operación, el método deberá vaciar la lista pasada como
parámetro.
¿Por qué es necesario vaciar la lista parámetro?
• unico() para eliminar las repeticiones de cualquier elemento. Por ejemplo,
si la lista contiene los valores: { 1, 2, 3, 3, 3, 1, 1, 4, 4, 5, 6, 7 } después de
56
CAP´
ITULO 2. LISTAS
llamar al método unico() la lista debe quedar como sigue: { 1, 2, 3, 4, 5, 6, 7
}.
• invertir() para invertir el orden de todos los elementos de la lista. Este
método puede ser recursivo. Por ejemplo, si la lista contiene los valores: { 8,
5, 7, 2, 3, 9 } después de llamar al método invertir() la lista debe contener: {
9, 3, 2, 7, 5, 8 }.
• repeticiones(Object o) para determinar el número de veces que aparece el
objeto o en la lista. Este método debe ser recursivo.
2. Programar una clase ListaOrdenada que sea subclase de Lista, en ésta los
elementos almacenados deben estar ordenados, para ello los elemento de la
lista deben implementar la interfaz Comparator y el constructor de la lista
debe recibir un comparador. En esta clase se debe incluir el método
fundir(Lista lis) para obtener una ListaOrdenada a partir de la intercalación
de los elementos de la lista, que lo llama con los de lis.
3. Crear una lista doblemente ligada que, como su nombre lo indica, está
formada por nodos que contienen una liga al siguiente elemento y una liga al
elemento anterior de la lista. Además de la información necesaria para el
trabajo de la lista, incorporar un cursor que permita recorrer la lista y
programar las siguientes operaciones:
• avanzar() para avanza el cursor al siguiente nodo, si lo hubiera, en caso
contrario se queda en el nodo actual.
• retroceder() para avanzar el cursor al nodo anterior si lo hubiera, en caso
contrario se queda en el nodo actual.
• irAlPrincipio() para llevar el cursor al primer elemento de la lista.
• irAlFinal() para llevar el cursor al último elemento de la lista. La
programación debe ser de tal manera que esta operación sea de orden O(1).
• eliminarActual() borra el nodo donde está situado actualmente el cursor.
• obtenerObjetoActual() devuelve el contenido del nodo al que hace
referencia el cursor.
• agregarAntes(Object dato) para agregar el contenido dato en un nodo
anterior a la posición actual referenciada por el cursor
• agregarDespues(Object dato) inserta el contenido dato después del nodo
refe-renciado por el cursor.
• estaAlFinal() devuelve true si el cursor está en el último nodo de la lista y
false en otro caso.
2.5. EJERCICIOS
57
• reemplazarActual(Object info) para modifica la información contenida en
el nodo actual.
• invertirLista() para invertir el orden de todos los nodos de la lista.
4. Escribir un programa que lea un archivo que tiene parejas (nombre-
alumno, curso) como sigue:
(Jorge Luis, Bases de Datos)
(Jorge Luis, Estructuras de Datos)
(Jorge Luis, Programación Java)
(Mar´
ıa, Estructuras de Datos)
(Mar´
ıa, Bases de Datos)
(Andrea, Bases de Datos)
(Jorge Luis, Cálculo)
(Mar´
ıa, Cálculo)
(Andrea, Estructuras de Datos)
y genere dos tipos de listas: una con la lista de los alumnos inscritos en cada
materia y otra en la cual se liste para cada alumno los cursos en los que está
inscrito. Como se muestra a continuación:
Jorge L:
Bases de Datos:
Bases de Datos
Jorge L
Estructuras de Datos
Mar´
ıa
Programación en Java
Andrea
Cálculo
Estructuras de Datos:
Mar´
ıa:
Jorge L
Estructuras de Datos
Mar´
ıa
Bases de Datos
Andrea
Cálculo
Programación en Java:
Andrea:
Jorge L
Bases de Datos
Estructuras de Datos
Cálculo:
Jorge L
Mar´
ıa
5. Desarrollar un programa para hacer conversiones de números de cualquier
base en otra.
Este programa debe incluir métodos para:
58
CAP´
ITULO 2. LISTAS
• Obtener el número binario de cualquier número entero positivo. En número
entero se divide entre 2 y el residuo es el primer d´ıgito a la derecha del
resultado binario, el cociente de la división se divide entre 2 y es el residuo
es el segundo d´ıgito de la derecha del resultado binario y as´ı sucesivamente
hasta que el resultado de la división es cero.
Se muestra un ejemplo en la siguiente figura:
División
35/2
17/2
8/2
4/2
2/2
1/2
Residuo
(34) = 0100011
10
Figura 2.6. Conversión de base 10 a base 2.
Utilizar una lista para almacenar el número binario. Este método puede
escribirse utilizando recursión.
• Obtener el número en cualquier base a partir de un número entero positivo
en base 10. Este método recibe el entero que debe convertir y la base a la
que se desea convertir.
• Obtener el número en base 10 de un número en cualquier base. Este
método recibe como parámetro una lista que contiene un número en
cualquier base y la base en la que está tal número.
6. Escribir un TAD para trabajar con polinomios de una variable de la forma
Pn( x) =
A 0 + A 1 x + A 2 x 2 + A 3 x 3 + ... + Anxn donde los coeficientes son reales.
Los métodos que debe incluir esta clase son:
(a) Un constructor por omisión para el polinomio 0 x 0
(b) Un constructor de copia.
(c) sumar(Polinomio p) para sumar dos polinomios.
(d) restar (Polinomio p) para restar dos polinomios.
(e) multiplicar(Polinomio p) para multiplicar dos polinomios.
(f) evaluar() para evaluar un polinomio.
(g) imprimir() para imprimir el polinomio.
(h) grado() para obtener el grado del polinomio.
(i) grado(int n) para obtener el grado del n-ésimo término.
(j) coeficiente(int n) para obtener el coeficiente del n-ésimo término.
Cap´ıtulo 3
Pilas
Una pila es una estructura de datos cuya única restricción está en la forma de
acceder a los elementos almacenados en ella, ya que tanto la entrada como la
salida de los datos es por un solo lugar. Aunque parece ser una restricción
muy fuerte, esta estructura es muy sencilla de implementar y sumamente
útil. En este cap´ıtulo se presenta el TAD Pila, algunas de sus aplicaciones y
una implementación con el análisis de la complejidad de los métodos que lo
forman.
3.1.
Introducci´
on
En ocasiones es necesario restringir la forma de acceso a los datos
almacenados en una estructura, por ejemplo, puede resultar útil que los datos
se recuperen en el orden inverso a como se almacenaron. Ejemplo de esto se
tiene al navegar en páginas web con las flechas que permiten moverse hacia
adelante y hacia atrás, siempre se regresa a la anterior y si se quiere desandar
el camino esto se hace exactamente a la inversa de como se recorrió el
camino. Para problemas como este existe el TAD Pila.
Salida
Entrada
D3
Tope
D2
D1
Figura 3.1. Pila con entrada y salida por arriba.
Una pila es una estructura de datos con un solo lugar de acceso a ellos
(figura 3.1.), debido a esto el último elemento en almacenarse es el primero
en utilizarse, de ah´ı que las pilas también se conozcan como LIFO ( last in,
first out ).
59
60
CAP´
ITULO 3. PILAS
Como puede deducirse, los datos no se almacenan en posiciones
determinadas. El lugar por el que realizan la entrada y salida de los datos se
conoce como tope de la pila. No hay restricción en cuanto a la duplicidad o
no de los datos. Ejemplo de una pila en la vida cotidiana son las pilas de
charolas en una panader´ıa, donde sólo se puede tomar la charola que está
arriba de la pila y si se quiere depositar una charola se debe hacer sobre las
demás.
3.2.
El TAD Pila
La interfaz para el manejo del tipo de datos pila es la siguiente: interfaz
Apilable {
public boolean estaVacia();
public void vaciar();
public void push(Object);
public Object pop();
public Object top();
public int tamanio();
public [Link] iterador();
Las operaciones básicas con las pilas son:
• estaVacia. Devuelve true si la pila está vac´ıa y false en otro caso.
• vaciar. Elimina todos los elementos de la pila, es decir, deja vac´ıa la pila.
El tama˜no de la pila después de esta operación debe ser cero.
• push. Inserta un elemento en la parte de arriba de la pila, siempre y cuando
haya espacio para él. Con esta operación el tama˜
no de la pila se incrementa en una unidad.
• pop. Devuelve el último elemento insertado en la pila y lo elimina de ella.
Si la pila está vac´ıa devuelve el valor null. En caso de que la pila no esté
vac´ıa el tama˜
no de la
pila se decrementa en una unidad.
• top. Regresa el valor del último elemento que se insertó en la pila o bien
null si ésta se encuentra vac´ıa. Con este método no se altera el estado de la
pila.
• iterador. Regresa un iterador sobre la pila.
Puede apreciarse que son pocas las operaciones con pilas, sin embargo son
muy útiles, como se verá en la siguiente sección.
3.3. APLICACIONES DE PILAS
61
3.3.
Aplicaciones de pilas
En esta sección se presentan algunos ejemplos de aplicación de pilas como
son la simulación de los movimientos en las torres de Hanoi, evaluación de
expresiones aritméticas y encontrar la salida de un laberinto.
3.3.1.
Torres de Hanoi
Un ejemplo de uso de pilas se encuentra en el siguiente pasatiempo en el que
se tienen tres palos o postes, en el primero de ellos se tienen n discos de
diferente diámetro acomodados por diámetro de mayor a menor. Los otros
dos palos están vac´ıos (figura 3.2.). El problema consiste en pasar los discos
del primer poste al segundo, usando el tercero como auxiliar.
Las reglas que deben seguirse son que cada movimiento involucra sólo a un
disco que puede colocarse sobre otro disco de diámetro mayor o en un palo
vac´ıo.
Figura 3.2. Torres de Hanoi.
Para facilitar la expresión de la solución al problema los discos se identifican
con un número entero de 1 a n que corresponde al diámetro del disco y los
postes se identifican con las letras A, B y C. Si la cantidad de discos es 3, la
solución al problema consta de los siguientes pasos: Mover el disco 1 del
palo A al B
Mover el disco 2 del palo A al C
Mover el disco 1 del palo B al C
Mover el disco 3 del palo A al B
Mover el disco 1 del palo C al A
Mover el disco 2 del palo C al B
Mover el disco 1 del palo A al B
Se puede observar que para 3 discos se requieren 7 movimientos. En general
para n discos se requieren 2 n − 1 movimientos. Existe una leyenda que dice
que en la ciudad de Hanoi viven unos monjes dedicados a resolver el
problema con 64 discos. De ah´ı que este problema se conoce como el
problema de las torres de Hanoi. Si mover cada disco toma un d´ıa, entonces
resolver el problema tomará 264 − 1 d´ıas, sin hacer ningún movimiento
erróneo, que supone una cantidad considerable de a˜
nos. La leyenda dice que cuando resuelvan el problema el mundo terminará.
62
CAP´
ITULO 3. PILAS
Ejemplo 3.1. Programa que resuelve el problema de las torres de Hanoi.
Para la solución programada de este problema cada palo se representa como
una pila, puesto que sólo hay un punto de acceso, y cada disco por un
número entero que simboliza el diámetro del disco.
/**
* Clase para resolver el problema de las torres de Hanoi
* @author Amparo López Gaona
*/
public class Hanoi {
private Pila[] palo;
// Arreglo de palos
private final int nDiscos;
// Cantidad de discos
/**
Constructor por omisión, crea 3 discos.
*/
public Hanoi () {
this (3);
/**
Constructor que crea la candidad de discos especificada
@param int -- cantidad de discos a crear
*/
public Hanoi(int n) {
[Link]("Torres de Hanoi");
nDiscos = n;
palo = new Pila[4];
//Crea los tres postes
for (int i = 1; i < 4; i++)
palo[i] = new Pila();
// coloca los discos en el primer palo
for (int i = nDiscos; i > 0; i--)
palo[1].push(new Integer(i));
[Link]("Las torres inicialmente tienen "); pinta();
// Muestra el contenido de las torres
solucion(nDiscos, 1, 2, 3);
// Ahora resuelve le problema
/**
* Metodo para mostrar el contenido de cada palo
*/
public void pinta () {
[Link] it1 = palo[1].iterador();
3.3. APLICACIONES DE PILAS
63
[Link] it2 = palo[2].iterador();
[Link] it3 = palo[3].iterador();
[Link]("Palo1 \t\t Palo2 \t\t Palo3"); for(int i = 0; i < nDiscos;
i++) {
if ([Link]())
pintaDisco(((Integer)[Link]()).intValue());
else [Link]("
\t\t");
if ([Link]())
pintaDisco(((Integer)[Link]()).intValue());
else [Link]("
\t\t");
if ([Link]())
pintaDisco(((Integer)[Link]()).intValue());
[Link]();
/* Método auxiliar para mostrar un disco pintando "su diametro" de estrellas
*/
private void pintaDisco(int n){
for (int i=0; i < n; i++)
[Link]("*");
[Link]("\t\t");
/* Método que resuelve el problema */
private void solucion (int n, int p1, int p2, int p3) {
if (n > 0) {
solucion(n-1, p1, p3, p2);
Object disco = palo[p1].pop();
palo[p2].push(disco);
pinta();
solucion(n-1, p3, p2, p1);
La solución al problema consiste en pasar primero el disco1 del palo1 al
palo2. Una vez hecho esto, el problema se reduce a pasar los n − 1 discos
restantes del palo1 al palo3
y finalmente regresar esos n − 1 discos del palo3 al palo2. La complejidad
del algoritmo está determinada por la cantidad de movimientos requeridos,
de ah´ı que este algoritmo sea de O(2 n).
64
CAP´
ITULO 3. PILAS
Ejemplo 3.2. La salida del programa para 3 discos se muestra a
continuación; puede verse el comportamiento como pila de cada palo.
Torres de Hanoi
Las torres inicialmente tienen
Palo1
Palo2
Palo3
**
***
Palo1
Palo2
Palo3
**
***
Palo1
Palo2
Palo3
***
**
Palo1
Palo2
Palo3
***
*
**
Palo1
Palo2
Palo3
***
**
Palo1
Palo2
Palo3
***
**
Palo1
Palo2
Palo3
**
***
Palo1
Palo2
Palo3
**
***
3.3.2.
An´
alisis y evaluaci´
on de expresiones
Las pilas son ampliamente utilizadas en los traductores de lenguaje de alto
nivel a código ejecutable. En esta sección se presentan dos ejemplos de ello.
En el primero se muestra la
3.3. APLICACIONES DE PILAS
65
forma de determinar, usando una pila, si una expresión que contiene
paréntesis los tiene balanceados. El segundo ejemplo consiste en evaluar
una expresión aritmética.
Balanceo de elementos
Un problema común en los traductores es determinar si una expresión
aritmética tiene bien anidados sus paréntesis. Para ello se debe verificar que
haya tantos paréntesis de apertura, o del lado izquierdo, como paréntesis de
cerrado, o lado derecho. Además, en ningún momento puede haber más
paréntesis derechos que izquierdos. Descrito as´ı puede parecer complicado
de resolver, pero la solución se simplifica si se utiliza una pila en la que se
van metiendo los paréntesis izquierdos y cada vez que se encuentra uno
derecho se saca de la pila. Si se termina de recorrer la expresión y la pila
está vac´ıa, entonces la expresión tiene sus paréntesis balanceados. En
cualquier otro caso ocurre un error.
Esta solución puede generalizarse al considerarse tres tipos de paréntesis:
los tradicionales, los cuadrados o corchetes y las llaves. En este caso se
requiere que no se tengan más paréntesis de un tipo que de otro y que
cierren adecuadamente. Por ejemplo, (a+[b*c]/[d-h])*25
es correcta
{a+{b+c+{d+e}+e}}
es correcta
{a+{b+c+{d+e}+e}
es incorrecta
((a+b}
es incorrecta
La solución al problema se tiene al seguir los pasos del siguiente algoritmo:
1. Crear una pila vac´ıa.
2. Leer cada elemento de la expresión, para cada uno hacer: (a) Si es un
paréntesis que abre "(", "[" o bien " {", meterlo a la pila.
(b) Si es un paréntesis que cierra y la pila está vac´ıa, reportar que la
expresión no tiene balanceados sus paréntesis, pues hay más del lado
derecho que izquierdo.
(c) Si es un paréntesis que cierra y la pila no está vac´ıa, sacar de la pila el
carácter y verificar que corresponda con el s´ımbolo que abre. Si no es as´ı
reportar un error.
(d) Ignorar cualquier otro elemento.
3. Si la pila no está vac´ıa reportar un error.
Ejemplo 3.3. Programa para determinar si una expresión tiene balanceados
sus paréntesis.
/** Programa para verificar que una expresión tiene balanceados sus
paréntesis
* @author Amparo López Gaona
*/
66
CAP´
ITULO 3. PILAS
public class Parentesis {
Pila pila = new Pila();
// Pila para guardar los paréntesis de apertura
public static void main (String [] args) {
if ([Link] != 1) {
[Link]("Forma de uso: java Parentesis expresion");
} else {
new Parentesis(pps[0]);
/*
Método para determinar que una expresión tiene balanceados sus
* paréntesis
* @param String -- cadena a examinar
*/
public void Parentesis (String linea) {
for (int i = 0; i < [Link](); i++)
if ([Link](i) == ’(’) [Link](new Character(’)’)); else if
([Link](i) == ’{’) [Link](new Character(’}’)); else if
([Link](i) == ’[’) [Link](new Character(’]’)); else if
([Link](i) == ’)’) verifica(’)’);
else if ([Link](i) == ’}’) verifica(’}’);
else if ([Link](i) == ’]’) verifica(’]’);
if ([Link]())
[Link]("Parentesis balanceados"); else
[Link]("Parentesis NO balanceados");
/* Método privado que verifica los paréntesis
Recibe un paréntesis de cerrado y verifica que en el tope de la
* pila esté el equivante de apertura.
*/
private void verifica (char c) {
if ([Link]()) {
[Link]("Parentesis NO balanceados");
} else {
Character s = [Link]();
if (c != [Link]())
[Link]("Parentesis NO balanceados");
3.3. APLICACIONES DE PILAS
67
El ejemplo se mostró con expresiones aritméticas, pero podr´ıa ser más
amplia la “expresión”
que se desee validar. Por ejemplo, las llaves en un programa en Java.
También con esta misma idea se pueden solucionar otros problemas
parecidos, por ejemplo, verificar que si en un programa se tienen
instrucciones if anidadas éstas estén anidadas correctamente, o en
documentos escritos en XML verificar que las etiquetas estén bien
anidadas.
Evaluaci´
on de expresiones aritm´
eticas
Si se tiene la expresión A+B se piensa “sumar A con B”, es decir, primero
se piensa la operación y luego los operandos. La notación usual se conoce
como infija, en ella cada operador aparece entre sus operandos, por
ejemplo: A+B. Existen otras formas de representar las expresiones
aritméticas, una de ellas se conoce como prefija, en la cual los operadores
se colocan antes de los operandos, por ejemplo, +AB. La notación postfija
es otra forma de representar expresiones aritméticas, en ésta los operadores
van después de los operandos, por ejemplo, AB+.
Las siguientes son ejemplos de expresiones en notación infija y en postfija:
Infija
Postfija
A+B*C
ABC*+
(A+B)*C
AB+C*
A+B-C
AB+C-
(A+B)*(C-D)
AB+CD-*
((A+B)*C-(D-E))ˆ (F+G)
AB+C*DE–FG+Ûna expresión en notación infija puede prestarse a
ambigüedades, por ejemplo, la expresión a+b*c puede interpretarse como
(a+b)*c o bien como a+(b*c), para resolver este problema se asigna
prioridad a los operadores y/o se incluyen paréntesis como se acaba de ver.
Las expresiones en notación prefija y postfija no presentan la ambigüedad
explicada antes, por lo tanto las expresiones en esta notación no requieren
de paréntesis.
Una vez que se tiene una expresión aritmética en notación postfija su
evaluación es muy sencilla, y también requiere de una pila. El algoritmo es
el siguiente: 1. Iniciar con una pila vac´ıa
2. Leer cada elemento de la expresión. Para cada uno hacer: (a) Si el
elemento es un operando se mete a la pila.
(b) Si no es operando debe ser operador, pues en esta notación no hay
paréntesis. En este caso se sacan los dos elementos del tope de la pila; se
realiza la operación y el resultado de la misma se coloca en el tope de la
pila.
op2 = [Link]();
op1 = [Link](),
68
CAP´
ITULO 3. PILAS
valor = op1 simbolo op2;
[Link](valor);
Por sacar los operandos en orden contrario a como se metieron en la pila, el
primer elemento que se saca será el segundo operando y el primer operando
será el segundo elemento que se saca de la pila. Esto es importante sólo
para las operaciones aritméticas que no son conmutativas.
3. Finalmente se saca de la pila el resultado.
Por ejemplo, la expresión (3+4)*9 en notación postfija es 3 4 + 9 * y su
evaluación se realiza con los siguientes pasos (figura 3.3.):
1. Leer el 3 y colocarlo en la pila.
2. Leer el 4 y colocarlo en la pila.
3. Leer el signo de suma. Por ser un operador, se sacan los dos elementos
del tope de la pila y se realiza la suma 3+4.
4. Colocar el 7, resultado de la suma, en la pila.
5. Leer el 9 y colocarlo en la pila.
6. Leer el signo de multiplicación, entonces se sacan los dos elementos del
tope de la pila y se realiza la operación 7*9.
7. Colocar el 63, resultado de la resta, en la pila.
8. Como ya no hay más elementos en la expresión se saca el valor que está
en la pila y que es el resultado de la evaluación. Es decir, el resultado de
evaluar la expresión es 63.
Si la expresión que se desea evaluar es 3+(4*9) en notación postfija queda
como 3 4 9 * +
y para obtener el resultado se ejecutan los siguientes pasos (figura 3.4.): 1.
Leer el 3 y colocarlo en la pila.
2. Leer el 4 y colocarlo en la pila.
3. Leer el 9 y colocarlo en la pila.
4. Leer el signo de multiplicación entonces se sacan los dos elementos del
tope de la pila y se realiza la multiplicación 4*9.
5. Colocar el 36, resultado de la multiplicación, en la pila.
3.3. APLICACIONES DE PILAS
69
34+9*
34+9*
34+9*
34+9*
34+9*
3
7
op1 = ?
op1 = ?
op1 = ?
op1 = 3
op1 = 3
op2 = ?
op2 = ?
op2 = 4
op2 = 4
op2 = 4
34+9*
34+9*
34+9*
34+9*
63
9
7
63
op1 = 3
op1 = 3
op1 = 7
op1 = 7
op2 = 4
op2 = 9
op2 = 9
op2 = 9
Figura 3.3. Evaluación de la expresión (3+4)*9.
6. Leer el signo de suma entonces se sacan los dos elementos del tope de la
pila y se realiza la suma 36 + 3.
7. Colocar el 39, resultado de la suma, en la pila.
8. Como se ya no hay más elementos en la expresión se saca el valor que
está en la pila y que es el resultado de la evaluación. Es decir el resultado de
evaluar la expresión es 39.
M´
etodo 3.1. Método que implementa el algoritmo para la evaluación de
expresiones aritméticas en notación postfija.
/**
* Método que recibe una expresión en notación postfija y la evalúa.
* @param String -- cadena con la expresión en posfija
* @return double -- valor de la expresión
*/
public double calcular (String expresion) {
Pila pila = new Pila();
StringTokenizer tokenizer;
String token;
70
CAP´
ITULO 3. PILAS
349*+
349*+
349*+
349*+
349*+
9
4
op1 = ?
op1 = ?
op1 = ?
op1 = 4
op1 = ?
op2 = ?
op2 = ?
op2 = 9
op2 = 9
op2 = ?
349*+
349*+
349*+
349*+
36
36
39
36
39
op1 = 4
op1 = 3
op1 = 3
op1 = 3
op2 = 9
op2 = 36
op2 = 36
op2 = 36
Figura 3.4. Evaluación de la expresión 3+(4*9).
final String blanco = " \t";
final String operadores = "+-*/%^";
tokenizer = new StringTokenizer(expresion, blanco+operadores, true);
while ([Link]()) {
token = [Link]();
if ([Link](token) >= 0)
// Es un espacio en blanco, se ignora
else if ([Link](token) >= 0) {
// Es operador, evalua lo de arriba de la pila
double op1 = (Double) [Link]();
double op2 = (Double) [Link]();
double operacion = 0;
switch([Link](0)) {
case ’+’: operacion = op2+op1; break;
case ’-’: operacion = op2-op1; break;
case ’*’: operacion = op2*op1; break;
case ’/’: operacion = op2/op1; break;
case ’^’: operacion = [Link](op2,op1); break;
3.3. APLICACIONES DE PILAS
71
case ’%’: operacion = op2%op1; break;
[Link](new Double(operacion));
} else if([Link] ("[+-]?[0-9]+[.]?[0-9]*")){
[Link](new Double(token));
} else
[Link]("Elemento no válido");
return ((Double)[Link]()).doubleValue();
3.3.3.
Salida de laberintos
En esta sección se presenta el problema de salir de un laberinto, el cual
puede ser una abstracción de una serie de problemas por ejemplo, colocar
cableado para una red de computadoras, donde las paredes son lugares por
los que no se puede pasar.
Suponer que se tiene un laberinto cuadrado. En el lado inferior derecho se
encuentra la única entrada a él y la única salida está en el extremo superior
izquierdo. En su interior hay una serie de paredes que impiden el paso. El
problema consiste en determinar si hay salida o no y en caso de que exista
una salida mostrarla. La única manera de moverse dentro del laberinto es
desplazarse a una localidad adyacente ya sea arriba, abajo, a la derecha o a
la izquierda, es decir, no son válidos los movimientos en diagonal; estos se
pueden realizar siempre y cuando no exista una pared entre ambas casillas.
En la figura 3.5. se muestra un laberinto de 4 × 4, las l´ıneas continuas son
paredes y las punteadas son referencia para poder verificar la solución. Si el
laberinto es visto como una matriz, la secuencia de coordenadas que está a
la derecha del mismo representa los cuadros por los que se tiene que pasar
para poder alcanzar la salida. La entrada está en la posición (3 , 3) y la
salida en la (0 , 0).
(3,3), (3,2), (3,1), (3,0), (2,0), (1,0), (0,0)
Figura 3.5. Laberinto y su camino de salida.
Para la solución presentada al problema descrito en esta sección se requiere
de una pila para ir guardando las posiciones por las que se pasó y no se
siguió por un posible camino a
72
CAP´
ITULO 3. PILAS
partir de ah´ı. Al no encontrar la salida se deshace el camino y se regresa al
punto inicial del mismo, se toma un elemento de la pila que indica el inicio
de un nuevo camino. Un algoritmo para la solución del problema es el
siguiente:
1. Meter a la pila las coordenadas de la celda de entrada al laberinto.
2. Mientras haya elementos en la pila y no se haya encontrado la salida.
(a) Tomar de la pila las coordenadas de una celda.
Avanzar a esa celda en caso de que no se haya visitado antes y marcarla
como visitada para evitar regresar ah´ı.
Si la celda ya ha sido visitada regresar a la iteración.
(b) Colocar en la pila los vecinos adyacentes a los que puede ir a partir del
punto actual, es decir, aquellos hacia los que no hay pared ni han sido
visitados anteriormente.
(c) Si se llegó a la salida, se tiene una solución.
3. Si se han visitado todas las posiciones adyacentes y no se encuentra
trayectoria hacia la salida, entonces no existe salida del laberinto.
Un aspecto interesante en la solución de este problema es determinar la
representación del laberinto. Una posibilidad es hacerlo mediante la
codificación en binario presentada en
[Budd00]. En la figura 3.6. se muestran las diferentes posibilidades de
paredes para un cuadro o celda del laberinto con su codificación en decimal
y en binario. La codificación en binario está dise˜
nada de manera que un 1 significa la presencia de una pared y un 0 la
ausencia de la misma, y se van numerando las paredes de cada celda,
primero la izquierda (un uno en el bit más significativo), arriba (un uno en
el segundo bit más significativo), derecha (un uno en el segundo bit menos
significativo) y finalmente abajo (un uno en bit menos significativo). Por
ejemplo, el número 11 en decimal es 1011 en binario y significa que hay
pared a la izquierda, otra a la derecha y otra más abajo, sin embargo en la
parte de arriba hay camino libre.
En la figura 3.7. se muestra un laberinto y su representación utilizando el
código mostrado en la figura 3.6.
Ejemplo 3.4. Solución al problema de encontrar la salida de un laberinto, si
existe. En ella se tienen dos arreglos bidimensionales, uno llamado paredes,
en el cual están representadas las paredes del laberinto. El otro, llamado
visitado, es en el que se marca el orden en que se visitan las celdas y con lo
cual se representan los pasos a seguir para salir del laberinto.
/**
* Clase para encontrar la salida de un laberinto
* @author Amparo López Gaona
*/
3.3. APLICACIONES DE PILAS
73
0000
0001
0010
0011
0100
0101
0110
0111
10
11
12
13
14
15
1000
1001
1010
1011
1100
1101
1110
1111
Figura 3.6. Código para las celdas del laberinto.
S
12
12
10
11
Figura 3.7. Laberinto y su representación en decimal.
public class Laberinto {
private int largo;
private int ancho ;
private int [][] paredes;
private int [][] visitado;
M´
etodo 3.2. Constructor que lee del archivo pasado como parámetro primero
las dimensiones del laberinto y luego el código para las paredes del mismo.
Cada número debe estar separado por un espacio en blanco o en l´ınea
aparte. Este constructor también deja en cero la matriz visitado para poder
empezar a trabajar con ella.
/**
* Constructor que lee el laberinto de un archivo.
* @param nombre -- nombre del archivo de texto que contiene el laberinto
*/
public Laberinto (String nombreArch) throws IOException {
Scanner in = new Scanner(new File(nombreArch));
ancho = [Link]();
largo = [Link]();
74
CAP´
ITULO 3. PILAS
paredes = new int[ancho][largo];
visitado = new int[ancho][largo];
for (int i = 0; i < ancho; i++)
for (int j = 0; j < largo; j++) {
paredes[i][j] = [Link]();
visitado[i][j] = 0;
M´
etodo 3.3. El método encontrarSalida es el encargado de implementar el
algoritmo presentado en esta sección.
/**
* Método para encontrar la salida del laberinto
* @return boolean -- devuelve true si hay salida y false en otro caso
*/
public boolean encontrarSalida () {
Pila pila = new Pila();
[Link](new Punto(largo-1, ancho-1));
// Guarda la entrada
int contVisitas = 0;
while (! [Link]()) {
// Mientras la pila tenga elementos
Punto pto = (Punto) [Link]();
// Toma el elemento del tope
int x = [Link]();
int y = [Link]();
if (visitado[x][y] == 0) { // Si no está visistado
visitado[x][y] = ++contVisitas;
//se marca como visitado
if ([Link](salida))
return true;
// Se llegó a la salida
guardarVecinos(x, y, pila); // se guardan sus vecinos
return false;
//
No se puede llegar a la salida
M´
etodo 3.4. El método guardarVecinos busca las celdas adyacentes a las que
se puede ir desde la celda pasada como parámetro. Para encontrarlas se
utiliza una operación para trabajar con un número a nivel de bits. El
operador es el de conjunción que se representa con un solo s´ımbolo
ampersand (&), y funciona tomando cada vez un bit del entero que es el
primer operando y el correspondiente del entero que es el segundo
operando, tomando el valor uno como verdadero y el cero como falso. As´ı,
por ejemplo: 1001 & 1010 devuelve 1000. En el caso del laberinto se va a
realizar la operación con los números potencia de 2
para descubrir si hay pared o no de cierto lado. Al aplicar este operador con
el 1 y obtener
3.3. APLICACIONES DE PILAS
75
cero significa que no hay pared en la parte inferior. Si se aplica con un 2, es
decir con 10, y obtener un cero significa que no hay pared del lado derecho,
etcétera.
/*
* Método privado para guardar en la pila las posiciones vecinas a las
* que se puede acceder a partir del punto dado
* @param x -- coordenada x del punto dado
* @param y -- coordenada y del punto dado
* @param pila -- pila en la que se guardan los vecinos
*/
private void guardarVecinos (int x, int y, Pila pila) {
if ((paredes[x][y] & 1) == 0)
[Link](new Punto(x+1, y));
if ((paredes[x][y] & 2) == 0)
[Link](new Punto(x, y+1));
if ((paredes[x][y] & 4) == 0)
[Link](new Punto(x-1, y));
if ((paredes[x][y] & 8) == 0)
[Link](new Punto(x, y-1));
M´
etodo 3.5. Método para mostrar la salida del laberinto, si la hay.
/**
* Método para mostrar la salida del laberinto
*/
public void mostrar() {
[Link]("Laberinto original:"); for(int i = 0; i < largo; i++){
for (int j = 0; j < ancho; j++)
[Link](paredes[i][j]+ " ");
[Link]();
[Link]("\nSolución:\n");
for(int i = 0; i < largo; i++){
for (int j = 0; j < ancho; j++)
[Link](visitado[i][j]+ " ");
[Link]();
Ejemplo 3.5. Ejemplos de corridas del programa Laberinto
76
CAP´
ITULO 3. PILAS
Laberinto original:
12 4 12 7
8286
8 3 10 10
9 1 3 11
Solución:
13 0 5 6
12 0 4 3
11 0 7 2
10 9 8 1
Laberinto original:
14 13 5 6
9 5 6 14
4696
9 1 7 11
Solución:
7000
6540
0032
0001
3.4.
Implementaci´
on del TAD Pila
En esta sección se presenta una implementación de los métodos para la
interfaz Apilable presentada en la sección 3.1. Para esta implementación de
los métodos se emplean nodos ligados de la clase Nodo utilizada en la
implementación de listas.
Clase 3.1. Clase Pila que implementa la interfaz Apilable para crear el tipo
abstracto de datos Pila. La estructura de cada pila tiene un nodo, que es el
tope de ella, y un contador para saber la cantidad de elementos almacenados
en la lista.
/**
* Clase para el tipo abstracto de datos Pila
* @author Amparo López Gaona
*/
public class Pila implements Apilable {
private Nodo tope;
private int nDatos;
3.4. IMPLEMENTACI ´
ON DEL TAD PILA
77
M´
etodo 3.6. Constructor por omisión, asigna null en tope de la pila y cero al
contador de datos.
/**
* Construye la pila.
*/
public Pila() {
tope = null;
nDatos = 0;
M´
etodo 3.7. El método estaVacia verifica que el tope de la pila esté en null, si
es el caso devuelve true y en caso contrario devuelve false.
/**
* Verifica que la pila esté vac´
ıa.
* @return true si lo está y falso en otro caso.
*/
public boolean estaVacia() {
return tope == null;
M´
etodo 3.8. El método vaciar asigna null al tope de la pila con lo cual se
asegura que la pila está vac´ıa. También asigna cero al contador de datos.
/**
* Vac´
ıa una pila.
*/
public void vaciar() {
tope = null;
nDatos = 0;
M´
etodo 3.9. El método tamanio devuelve un entero que indica la cantidad de
elementos almacenados en la pila.
/**
* Método para conocer el tama~
no de una pila
* @return int -- cantidad de elementos en la pila
*/
public int tamanio() {
return nDatos;
78
CAP´
ITULO 3. PILAS
M´
etodo 3.10. El método top devuelve el elemento del tope de la pila, sin
alterar ésta. Si la pila está vac´ıa devuelve null.
/**
* Devuelve el elemento del tope de la pila (sin alterar ésta)
* o bien null si se encuentra vac´
ıa.
*/
public Object top() {
return (estaVacia()) ? null : [Link];
}
M´
etodo 3.11. El método pop devuelve el elemento del tope de la pila si la pila
no está vac´ıa, en otro caso devuelve null.
/**
* Extrae el elemento del tope de la pila.
* Devuelve null si la pila está vac´
ıa.
*/
public Object pop() {
if (estaVacia())
return null;
Object dato = [Link];
tope = [Link];
nDatos --;
return dato;
M´
etodo 3.12. El método push agrega un elemento a la pila.
/**
* Agrega un nuevo elemento en la pila.
* @param x el elemento a insertar.
*/
public void push(Object dato) {
tope = new Nodo(dato, tope);
nDatos++;
M´
etodo 3.13. El método iterador devuelve un iterador sobre la pila.
/** Iterador para conseguir todos los elementos de la pila sin alterarla.
*/
3.4. IMPLEMENTACI ´
ON DEL TAD PILA
79
public [Link] iterador() {
return new MiIterador();
private class MiIterador implements [Link] {
private Nodo posicion = tope;
public boolean hasNext() {
return tope != null;
}
public Object next() {
if (hasNext()) {
Object o = [Link];
posicion = [Link];
return o;
return null;
public void remove() {
throw new IllegalStateException();
El tiempo que toma cada una de las operaciones sobre pilas con la
implementación anterior se muestra en la tabla 3.1. Puede observarse que el
tiempo es constante en todos los métodos, es decir, no depende del tama˜
no de la pila. Con lo cual se tiene que es una estructura de datos con
operaciones muy rápidas.
Operación
Tiempo de ejecución
constructor
O(1)
estaVacia
O(1)
iterador
O(1)
hasNext
O(1)
next
O(1)
pop
O(1)
push
O(1)
tamanio
O(1)
top
O(1)
vaciar
O(1)
Tabla 3.1. Tiempo de ejecución de los métodos de la clase Pila.
80
CAP´
ITULO 3. PILAS
3.5.
Ejercicios
1. Escribir dos métodos para mostrar el contenido de una pila sin alterarla.
El primer método debe mostrar los elementos del tope de la pila hacia abajo
y el segundo del fondo de la pila hacia arriba.
2. Escribir un programa que lea palabras/frases y determine si son pal
´ındromas, es decir si se leen lo mismo al derecho que al revés. Para ello se
deben ignorar los espacios en blanco y los signos de puntuación. Por
ejemplo Isaac no ronca as´
ı es una frase
pal´ındroma. Palabras pal´ındromas son: reconocer, somos, radar.
3. Escribir un programa para convertir un número en base 10 a cualquier
base, utilizando una pila. Ayuda: recordar que para la conversión se requiere
hacer divisiones sucesivas.
4. Un bibliotecario tiene el siguiente problema: tiene un estante con tres
repisas. En dos de las repisas se tienen los tomos de una enciclopedia, sólo
que están desordenados. En la primera se tienen m tomos y en la segunda n.
Los tomos de la enciclopedia están numerados del 1 al m × n. Escribir un
programa para indicar el orden en que se deben colocar los libros en la
tercera repisa para que queden ordenados con la restricción de que para
sacar los libros de las repisas éstas se deben trabajar como pilas y la repisa
en la cual deben estar ordenados puede utilizarse como lista.
5. Escribir un programa para convertir una expresión aritmética en notación
infija a postfija.
El algoritmo de conversión empieza teniendo una pila vac´ıa y una
expresión, en notación infija, correcta.
(a) Leer un elemento.
(b) Mientras haya elementos, hacer los siguientes pasos: i. Si se tiene un
operando se coloca directamente en la salida.
ii. Si es el primer operador se debe colocar en la pila incluyendo un
paréntesis izquierdo.
iii. Si es un paréntesis derecho se sacan de la pila los operadores que ah´ı se
encuentren y se van colocando en la salida cada uno, hasta encontrar el
paréntesis izquierdo correspondiente, el cual se saca de la pila pero no se
coloca en la salida.
iv. Si es un operador diferente de paréntesis derecho, se sacan de la pila
todos los operadores de mayor o igual prioridad que el que se tienen en la
mano, excepto el paréntesis izquierdo. Al finalizar se coloca el operador en
la pila.
v. Leer el siguiente elemento.
(c) Se sacan de la pila todos los operadores.
3.5. EJERCICIOS
81
6. Se tienen dos pilas que contienen números enteros; los números en la
primera pila están ordenados en forma ascendente del tope hacia el fondo, y
los de la segunda están ordenados en forma descendente del tope hacia el
fondo. Utilizando el TAD Pila de este cap´ıtulo escribir un programa que
fusione ambas pilas en una tercera ordenada descendentemente desde el
tope hacia el fondo. Restricción: el problema debe resolverse utilizando tres
pilas.
7. Escribir un programa que efectúe operaciones de suma y resta de dos
números de más de 10 d´ıgitos, utilizando pilas. Ayuda: cada d´ıgito se
almacena en una localidad de la pila 8. Se tiene una lista con los datos de
los clientes de una compa˜
n´ıa de telefon´ıa celular,
los cuales pueden aparecer repetidos en la lista si tienen registrado más de
un número telefónico. La compa˜
n´ıa para su próximo aniversario desea enviar un regalo a sus clientes, sin
dar más de uno a cada cliente. Los regalos se encuentran almacenados en
una pila de regalos. Se requiere escribir un programa que permita generar
una nueva lista donde los clientes aparezcan sólo una vez con sus regalos
asignados.
9. En un almacén se guarda mercanc´ıa en contenedores. No es posible
colocar más de n contenedores uno encima del otro y no hay área para más
de m pilas de contenedores.
Cada contenedor tiene un número y un nombre de la empresa propietaria.
Escribir un programa que permita administrar el ingreso y salida de los
contenedores. Notar que para retirar un contenedor es necesario retirar los
contenedores que están encima de él y colocarlos en otra pila.
10. La mayor´ıa de los editores de texto permiten corregir lo tecleado en
una l´ınea utilizando la tecla de “backspace” para borrar el último carácter
teclado. Para borrar la l´ınea completa se utiliza alguna secuencia de teclas
que pueden empezar con la tecla < Ctrl > , por ejemplo, en el editor emacs
con < Ctrl > A y < Ctrl > K se borra toda la l´ınea.
El ejercicio consiste en escribir un programa para simular esta caracter
´ıstica de los editores. En la simulación se usará el guión (–) para borrar el
último carácter y el signo de pesos ($) para borrar desde el punto en donde
se está hasta el final de la l´ınea, y el signo de porcentaje ( %)—para borrar
toda la l´ınea desde el inicio. Ejemplos de uso: Dar una l´
ınea de texto: Pai-les$Eji-e%Pa3--iliy-lla
La l´
ınea editada es: Pililla
Otra vez (s/n)? s
Dar una l´
ınea de texto: AP$$-Estrr-ucturas- Linela--alez-s
La l´
ınea editada es: Estructuras Lineales
Otra vez (s/n)? n
Cap´ıtulo 4
Colas
En cap´ıtulos anteriores se presentaron el TAD Lista, el cual no impone
restricciones en cuanto al ingreso/egreso de sus elementos, pueden hacerse
en cualquier lugar, y el TAD
Pila, en donde sólo hay un punto de acceso para los datos, de hecho éste
último TAD se conoce como FIFO ( first in, first out ) por la forma de
manipulación de los datos. En este cap´ıtulo se presenta un TAD intermedio
entre las listas y las pilas, en el cual la forma de acceso a los datos es más
restringida que en las listas pero menos que en las pilas.
4.1.
Introducci´
on
Una cola es una estructura de datos que almacena una colección de
elementos sin restricción en cuanto al valor de los mismos, con o sin
repetición, sin embargo el acceso a ellos está restringido al permitir que se
inserten siempre por el mismo lado (denominado final o parte trasera de la
cola) y se eliminen por otro (denominado frente de la cola). Es decir, se
sacan en el mismo orden en que entraron. En ocasiones suele referirse a
ellas como estructuras FIFO ( first in, first out ). (figura 4.1.).
D4 D3 D2 D1
final
frente
Figura 4.1. Cola de datos.
En la vida diaria el comportamiento de esta estructura de datos se presenta
en las filas frente a las taquillas de venta de boletos para espectáculos, en el
banco, en el supermercado, en la caseta de cobro de las autopistas, etcétera.
Las personas son atendidas en el orden en que llegan a la cola. En
computación se usan mucho sobre todo para atender peticiones de algún
servicio compartido, como la impresora o el CPU. Por ejemplo, en sistemas
operativos para 83
84
CAP´
ITULO 4. COLAS
imprimir archivos se van formando los trabajos en una cola, denominada
cola de impresión, y una forma sencilla de atenderlos es en el orden de
llegada. También se utilizan para transferir datos de manera as´ıncrona (los
datos no se reciben necesariamente con la misma frecuencia que se env´ıan)
entre dos procesos, por ejemplo, archivos de entrada/salida, sockets, etc.
Otro ejemplo son los buffers de lectura, aqu´ı se van guardando los
caracteres tan pronto son tecleados por el usuario y se muestran en la
pantalla en el mismo orden en que fueron tecleados. Como estos hay varios
ejemplos.
4.2.
El TAD Cola
La interfaz para el tipo de datos Cola es la siguiente: interface Encolable {
public boolean estaVacia();
public void vaciar();
public int tamanio();
public void agregar(Object dato);
public void eliminar() ;
public Object tomar();
public [Link] iterador();
El comportamiento de los métodos de la interfaz Encolable debe ser el
siguiente:
• agregar. Inserta un nuevo elemento colocándolo al final de la cola. El
tama˜no de la cola aumenta en una unidad.
• eliminar. Si la cola no está vac´ıa elimina el elemento que está al inicio de
la misma.
El tama˜
no de la cola disminuye en una unidad.
• estaVacia. Permite saber si la cola está vac´ıa devolviendo true si no tiene
elementos y false en otro caso.
• iterador. Devuelve un iterador sobre la cola.
• tamanio. Devuelve la cantidad de elementos almacenados en la cola.
• vaciar. Vac´ıa el contenido de una cola. Si después de llamar a este método
se llama al método estaVacia se debe obtener true.
• tomar. Devuelve el valor del primer elemento de la cola, sin alterar el
estado de ésta.
4.3. APLICACIONES DE COLAS
85
4.3.
Aplicaciones de colas
En esta sección se presentan algunos ejemplos de aplicación de colas, como
son un problema en el área de sistemas operativos, otro de ordenamiento de
vagones en un tren de carga y una simulación en un laberinto.
4.3.1.
Uso en sistemas operativos
En un sistema operativo multitarea, el CPU es compartido entre múltiples
procesos. En un momento dado, sólo un proceso se está ejecutando y los
demás están durmiendo o están bloqueados. El tiempo del CPU es
administrado por un planificador ( scheduler ), el cual conserva todos los
procesos vigentes en una cola con el proceso activo al frente de la misma
(figura 4.2.).
Proceso en ejecución
A
Siguiente proceso
Figura 4.2. Cola de procesos.
Para utilizar de manera equitativa los recursos del sistema, en este caso el
uso del CPU, a cada proceso se le asigna una cantidad especifica de tiempo
de CPU denominada quantum.
Si el proceso está en ejecución y se le acaba su quantum o es suspendido se
coloca al final de la cola. El siguiente proceso en la cola, el que queda al
inicio, tiene ahora el derecho de usar el CPU a lo más durante el tiempo que
dure su quantum. A esta forma de trabajo se le denomina comúnmente
como planificación round-robin. En otras palabras, round-robin es un
método para seleccionar todos los elementos en un grupo de manera
ordenada y equitativa, normalmente comenzando por el primer elemento de
la cola hasta llegar al último y empezando de nuevo desde el primer
elemento (figura 4.3.).
El programa está compuesto por dos clases una llamada Proceso, la cual va
a simular un proceso como un objeto con nombre, identificador, estado y
tiempo requerido de CPU
para su ejecución, además de los métodos necesarios para manipular estos
atributos.1 La otra clase llamada PlanificacionDeProcesos es la clase desde
la cual se va a ejecutar el programa, solicitando al usuario la cantidad de
procesos que requiere ejecutar para realizar 1Esta clase se deja como
ejercicio al lector.
86
CAP´
ITULO 4. COLAS
Siguiente proceso
Tiempo n
D
C
Proceso en ejecución
Tiempo n+k
Figura 4.3. Planificación round-robin.
la prueba y el valor para el quantum. El tiempo para cada proceso se puede
generar con un número aleatorio o bien se puede solicitar también como
dato de entrada.
Ejemplo 4.1. En el método roundrobin se implementa el algoritmo de
roundrobin, éste toma una cola de procesos y un quantum y a partir de esto
organiza la ejecución de los procesos siguiendo el algoritmo esbozado en
párrafos anteriores.
/**
* método para despacho de procesos
* @param q -- quantum de cada proceso
* procesos -- Cola en que se almacenan los procesos en espera.
*/
private static void roundrobin(int q, Cola procesos) {
int tiempo = 0;
int t;
int resta;
[Link]("\n Proceso\tT. de Ejecución"); Iterator it =
[Link]();
// Muestra la cola de procesos activos
while ([Link]()) {
Proceso p = (Proceso) [Link]();
[Link]([Link]() + "\t" + [Link]());
[Link]("\n Proceso\tT. Restante\tTiempo de Salida"); while
(![Link]()) {
Proceso proceso = (Proceso) [Link]();
[Link]();
t = [Link]();
resta = t - q;
if (resta > 0) {
tiempo += q;
[Link](resta);
4.3. APLICACIONES DE COLAS
87
[Link](true);
[Link](proceso);
[Link]([Link]() + "\t" +
[Link]() +
"\t\t" + tiempo);
} else {
tiempo += q + resta;
[Link](0);
if ([Link]()){
[Link]([Link]() + "\t"
+ [Link]() + "\t\t" + tiempo); [Link](false);
Ejemplo 4.2. A continuación un ejemplo de la salida que se puede obtener
con el método anterior, al tener 10 procesos y un quantum de 30 unidades
de tiempo.
Nombre Proc T. de Ejecución
Proceso --> 1 65
Proceso --> 2 12
Proceso --> 3 83
Proceso --> 4 13
Proceso --> 5 6
Proceso --> 6 43
Proceso --> 7 45
Proceso --> 8 2
Proceso --> 9 87
Proceso --> 10 57
Proceso
T. Restante
Tiempo de Salida
Proceso --> 1
35
30
Proceso --> 2
42
Proceso --> 3
53
72
Proceso --> 4
85
Proceso --> 5
91
Proceso --> 6
13
121
Proceso --> 7
15
151
Proceso --> 8
153
Proceso --> 9
57
183
Proceso --> 10
27
213
Proceso --> 1
243
Proceso --> 3
23
273
88
CAP´
ITULO 4. COLAS
Proceso --> 6
286
Proceso --> 7
301
Proceso --> 9
27
331
Proceso --> 10
0
358
Proceso --> 1
363
Proceso --> 3
386
Proceso --> 9
413
4.3.2.
Acomodo de trenes
En esta sección se presenta otro problema de organización un poco más
complejo que el anterior.
Suponer que una compa˜
n´ıa de transporte tiene una serie de trenes de carga, a su vez, cada tren está
formado por varios vagones. Se tiene que entregar la carga en diferentes
estaciones a lo largo de cierto camino. Para simplificar, suponer que las
estaciones están numeradas del 1 al n de acuerdo al orden en que deben
visitarse. Los vagones se etiquetan de acuerdo con su destino, aunque no
necesariamente en orden secuencial. Para facilitar la entrega de la carga, los
vagones se deben acomodar en orden secuencial antes de partir de la
terminal de origen, as´ı se van dejando en el orden correcto cada vez. Para
hacerlo se tiene un área de maniobras compuesta por k pistas que funcionan
como colas. El problema consiste en describir los movimientos necesarios
para que, tomando como punto de partida la entrada de los vagones por la
entrada principal del área de maniobras en el orden en que están
originalmente en el tren, salgan en orden ascendente del 1 al n.
Un algoritmo para resolver el problema de reacomodo de los vagones de un
tren consiste en repetir los siguientes pasos hasta que se hayan acomodado
todos los vagones del tren, o bien se determine que no es posible
reacomodarlos.
1. Tomar el primer vagón del tren.
2. Si el vagón es el siguiente en el orden secuencial de los vagones de
salida: (a) Pasar de largo por el área de maniobras y acomodarlo en la
salida.
(b) Si es posible, sacar otros vagones del área de maniobras. Para ello se
revisa si al frente de alguna cola del área de maniobras se encuentra un
vagón etiquetado con el número consecutivo siguiente al vagón que se
acaba de sacar. Si es el caso, sacar-lo de la cola y colocarlo en la salida.
Hacer este paso tantas veces como sea posible.
3. Si no se puede hacer el paso anterior, meter el vagón a la pista (cola) del
área de maniobras en donde el número del último vagón sea menor que el
del vagón que se va a meter. Si no existe una cola con esta caracter´ıstica
colocarlo en una cola vac´ıa.
Si no se puede colocar en ninguna cola entonces no hay solución al
problema.
4.3. APLICACIONES DE COLAS
89
Ejemplo 4.3. Un programa en Java para el acomodo de trenes siguiendo el
algoritmo descrito anteriormente es el siguiente:
/**
Clase para acomodar los vagones de un tren en orden secuencial
utilizando colas
@author Amparo López Gaona
*/
public class Trenes {
private Cola[] pista;
// Pistas para maniobras
private int nVagones;
// Cantidad de vagones del tren
private int nPistas;
// Cantidad de pistas para maniobras
private int[] ordenInicial;
// Orden inicial de los vagones
private int[] ultimoVgnEnCola;
// Ultimo vagón en cada pista
M´
etodo 4.1. Constructor que recibe un arreglo de enteros con el orden inicial
de los vagones y la cantidad de pistas para maniobras.
/**
* Constructor que toma el orden
de los vagones antes del reacomodo y
* la cantidad de pistas para maniobras.
* @param ordenIni -- arreglo de enteros en que se especifica el orden
inicial de los vagones.
* @param numPistas -- cantidad de pistas en el área de maniobras
*/
public Trenes (int[] ordenIni, int numPistas) {
nVagones = [Link];
nPistas = numPistas;
pista = new Cola[nPistas + 1];
for (int i = 1; i <= nPistas; i++)
pista[i] = new Cola();
ultimoVgnEnCola = new int[nPistas + 1];
ordenInicial = new int[[Link]];
for (int i= 0; i < [Link]; i++)
ordenInicial[i] = ordenIni[i];
}
M´
etodo 4.2. Constructor por omisión, éste proporciona un orden inicial para
un tren de nueve vagones y trabaja con tres pistas en el área de maniobras
90
CAP´
ITULO 4. COLAS
/**
* Constructor por omisión.
*/
public Trenes() {
this(new int [] {3,6,9,2,4,7,1,8,5}, 3);
M´
etodo 4.3. Método para acomodar los vagones del tren según el algoritmo
descrito en esta sección.
/**
Método para acomodar los vagones del tren
@return boolean - devuelve true si es posible acomodarlos y false
*
en otro caso.
*/
public boolean acomodarVagones() {
int sgteVagonFuera = 1;
for (int i = 0; i < nVagones; i++){
if (ordenInicial[i] == sgteVagonFuera) { // Saca el vagón
[Link]("Mueve el vagón " + ordenInicial[i] +
"de la pista de entrada a la salida");
sgteVagonFuera++;
while (sacarDeAreaM(sgteVagonFuera)) {
//Sacar otros del área de maniobras?
sgteVagonFuera++;
} else if (!colocarEnAreaM(ordenInicial[i])) {
return false;
return true;
M´
etodo 4.4. El método sacarDeAreaM es un método auxiliar para sacar un
vagón del área de maniobras. Recibe el número que identifica el vagón que
se desea sacar y devuelve true si es posible hacerlo y false en otro caso.
private boolean sacarDeAreaM(int vagon) {
for (int i = 1; i < nPistas; i++)
if (!pista[i].estaVacia() &&
((Integer) pista[i].tomar()).intValue() == vagon) {
pista[i].eliminar();
4.3. APLICACIONES DE COLAS
91
[Link]("Mueve el vagón "+ vagon + " del campo de "
+"maniobra "+ i + " a la salida"); if(pista[i].estaVacia())
ultimoVgnEnCola[i] = 0;
return true;
return false;
M´
etodo 4.5. El método colocarEnAreaM es el empleado para colocar un
vagón en el área de maniobras. Recibe el identificador del vagón que se va
colocar.
/**
*
Método para colocar un vagón en el área de maniobras
@param - número del vagón que se desea colocar.
@return boolean - devuelve true si es posible colocarlo en alguna
de las pistas y false en otro caso
**/
public boolean colocarEnAreaM(int vagon) {
for (int i = 1; i < nPistas; i++)
if (pista[i].estaVacia() || vagon > ultimoVgnEnCola[i]) {
pista[i].agregar(new Integer(vagon));
ultimoVgnEnCola[i] = vagon;
[Link]("Mueve el vagón "+ vagon + " de la entrada al "
+ "área "+ i);
return true;
return false;
// No hubo lugar
}
Ejemplo 4.4. Un ejemplo del funcionamiento de este programa, utilizando
el constructor por omisión, es el siguiente:
Mover el vagón 3 de la entrada al área 1
Mover el vagón 6 de la entrada al área 1
Mover el vagón 9 de la entrada al área 1
Mover el vagón 2 de la entrada al área 2
Mover el vagón 4 de la entrada al área 2
Mover el vagón 7 de la entrada al área 2
Mueve el vagón 1 de la pista de entrada a la salida
Mueve el vagón 2 del campo de maniobra 2 a la salida Mueve el vagón 3
del campo de maniobra 1 a la salida Mueve el vagón 4 del campo de
maniobra 2 a la salida
92
CAP´
ITULO 4. COLAS
Mover el vagón 8 de la entrada al área 2
Mueve el vagón 5 de la pista de entrada a la salida
Mueve el vagón 6 del campo de maniobra 1 a la salida Mueve el vagón 7
del campo de maniobra 2 a la salida Mueve el vagón 8 del campo de
maniobra 2 a la salida Mueve el vagón 9 del campo de maniobra 1 a la
salida 4.3.3.
Salida r´
apida de laberintos
Se tiene el problema del laberinto presentado en el cap´ıtulo anterior, sólo
que esta vez se tienen dos necesidades extra: la entrada y la salida puede ser
en cualquier celda, y se desea la salida que pase por la menor cantidad de
celdas, es decir, la más rápida. Este problema es una simplificación de otros
problemas, por ejemplo, si se desea cablear un lugar se deben evitar algunos
obstáculos, como paredes o lugares por los por alguna razón no se deben o
pueden cablear, y además se requiere optimizar la cantidad de cable
utilizada.
La solución consiste en tomar la celda inicial y etiquetar sus vecinos con un
número 1, porque están a distancia uno de tal celda. Luego, tomar los
vecinos que están a distancia 2, es decir, aquellos que están a distancia uno
de los que están a distancia uno del original.
As´ı, hasta que al etiquetar las celdas se tiene que una de éstas es la celda de
salida. Una vez encontrada la salida, si la hay, se recorre el camino en
sentido contrario hasta llegar al inicio, y con eso se tiene el camino más
corto.
Por ejemplo, con el laberinto de la parte izquierda de la figura 4.4. que tiene
como entrada el (0,1) y salida el (3,1).2 El paso de marcado se ver´ıa como
se muestra en la parte derecha de la misma figura. En el primer paso de esta
etapa se marcan los vecinos que están a distancia 1, en este caso (0,0) y
(0,2) (recordar que no se permiten movimientos en diagonal).
Luego para cada una de estas celdas marcadas se localiza sus vecinos y se
indica que están a distancia dos. La celda (0,0) sólo tiene un vecino, el (1,0)
y los vecinos de la celda (0,2) son (0,3) y (1,2). Se procede igual hasta que
se encuentra la celda final. En la segunda etapa se determina la salida (0,1),
(0,0), (1,0), (2,0), (2,1)y (3,1). Notar que no necesariamente hay una salida
única porque al llegar a (1,0) hay dos alternativas al (2,0) y la (1,1). De ah´ı
que la segunda solución sea (0,1), (0,0), (1,0), (1,1), (2,1), (3,1).
En esta solución se utiliza una cola en el marcado de vecinos para
almacenar las celdas de las que no se han revisado sus vecinos y en la etapa
de marcar la ruta de regreso se utiliza una pila para guardar las celdas,
porque se guardan de la salida al inicio, es decir, en orden contrario a la
forma en que se requieren. Al final se sacan de la pila las celdas y se tiene
el camino del inicio al fin.
Ejemplo 4.5. Programa en Java con la solución al problema de encontrar la
salida rápida de un laberinto.
2 Se numera de izquierda a derecha y de arriba a abajo empezando en cero.
4.3. APLICACIONES DE COLAS
93
2
3
Figura 4.4. Marcado de celdas en un laberinto.
/**
* Clase para encontrar la salida rápida de un laberinto
* @author Amparo López Gaona
*/
public class Laberinto {
private int largo, ancho;
// Dimensiones del laberinto
private Punto inicio, fin;
// Puntos de entrada y salida del laberinto
private int [][] paredes;
// Laberinto
private int [][] camino;
// Matriz para marcado de celdas
M´
etodo 4.6. Constructor que crea un laberinto a partir de la matriz dada, el
punto de entrada y el de salida. Inicializa la matriz de marcado con cero.
/** Constructor que crea un laberinto a partir de la matriz dada, el punto
* de entrada y el de salida
* @param laberinto -- matriz de enteros que representa el laberinto
* @param entrada -- punto de entrada
* @param salida -- punto de salida
*/
public Laberinto (int [][] laberinto, Punto entrada, Punto salida) {
largo = [Link];
ancho = laberinto[0].length;
camino = new int[largo][ancho];
for (int i = 0; i < ancho; i++)
for (int j = 0; j < largo; j++) {
paredes[i][j] = laberinto[i][j];
camino[i][j] = 0;
inicio = new Punto(entrada);
fin = new Punto(salida);
94
CAP´
ITULO 4. COLAS
M´
etodo 4.7. Constructor que crea un laberinto a partir de la matriz dada en un
archivo, el punto de entrada y el de salida. En el archivo deben estar
primero las dimensiones de la matriz, luego las coordenadas del punto
inicial y las del final, y por último la representación del laberinto.
/** Constructor que crea un laberinto a partir de un archivo
* @param nombreArch -- Cadena con el nombre del archivo.
*/
public Laberinto (String nombreArch) throws IOException {
Scanner in = new Scanner (new File(nombreArch));
ancho = [Link]();
largo = [Link]();
paredes = new int[ancho][largo];
camino = new int[ancho][largo];
inicio = new Punto ([Link](), [Link]());
fin = new Punto ([Link](), [Link]());
for (int i = 0; i < ancho; i++)
for (int j = 0; j < largo; j++) {
paredes[i][j] = [Link]();
camino[i][j] = 0;
}
}
M´
etodo 4.8. Método para encontrar la salida del laberinto, primero marca los
vecinos y luego recorre el camino inverso para mostrar la salida.
/** Método para encontrar la salida.
* @return boolean -- Devuelve true si hay salida y false en otro caso.
*/
public boolean encontrarSalida() {
Punto punto = inicio;
Cola cola = new Cola();
boolean hayCamino = true;
boolean encontreSalida = false;
while(hayCamino && !encontreSalida) {
// Marcar los vecinos
if (etiquetarVecinos(punto, cola))
encontreSalida = true;
if ([Link]())
hayCamino = false;
// No hay camino
punto = (Punto)[Link]();
[Link]();
}
4.3. APLICACIONES DE COLAS
95
if (encontreSalida) {
// Recorrer el camino al revés para indicarlo
camino[[Link]()][[Link]()] = 0;
[Link]("Encontré la salida");
Pila caminoCorto = marcarCamino(); //Busca vecinos con trayectoria -1
mostrar(caminoCorto);
return true;
} else {
[Link]("NO hay solución");
return false;
M´
etodo 4.9. Método para etiquetar los vecinos, siempre que no haya pared ni
se encuentre con la salida, marca la distancia y lo agrega a la cola de
pendientes para marcar posterior-mente a sus vecinos.
/** Método para etiquetar los vecinos.
* @param Punto -- punto del cual se marcaran los vecinos
* @param Cola -- cola que guardara los vecinos pendientes de marcado
* @return boolean -- true si encuentra la salida y false en otro caso.
*/
private boolean etiquetarVecinos(Punto p, Cola cola) {
int x = [Link]();
int y = [Link]();
if ((paredes[x][y] & 1) == 0 && camino[x+1][y] == 0) {
camino[x+1][y] = camino[x][y] + 1;
if ([Link](new Punto(x+1, y)))
return true;
[Link](new Punto(x+1, y));
if ((paredes[x][y] & 2) == 0 && camino[x][y+1] == 0) {
camino[x][y+1]=
camino[x][y] + 1;
if ([Link](new Punto(x, y+1)))
return true;
[Link](new Punto(x, y+1));
if ((paredes[x][y] & 4) == 0 && camino[x-1][y] == 0) {
camino[x-1][y] =
camino[x][y] + 1;
if ([Link](new Punto(x-1, y)))
return true;
[Link](new Punto(x-1,y));
96
CAP´
ITULO 4. COLAS
if ((paredes[x][y] & 8) == 0 && camino[x][y-1] == 0) {
camino[x][y-1] =
camino[x][y] + 1;
if ([Link](new Punto(x, y-1)))
return true;
[Link](new Punto(x, y-1));
return false;
M´
etodo 4.10. Método para marcar el camino para salir del laberinto. Empieza
en la salida y va retrocediendo hasta encontrar la entrada. Cada celda por la
que pasa se guarda en una pila.
/*
* Método para marcar el camino para salir del laberinto.
* @return Pila -- pila en la que está el camino de salida del laberinto.
*/
private Pila marcarCamino() {
Pila ruta = new Pila();
int distancia = camino[[Link]()][[Link]()]; Punto punto = fin;
int x = [Link]();
int y = [Link]();
boolean heAvanzado;
[Link](fin);
while() {
heAvanzado = false;
if ((paredes[x][y] & 1) == 0 && camino[x+1][y] == distancia -1
&& !heAvanzado) {
punto = new Punto(x+1, y);
heAvanzado = true;
if (!heAvanzado &&(paredes[x][y] & 2) == 0 && camino[x][y+1] ==
distancia-1){
punto = new Punto(x,y+1);
heAvanzado = true;
if (!heAvanzado && (paredes[x][y] & 4) == 0 && camino[x-1][y] ==
distancia-1){
punto = new Punto(x-1,y);
heAvanzado = true;
if (!heAvanzado && (paredes[x][y] & 8) == 0 && camino[x][y-1] ==
distancia-1){
punto = new Punto(x, y-1);
4.4. IMPLEMENTACI ´
ON DEL TAD COLA
97
heAvanzado = true;
[Link](punto);
distancia --;
x = [Link]();
y = [Link]();
}
return ruta;
M´
etodo 4.11. Método para mostrar la salida del laberinto, sacando todos los
elementos de la pila.
/**
* Método para mostrar la salida del laberinto
* @param Pila - pila que contiene el camino de salida encontrado
*/
public void mostrar(Pila p) {
[Link]("El laberinto original tiene:"); for(int i = 0; i < largo;
i++){
for (int j = 0; j < ancho; j++)
[Link](paredes[i][j]+ " ");
[Link]();
[Link]("El camino es ");
[Link] it = [Link]();
while([Link]())
[Link]([Link]()+ " ");
}
}
4.4.
Implementaci´
on del TAD Cola
En esta sección se presenta una implementación de los métodos para la
interfaz Encolable presentada en la sección 4.1. Esta implementación de los
métodos se hace usando nodos ligados de la clase Nodo utilizada en la
implementación de listas presentada en el cap´ıtulo 2.
Clase 4.1. La clase Cola utiliza dos referencias, una al inicio para poder
tomar los elementos de ah´ı; y la otra al final para que entren los nuevos
elementos a la cola.
/**
* Implementación del TAD Cola usando nodos ligados
98
CAP´
ITULO 4. COLAS
* @author Amparo López Gaona
* @version Abril 2011
*/
public class Cola implements Encolable{
protected Nodo inicio;
protected Nodo fin;
protected int nDatos;
M´
etodo 4.12. Constructor por omisión. Asigna el valor null tanto al inicio
como al final de la cola.
/**
Constructor por omisión. Construye la cola vac´
ıa
*/
public Cola() {
inicio = null;
fin = null;
nDatos = 0;
M´
etodo 4.13. Método para determinar si una cola está vac´ıa, esto es, si el
inicio de la misma tiene como valor null.
/**
* Prueba que la cola esté vac´
ıa.
* @return true si está vac´
ıa y false en otro caso.
*/
public boolean estaVacia() {
return inicio == null;
M´
etodo 4.14. Método para eliminar todos los elementos de la cola, lo único
que hace es que las referencias al inicio y al final de la misma tengan valor
null.
/**
* Elimina todos los elementos de la cola.
*/
public void vaciar() {
inicio = fin = null;
nDatos = 0;
M´
etodo 4.15. El método tamanio devuelve un entero que indica la cantidad de
elementos almacenados en la cola.
4.4. IMPLEMENTACI ´
ON DEL TAD COLA
99
/**
* Método para conocer el tama~
no de una cola
* @return int -- cantidad de elementos en la cola
*/
public int tamanio() {
return nDatos;
M´
etodo 4.16. Método para agregar un elemento en la cola; éste siempre se
agrega al final de la misma.
/**
* Inserta un elemento en la cola
* @param dato - elemento que será insertado
*/
public void agregar(Object dato) {
if (inicio == null)
inicio = fin = new Nodo(dato);
else {
Nodo temp = new Nodo(dato);
[Link] = temp;
fin = temp;
nDatos++;
M´
etodo 4.17. Método para obtener el primer elemento de la cola. Siempre se
toma el elemento del inicio de la misma, sin alterar la cola.
/**
* Devuelve el primer elemento de la cola
* @return Object - elemento del inicio de la cola
*/
public Object tomar() {
if (inicio == null) return null;
//La cola está vac´
ıa
return [Link];
M´
etodo 4.18. Método para sacar un elemento de la cola. Toma el elemento
inicial, si lo hay.
/**
* Elimina el primer elemento de la cola
100
CAP´
ITULO 4. COLAS
*/
public void eliminar() {
if (inicio != null) {
inicio = [Link];
nDatos--;
M´
etodo 4.19. Método para obtener un iterador sobre los elementos de la cola.
Se deja como ejercicio su programación.
public [Link] iterador() {
return new MiIterador();
El tiempo que toma cada una de las operaciones sobre colas con la
implementación anterior se muestra en la tabla 4.1.
Operación
Tiempo de ejecución
constructor
O(1)
agregar
O(1)
estaVacia
O(1)
eliminar
O(1)
hasNext
O(1)
iterador
O(1)
next
O(1)
tamanio
O(1)
tomar
O(1)
vaciar
O(1)
Tabla 4.1. Tiempo de ejecución de los métodos de la clase Cola.
Debido a que se utiliza una referencia al inicio y otra al final de la cola, la
complejidad de todos los métodos presentados en la clase Cola es O(1), lo
cual es insuperable.
4.5.
Ejercicios
1. A˜
nadir a la clase Cola presentada en este cap´ıtulo métodos para: (a) Obtener
un iterador sobre la cola.
4.5. EJERCICIOS
101
(b) Obtener el último elemento de una cola.
(c) Producir una cola al revés. Por ejemplo, si la cola tiene A,B,C,D una
llamada a este método debe devolver una cola con los elementos D,C,B,A
en ese orden. Se permite utilizar alguna estructura vista en cap´ıtulos
anteriores.
(d) Concatenar dos colas, es decir, colocar los elementos de una cola al final
de la otra.
(e) Intercarlar los elementos de dos colas.
2. Escribir una implementación para el TAD Cola que, a diferencia de la
presentada en este cap´ıtulo, utilice un par de pilas.
3. Escribir una implementación para el TAD Pila que, a diferencia de la
presentada en el cap´ıtulo anterior, utilice un par de colas.
4. Una cola circular es aquella en la que ningún elemento apunta a null, en
lugar de eso el último elemento apunta al primero de la cola. El ejercicio
consiste en crear un TAD
ColaCircular para manejar colas circulares implementando la interfaz
Encolable descrita en este cap´ıtulo.
En este caso se requieren dos referencias, una apunta al primer nodo
disponible y la otra al primero ocupado (figura 4.5.). Al crear el primer
nodo, éste se apunta a s´ı mismo.
Cuando se inserta un nuevo elemento, la posición el primer elemento
disponible avanza y cuando un elemento es eliminado, la referencia al
primer elemento ocupado avanza. Si se intenta remover un elemento de una
cola vac´ıa se dispara una excepción. Al intentar insertar un elemento en
una cola llena en lugar de disparar una excepción se crea un nuevo
elemento.
primer lleno
primer desocupado
Figura 4.5. Cola circular.
Los métodos se deben comportar como sigue:
102
CAP´
ITULO 4. COLAS
• estaVacia Devuelve true si la cola se encuentra vac´ıa y false en otro caso.
La cola circular estará vac´ıa si todos los nodos que la conforman están vac
´ıos.
• vaciar. Método que limpia la cola circular dejándola vac´ıa. Este método
no deberá eliminar los nodos, solamente deberá dejarlos vac´ıos.
• agregar. Método que agrega, al final de la cola circular, el elemento
pasado como parámetro. Si la cola se encuentra llena se deberá crear un
nuevo nodo.
• eliminar. Método que elimina el elemento posicionado al inicio de la cola
circular.
Si la cola circular se encuentra vac´ıa deberá disparar una excepción.
• tomar. Método que devuelve el elemento posicionado al inicio de la cola,
sin modificar la cola.
• iterador. Método que devuelve un iterador con los elementos de la cola
circular.
5. Resolver el problema de José visto en el cap´ıtulo 2, utilizando una cola
circular.
6. Crear un TAD para manejar colas con doble salida, llamadas deque s. En
estas colas se permite que los elementos sean insertados y suprimidos en
ambos extremos. Por esto en ocasiones se le denomine bicola. Este TAD
puede crearse como una extensión de la clase Cola.
La clase Deque debe contener, además de los métodos para las colas, los
siguientes:
• agregarPrimero(Object elem) para insertar un elemento al inicio de la
cola.
• eliminarUltimo para eliminar el último elemento de la cola.
• ultimoElemento para obtener último elemento de la cola, sin eliminarlo.
• iteradorEnReversa para obtener un iterador que se desplace sobre la cola
de fin a inicio.
7. El gerente general de un complejo cinematográfico desea que la fila para
la compra de boletos se comporte como una cola de las presentadas en este
cap´ıtulo, con la excepción que quiere dividir a los compradores en dos
grupos: clientes ocasionales y clientes asiduos. También desea que los
clientes en la cola sean atendidos en el orden de llegada dentro de cada
grupo, pero siempre que haya clientes asiduos en la cola, éstos deben ser
atendidos antes que los ocasionales. El ejercicio consiste en definir un tipo
abstracto de datos para este tipo de colas con operaciones para: (a) crear una
cola vac´ıa,
(b) a˜
nadir un elemento nuevo a una cola,
(c) consultar el primer elemento de una cola,
4.5. EJERCICIOS
103
(d) eliminar el primer elemento de una cola,
(e) consultar el número de clientes ocasionales en una cola, (f) consultar el
número de clientes asiduos en una cola, (g) consultar si una cola está vac´ıa
o no está vac´ıa.
Para la programación del TAD se puede partir de cero o bien utilizar colas
del TAD
Cola, la única restricción es que las operaciones descritas sean de orden
constante.
8. Se tiene un almacén donde se encuentran las consolas de videojuegos
fabricadas en cierta planta. Al recibir las consolas se acomodan como van
llegando: las primeras al fondo del almacén y las últimas más cerca de, o en
la entrada del almacén. Los datos de cada consola de videojuegos son
código y descripción. El almacén dispone de una sola puerta atravesada por
una banda magnética por donde entran las consolas de videojuegos para
almacenarlas y salen las que se van a distribuir a las tiendas.
Adicionalmente, se tiene una cola de solicitudes de consolas de videojuegos
realizadas por las tiendas, donde aparece el nombre de la tienda y la
cantidad solicitada de consolas de videojuegos. Escribir un programa para
ayudar a asignar a cada tienda las consolas de videojuegos, generando una
nueva estructura que contenga la tienda y el código de las consolas de
videojuegos asignadas.
9. Un restaurante dispone de m mesas. Por cada mesa se tiene su código de
identificación y su capacidad en comensales. Hay una cola de espera para ir
ocupando las mesas, de forma que, para cada elemento, se sabe el nombre
de la persona que ha hecho la reservación y el número de comensales. El
ejercicio consiste en hacer un programa para asignar las mesas según las
reservaciones. Escribir la solución en dos versiones: una buscando la
primera mesa en la que caben los comensales y otra optimizando la
ocupación de las mesas.
Cap´ıtulo 5
Arboles
Las estructuras de datos hasta ahora vistas se distinguen entre s´ı en la
forma en que permiten introducir y retirar elementos de ellas. Estas
estructuras tienen en común el hecho de que siempre es posible conocer el
elemento que está antes o después de algún elemento dado, por eso se dice
que son estructuras lineales. En este cap´ıtulo se presenta una estructura de
datos que los organiza imponiéndoles un orden jerárquico. Esta estructura
se conoce como árbol.
En computación hay diferentes tipos de árboles, en este cap´ıtulo se
presentan los árboles en general, los árboles binarios, y dentro de éstos, los
árboles de búsqueda y los balanceados.
También se presentan algunas aplicaciones en las que se utilizan con
estructuras de datos presentadas en cap´ıtulos anteriores.
5.1.
Introducci´
on
Un árbol es una colección de nodos conectados por arcos, en la cual no hay
ciclos. En dicha colección existe un nodo distinguido llamado ra´ız y el
resto de los nodos, si los hay, se dividen en conjuntos ajenos llamados
subárboles. En todo árbol siempre hay un único camino de la ra´ız a
cualquier otro nodo. Ejemplos de árboles son: la tabla de contenido de un
libro, la de los torneos deportivos, los árboles genealógicos, la organización
de archivos en una computadora.
Las diferentes relaciones que existen entre los nodos de un árbol tienen un
nombre espec´ıfico. Los nodos directamente conectados a un nodo
particular son los hijos de ese nodo. Un nodo a es padre de un nodo b si
existe un enlace de a a b (en ese caso, también se dice que b es hijo de a).
Existe un solo nodo sin padres a partir del cual se forma el árbol, a éste se le
conoce como ra´ız. Un nodo que no tiene hijos se conoce como hoja o nodo
externo. Los nodos que tienen padre y al menos un hijo se denominan
nodos interiores. En los árboles se usa la misma terminolog´ıa que en las
familias para designar la relación entre nodos, as´ı, los hijos del mismo
padre se llaman hermanos, los nodos tienen padres, abuelos, etc. El grado
de un nodo es el número de hijos que tiene. El grado de un hoja es cero. El
grado de un árbol es el mayor grado que tiene sus nodos.
105
106
CAP´
ITULO 5. ´
ARBOLES
En la figura 5.1. se presenta un árbol con una estructura jerárquica de clases
en Java. En la figura, la ra´ız es el nodo etiquetado con la palabra Object,
las hojas son Ahorro, Nacional, Internacional, Cheque, DepositoException
y RetiroException. Notar que a diferencia de los árboles en el mundo real,
en computación la ra´ız queda arriba y las hojas abajo. Un subárbol es el
enmarcado en un cuadro con l´ıneas punteadas y formado por los nodos
Cuenta (ra´ız de este subárbol), Ahorro, Crédito, Cheque, Nacional e
Internacional.
Object
Cuenta
CuentaException
Ahorro
Credito
Cheque
RetiroException
DepositoException
Nacional
Internacional
Figura 5.1. Ejemplo de un árbol.
En los árboles se utiliza término nivel. Por definición la ra´ız del árbol está
en el nivel 1 y cualquier nodo está a un nivel más que el de su padre. As´ı
los hijos de la ra´ız están en el nivel 2, sus nietos en el 3, etc. Los ancestros
de un nodo n son todos los nodos en los niveles inferiores conectados a n,
de manera directa o indirecta. Los descendientes de un nodo son todos los
nodos de niveles superiores conectados a él. La altura de un árbol es el
número máximo de niveles que tiene.
El árbol de la figura 5.1. tiene una altura de 4. Los ancestros de Ahorro son
Cuenta y Object. Descendientes de Cuenta son Ahorro, Crédito, Cheque,
Nacional, Internacional.
Los nodos internos son Object, Cuenta, CuentaExcepcion y Crédito.
Ejemplo 5.1. En los analizadores sintácticos se utilizan árboles para revisar
la sintaxis. Por ejemplo, si se tiene la siguiente gramática para expresiones
condicionales:
<proposición> ::= <selección> | <expr>
<selección>
::= if ( <exp> ) <proposición> else <proposición>
<expr>
::= <relacional> | <asignación> | identificador
<relacional>
::= <expr> < <expr>
<asignación>
::= <expr> = <expr>
y se desea determinar si la instrucción: if (a <b) max = b else max = a es
correcta sintácticamente, se construye un árbol como el que sigue:
5.2. ´
ARBOLES BINARIOS
107
proposición
selección
if
(
expresión
prop
else
prop
relacional
...
expr < expr
Figura 5.2. Árbol sintáctico.
Las hojas representan los elementos terminales (palabras reservadas,
identificadores y/o s´ımbolos) empleados en la instrucción. Los nodos
interiores representan las categor´ıas sintácticas. El compilador entre sus
tareas tiene la de construir estos árboles para cada programa y verificar que
éste cumpla con las reglas de la gramática, en cuyo caso genera el código
objeto.
5.2.
Arboles binarios
Un árbol cuyos nodos internos tienen a lo más dos hijos se denomina árbol
binario. En este caso para referirse a cada hijo se hace por su ubicación con
respecto al nodo padre, as´ı cada nodo tiene hijo izquierdo e hijo derecho.
En la figura 5.3. se presenta un ejemplo de árbol binario. En este caso
concreto se tiene un árbol binario que representa una expresión aritmética,
en la cual cada operador tiene dos operandos. Las hojas son los operandos
de la expresión, sean éstos variables o constantes, y los nodos internos son
los operadores.
Figura 5.3. Árbol binario para la expresión ((A-B)+(X/2))/(B*(C+D)).
108
CAP´
ITULO 5. ´
ARBOLES
Un árbol binario puede definirse como
• un solo nodo, o
• un árbol binario cuya ra´ız tiene a lo más dos hijos, siendo cada uno a vez
un árbol binario.
Debido a la facilidad de manipular árboles binarios es conveniente
representar cualquier árbol como árbol binario. Para convertirlo
conservando la relación jerárquica entre los nodos, puede verse que en
cualquier árbol hay dos tipos de nodos: los hijos y los hermanos. Entonces
se pone del lado izquierdo al primer hijo y del lado derecho a los hermanos.
En la figura 5.4.
se tiene un árbol no binario del lado izquierdo y su representación binaria
del lado derecho.
F
G
Figura 5.4. Conversión de árbol no binario a binario.
Aunque no parece un árbol s´ı lo es. Al girarlo se puede ver de la manera
acostumbrada, como se muestra en la figura 5.5.
5.3.
Implementaci´
on de ´
arboles binarios
Para implementar un árbol binario se usan nodos que además de contener la
información necesaria para la aplicación tienen dos ligas: una para el hijo
izquierdo y otra para el derecho.
En este caso no hay una interfaz que implementar, el árbol se va creando de
acuerdo con las necesidades de la aplicación. Los nodos que se utilizan en
la construcción de todo árbol binario en este texto son de la clase
NodoArbol.
Clase 5.1. La clase NodoArbol cuyos objetos se utilizan para trabajar con
árboles binarios.
Tiene el valor que se almacena en el nodo y dos referencias: una a cada uno
de sus hijos.
/**
* Clase de nodos para trabajar con árboles binarios.
* @author Amparo López Gaona
*/
public class NodoArbol {
5.3. IMPLEMENTACI ´
ON DE ´
ARBOLES BINARIOS
109
G
D
Figura 5.5. Árbol binario creado a partir de un árbol no binario.
public Object valor;
// Valor del nodo
public NodoArbol izquierda;
// Liga a la izquierda
public NodoArbol derecha;
// Liga a la derecha
M´
etodo 5.1. Constructor por omisión, inicializa todos los atributos del nodo
con el valor null.
/*
* Inicializa con null el nodo creado.
*/
public NodoArbol () {
this (null, null, null);
M´
etodo 5.2. Constructor que inicializa el nodo con el valor recibido como
parámetro y los hijos con el valor null.
/**
* Inicializa el nodo con el valor dado como parámetro
* @param valor -- valor que tendrá el nodo
*/
public NodoArbol (Object valor) {
this (valor, null, null);
110
CAP´
ITULO 5. ´
ARBOLES
M´
etodo 5.3. Constructor que inicializa el nodo con los valores recibidos para
cada uno de sus atributos.
/**
* Inicializa el nodo con el valor dado y subárboles dados como parámetro
* @param v -- valor que tendrá el nodo
* @param izq -- ra´
ız del sub-árbol izquierdo
* @param der -- ra´
ız del sub-árbol derecho
*/
public NodoArbol (Object v, NodoArbol izq, NodoArbol der) {
valor = v;
izquierda = izq;
derecha = der;
5.3.1.
Recorridos
Existen operaciones sobre árboles binarios que implican trabajar con todos
los nodos del árbol, por ejemplo: calcular la cantidad de nodos que tiene el
árbol, determinar si dos árboles son idénticos, hacer una copia del árbol,
imprimir el árbol, etcétera. Para realizar tales acciones se requiere visitar
todos los nodos del árbol. Durante esta visita se realiza la operación
deseada, la cual puede ser: contabilizar, comparar, imprimir, etcétera.
Como no se tiene estructura lineal, no es posible recorrerlo con un iterador,
ya que éste trabaja siempre con el siguiente elemento, pero en los árboles
no existe tal concepto. A partir de cada nodo se puede continuar con su hijo
izquierdo y/o derecho, por lo que existen varias formas de recorrer árboles;
las comunes son las siguientes tres: 1. Preorden: se visita la ra´ız del árbol,
luego el subárbol izquierdo y finalmente el subárbol derecho. Cada subárbol
se visita a su vez en preorden, es decir, primero la ra´ız del subárbol,
seguida del subárbol izquierdo y del derecho.
2. Inorden: se visita el subárbol izquierdo, luego la ra´ız y finalmente el
subárbol derecho.
Cada subárbol se visita a su vez en inorden, es decir, primero el subárbol
izquierdo seguido de su ra´ız y de su propio subárbol del derecho.
3. Postorden: primero se visita el subárbol izquierdo luego el subárbol
derecho y por último la ra´ız. Cada subárbol se visita a su vez en postorden,
es decir, primero su subárbol izquierdo, luego el derecho y finalmente su ra
´ız.
Ejemplo 5.2. Si se tiene el árbol de la figura 5.3., el resultado con los
distintos recorridos es el siguiente:
5.3. IMPLEMENTACI ´
ON DE ´
ARBOLES BINARIOS
111
• Preorden: /+-AB/X2*B+CD que corresponde a la notación prefija.
• Inorden: A-B+X/2/B*C+D que corresponde a la notación infija.
• Postorden: AB-X2/+BCD+*/ que corresponde a la notación postfija.
M´
etodo 5.4. Método para el recorrido en preorden de un árbol binario. La
programación de estos métodos es relativamente sencilla, si se hace en
forma recursiva, como se muestra a continuación.
/**
* Recorrido en preorden
* @param nodo -- nodo a partir del cual se realiza el recorrido
*/
public void preorden (NodoArbol nodo) {
if (nodo != null) {
procesar([Link]);
preorden([Link]);
preorden([Link]);
En ocasiones se requiere hacer el recorrido de un árbol por niveles. Esto se
consigue examinando, de izquierda a derecha, primero todos los nodos del
nivel 1, luego los del nivel 2, etcétera. Para lograr este recorrido se utiliza
una cola en la cual se van guardando los nodos visitados. Primero se inserta
la ra´ız y luego, en cada paso, se elimina un nodo de la cola y sus hijos se
colocan en ella.
Ejemplo 5.3. Programa para recorrer un árbol por niveles según el
algoritmos explicado en el párrafo anterior. Se implementa como un
iterador, que resulta interesante, pues todo el trabajo lo hace el método next.
En el constructor se guarda, en la cola, la ra´ız del árbol que se va a
recorrer.
/**
* Programa que recorre un árbol por niveles.
* @author Amparo López Gaona
* @version Abril 2011
*/
class OrdenPorNivel implements [Link] {
private Cola c = new Cola();
public OrdenPorNivel (NodoArbol raiz) {
[Link](raiz);
112
CAP´
ITULO 5. ´
ARBOLES
public boolean hasNext() {
return ! [Link]();
public Object next() {
NodoArbol actual = (NodoArbol) [Link]();
[Link]();
if ([Link] != null)
[Link]([Link]);
if ([Link] != null)
[Link]([Link]);
return [Link];
}
public void remove () {
throw new IllegalStateException();
5.4.
Aplicaciones de ´
arboles binarios
En la sección anterior se pudo ver que resulta de gran utilidad tener un árbol
con una expresión aritmética, pues de esta manera es posible obtener
diferentes representaciones, en particular se puede tener en notación
postfija, que como se vio en el cap´ıtulo 3 facilita su evaluación. Por esta
razón, una de las aplicaciones presentadas en esta sección es la construcción
de un árbol para cualquier expresión aritmética.
La otra aplicación presentada consiste en construir y recorrer un árbol en la
implementación de un juego en el que la computadora adivina el animal que
piensa el usuario. Dicho árbol se trabaja de acuerdo con las respuestas del
usuario a preguntas formuladas, tales respuestas sólo pueden ser s´
ı o no.
5.4.1.
Arbol para expresiones aritm´
eticas
En esta sección se presenta un programa para crear un árbol a partir de una
expresión aritmética, suponiendo que ésta está bien estructurada. El
algoritmo requiere dos pilas, una para almacenar los operadores de la
expresión y otra para los operandos. El algoritmo consiste en leer cada
elemento de la expresión y para cada uno hacer los siguiente: 1. Si es un
operando almacenarlo en la pila de operandos.
2. Si es un paréntesis izquierdo, guardarlo en la pila de operadores.
3. Si es un paréntesis derecho, extraer cada operador de la pila de
operadores hasta encontrar el paréntesis izquierdo correspondiente. Para
cada operador extra´ıdo se crea
5.4. APLICACIONES DE ´
ARBOLES BINARIOS
113
un subárbol, formado por los dos operandos del tope de la pila de operandos
y el operador recién extra´ıdo; este subárbol se almacena en la pila de
operandos.
4. Si es un operador aritmético y la pila de operadores está vac´ıa, guardarlo
ah´ı. En caso contrario, se compara con el que está en el tope de la pila de
operadores, si su prioridad es mayor que el de la pila lo almacena en esa
pila. En caso contrario, se sacan de la pila de operandos los dos últimos y
con ellos y el operador que se toma de la pila de operadores forma un nuevo
operando, el cual se almacena en la pila de operandos. Este paso se repite
hasta que el operador del tope de la pila de operadores tenga mayor
prioridad que el operador que se está examinando.
Ejemplo 5.4. Clase para generar un árbol binario a partir de una expresión
aritmética según el algoritmo descrito.
/**
* Clase para generar arboles binarios a partir de expresiones aritméticas
* @author Amparo López Gaona
* @version Abril 2011
*/
public class ArbolDeExpresiones {
Pila pOperandos;
Pila pOperadores;
final String blanco;
final String operadores;
M´
etodo 5.5. El constructor por omisión crea las dos pilas necesarias para la
creación del árbol, define los operadores aritméticos y los espacios en
blanco.
/** Constructor por omisión
*/
public ArbolDeExpresiones() {
pOperandos = new Pila();
pOperadores = new Pila();
blanco = " \t";
operadores = ")+-*/%^(";
//acomodados por precedencia;
}
M´
etodo 5.6. Método para construir un árbol para una expresión aritmética.
/** Método para construir un árbol para una expresión aritmética dada.
* @param expresión -- Cadena con la expresión aritmética
* @return NodoArbol -- nodo ra´
ız del árbol creado
*/
114
CAP´
ITULO 5. ´
ARBOLES
public NodoArbol construirArbol(String expresion) {
StringTokenizer tokenizer;
String token;
NodoArbol raiz = null;
tokenizer = new StringTokenizer(expresion, blanco+operadores, true);
while ([Link]()) {
token = [Link]();
if ([Link](token) >= 0)
// Es un espacio en blanco, se ignora
else if ([Link](token) < 0)
// Es operando y lo guarda como nodo del árbol
[Link](new NodoArbol(token));
else if([Link](")")) { // Saca elementos hasta encontrar (
while (![Link]() && ![Link]().equals("(")) {
guardarSubArbol();
[Link]();
// Saca el paréntesis izquierdo
} else {
if ( && ![Link]()) {
//operador diferente de cualquier paréntesis
String op = (String) [Link]();
while ( && ![Link]()
&& [Link](op) >= [Link](token)) {
guardarSubArbol();
if (![Link]())
op = (String)[Link]();
}
[Link](token);
//Guarda el operador
raiz = (NodoArbol)[Link]();
//Sacar todo lo que queda
while (![Link]()) {
if ([Link]().equals("(")) {
[Link]();
} else {
guardarSubArbol();
raiz = (NodoArbol) [Link]();
return raiz;
5.4. APLICACIONES DE ´
ARBOLES BINARIOS
115
M´
etodo 5.7. Método privado para guardar en la pila de operandos un subárbol
con una subexpresión formada a partir de los operandos del tope de la pila
de operandos y el operador en el tope de la pila de operadores.
/*
* Método privado para almacenar en la pila un subárbol
*/
private void guardarSubArbol() {
NodoArbol op2 = (NodoArbol)[Link]();
NodoArbol op1 = (NodoArbol)[Link]();
[Link](new NodoArbol(op1, [Link](), op2));
Puede observarse que en las pilas se almacenan diferentes tipos de datos;
esto es posible debido a que se implementó una pila genérica que guarda
objetos de la clase Object y porque toda clase definida en Java es subclase
de Object.
En esta clase se pueden incluir el método para evaluar la expresión y el
método para verificar que los paréntesis estén balanceados vistos en el cap
´ıtulo de pilas. También se pueden incluir métodos para imprimir el árbol en
sus diferentes representaciones (infija, prefija, posfijo).
5.4.2.
Juego de adivinar animales
El juego consiste en que la computadora adivina un animal que el jugador
haya pensado.
Para lograrlo se van formulando preguntas al usuario/jugador, y con base en
sus respuestas (sólo pueden ser si o no) tratar de adivinar el animal en
cuestión.
Los datos acerca de los animales se almacenan en un árbol binario de
cadenas. Los nodos interiores contienen las preguntas y las hojas los
animales. El lado izquierdo de cada nodo es el camino a seguir en caso de
una respuesta afirmativa, y el lado derecho corresponde a respuestas
negativas, como se muestra en la figura 5.6.
¿nada?
si
no
pez
¿ruge?
si
no
puma
perro
Figura 5.6. Árbol de animales.
116
CAP´
ITULO 5. ´
ARBOLES
El algoritmo consiste en mostrar una pregunta acerca de un animal.
1. Mientras no se llegue a una hoja:
(a) Si la respuesta es si, ir por el lado izquierdo del árbol para hacer otra
pregunta.
(b) Si la respuesta es no, ir por el lado derecho del árbol para hacer otra
pregunta.
2. Al llegar a una hoja mostrar el animal que contiene y preguntar si es el
buscado. Si la respuesta es afirmativa la computadora ha adivinado el
animal en cuestión y el juego termina. En caso contrario se debe dar de alta
un nuevo animal.
Ejemplo 5.5. Programa que juega con el usuario a adivinar animales y va
construyendo el árbol. Se necesita el nodo ra´ız del árbol y un Scanner para
interactuar con el usuario.
/**
* Clase para jugar a adivinar animales
* @author Amparo López Gaona
* @version Abril 2011
*/
class AdivinaAnimal {
private NodoArbol raiz;
private Scanner in;
M´
etodo 5.8. En el constructor se va construyendo el árbol de acuerdo con las
respuestas del usuario.
/**
*
Método para adivinar un animal
*/
public void jugar () {
raiz = new NodoArbol("Perro");
in = new Scanner([Link]);
NodoArbol nodo = raiz;
while (nodo != null) {
// si el nodo tiene hijos hay preguntas
if ([Link] != null) {
[Link]([Link]);
// Muestra una pregunta
nodo = (respuesta()) ? [Link] : [Link];
} else { // Si no hay hijos, se tiene un animal
[Link]("Ya sé, es un(a) " + [Link]); if (respuesta())
[Link]("Gane :) !!");
else // Aprenderé algo
5.4. APLICACIONES DE ´
ARBOLES BINARIOS
117
animalNuevo (nodo);
nodo = null;
M´
etodo 5.9. Método privado para leer la respuesta del usuario; regresa un
valor booleano de verdadero en caso que la respuesta sea si y falso en otro
caso.
/*
* Método privado para leer una respuesta del usuario sólo puede ser si o no
* @return boolean -- true si la respuesta es si y false en otro caso.
*/
private boolean respuesta () {
String resp = [Link]().tolower();
while(true) {
if ([Link]("si") return true;
if ([Link]("no") return false;
[Link]("La respuesta debe ser si o no");
}
En caso de no adivinar el animal, se debe incorporar ese nuevo dato al
árbol. Para ello, se pregunta al usuario la forma de determinar de qué
animal se trata, y el nodo que es una hoja se convierte en nodo interno con
dos hijos, cada uno con un animal. El nuevo animal se coloca en el lado
correspondiente de acuerdo con la respuesta a la pregunta solicitada. Por
ejemplo, con el árbol del lado izquierdo de la figura 5.7. y el intento de
adivinar si se trata de una ballena, el árbol se modifica, como se muestra en
el lado derecho de la misma figura.
¿nada?
¿nada?
pez
¿ruge?
¿mamífero?
¿ruge?
puma
perro
ballena
pez
puma
perro
Figura 5.7. Inserción de un nuevo animal en el árbol.
M´
etodo 5.10. El método animalNuevo tiene como objetivo dar de alta un
nuevo animal en el árbol. Recibe como parámetro el nodo que será el padre
del animal.
118
CAP´
ITULO 5. ´
ARBOLES
/**
* Método para dar de alta un nuevo animal
* @param nodo -- nodo del cual se va a colgar el animal
*/
private void animalNuevo (NodoArbol nodo) {
String animalNodo = (String) [Link];
[Link]("¿Cuál es tu animal?"); try {
String nuevoA = [Link]();
[Link]("Con qué pregunta, cuya respuesta sea si/no, puedo"
+ " determinar que se trata de un(a) " + nuevoA); String pregunta =
[Link]();
NodoArbol nodo1 = new NodoArbol(animalNodo);
NodoArbol nodo2 = new NodoArbol(nuevoA);
[Link]("¿Para un(a) "+nuevoA+" la respuesta es si/no?");
[Link] = "¿"+pregunta+"?";
if (respuesta()) {
[Link] = nodo2;
[Link] = nodo1;
} else {
[Link] = nodo1;
[Link] = nodo2;
} catch (IOException e) { } // no hace nada
A continuación la ejecución del programa con el árbol de la figura 5.6.
¿El animal nada?
No
¿Ruge?
No
Ya sé, es un perro
Si
Gané!!!
¿Quieres jugar otra vez?
¿El animal nada?
Si
Ya sé es un pez.
No
¿Qué animal pensaste?
ballena
5.5. ´
ARBOLES BINARIOS DE B ´
USQUEDA
119
¿Qué pregunta debo hacer para determinar que es un ballena (la respuesta
debe ser Si/No)?
¿Es mam´
ıfero?
¿Para un ballena la respuesta es si o no?
Si
¿Quieres jugar otra vez?
5.5.
Arboles binarios de b´
usqueda
Suponer que se tiene el problema de eliminar los elementos duplicados de
una colección de datos. Para resolverlo se puede ir comparando cada dato
con el resto de los datos en la colección y al encontrar duplicados
eliminarlos. Esto requiere recorrer varias veces el arreglo de datos, lo cual
es una tarea de complejidad O( n 2). Para reducir la complejidad, el
problema puede resolverse ordenando los datos, as´ı los elementos
duplicados quedan juntos y con una pasada al arreglo se pueden eliminar
los duplicados (aqu´ı la complejidad está en el ordenamiento). Una tercera
opción es utilizar un árbol como sigue: 1. Leer el primer número y
almacenarlo en la ra´ız del árbol.
2. Repetir hasta encontrar un duplicado o el árbol esté vac´ıo.
(a) Leer un número y compararlo con la ra´ız.
(b) Si coinciden entonces hay un duplicado y no se incluye en el árbol.
(c) Si el número es menor repetir el proceso con el árbol izquierdo.
(d) Si el número es mayor repetir el proceso con el árbol derecho.
3. Si se llegó a un árbol vac´ıo entonces almacenar el número en una nueva
posición del árbol.
Un árbol binario de búsqueda es un árbol binario en el que para cada nodo
se tiene que los valores de su subárbol izquierdo son menores que él, y los
del subárbol derecho son mayores que él. De esta forma al hacer un
recorrido en inorden se obtienen los datos en orden ascendente. En la figura
5.8. se tiene un árbol binario de búsqueda (en adelante sólo árbol de
búsqueda) con nombres de personas. Al recorrerlo en inorden se tiene
Abigail, Adán, Adela, Agust´
ın, Alberto, Alejandro, Alicia, Amparo, Andrea y Armando.
Una ventaja de usar estos árboles es que para localizar un elemento se tiene
que recorrer a lo más de la ra´ız a la hoja correspondiente, lo cual toma
O(log n). El árbol del ejemplo contiene 10 elementos y su profundidad es 4.
Por lo tanto, para encontrar un dato o determinar que no está se tienen que
hacer como máximo cuatro comparaciones.
120
CAP´
ITULO 5. ´
ARBOLES
Alejandro
Agustín
Amparo
Adán
Alberto
Alicia
Andrea
Abigail
Adela
Armando
Figura 5.8. Árbol de búsqueda.
5.5.1.
Implementaci´
on
Para crear el TAD correspondiente a árboles binarios de búsqueda se
implementará la siguiente interfaz:
public interface ArbolBuscable {
public boolean estaVacio();
public void vaciar();
public int tamanio();
public void agregar(Object elem);
public boolean contiene(Object elem);
public void eliminar(Object elem);
Para cada una de estas operaciones se espera el siguiente comportamiento:
• agregar. Agrega al árbol el objeto especificado como el parámetro,
respetando la relación de orden necesaria para que sea un árbol binario de
búsqueda. La cantidad de nodos en el árbol se incrementa en una unidad si
el elemento no se encuentra en el árbol.
• contiene. Regresa true si el elemento está en el árbol y false en otro caso.
Este método no cambia el estado del árbol.
• eliminar. Si el elemento existe en el árbol lo elimina y se reduce la
cantidad de nodos del árbol en una unidad. En caso contrario no hace nada.
• estaVacia. Devuelve true si el árbol está vac´ıo y false en otro caso.
• tamanio. Devuelve la cantidad de elementos almacenados en el árbol.
5.5. ´
ARBOLES BINARIOS DE B ´
USQUEDA
121
• vaciar. Elimina todos los elementos del árbol. El número de nodos del
árbol después de esta operación es cero.
Clase 5.2. Clase para el TAD árbol binario de búsqueda. En esta clase se
requiere un nodo para guardar la ra´ız del árbol, un contador para el número
de elementos en el árbol y un comparador para saber la relación de orden
entre dos elementos.
/**
* Clase para trabajar con árboles binarios de búsqueda
* @author Amparo López Gaona
*/
public class ArbolBinarioBusqueda implements ArbolBuscable{
protected NodoArbol raiz;
private final import [Link] prueba;
private int nDatos;
M´
etodo 5.11. Constructor que recibe un objeto que implementa la interfaz
Comparator del paquete [Link] para poder establecer una relación de
orden entre los valores en el árbol.
/**
* Inicializa un árbol binario de búsqueda.
* @param c objeto comparador usado para colocar elementos en secuencia.
*/
public ArbolBinarioBusqueda (Comparator c) {
prueba = c;
raiz = null;
nDatos = 0;
M´
etodo 5.12. Constructor por omisión. Inicializa el árbol binario de búsqueda
con un comparador para objetos de la clase Integer.
/**
* Constructor por omisión. Compara enteros.
*/
public ArbolBinarioBusqueda () {
this(new ComparadorEnteros());
M´
etodo 5.13. Método para determinar si un árbol está vac´ıo o no.
122
CAP´
ITULO 5. ´
ARBOLES
/**
* Determina si el árbol está vac´
ıo.
* @return true en caso de que el árbol esté vac´
ıo.
*/
public boolean estaVacio () {
return raiz == null;
M´
etodo 5.14. Método para eliminar todos los elementos del árbol, lo único
que hace es que la ra´ız tenga valor null y el número de datos sea cero.
/**
* Elimina todos los elementos de la cola.
*/
public void vaciar() {
raiz = null;
nDatos = 0;
M´
etodo 5.15. El método tamanio devuelve un entero que indica la cantidad de
elementos almacenados en el árbol.
/**
* Método para conocer el tama~
no de una cola
* @return int -- cantidad de elementos en la cola
*/
public int tamanio() {
return nDatos;
Clase 5.3. Clase privada para comparar dos objetos de la clase Integer. Se
usa cuando no se proporciona un comparador al constructor.
/*
* Clase privada para comparar dos objetos de la clase Integer.
*/
private class MiComparator implements Comparator {
public int compare(Object o1, Object o2) {
return (Integer)o1 - (Integer)o2;
5.5. ´
ARBOLES BINARIOS DE B ´
USQUEDA
123
M´
etodo 5.16. El método agregar permite insertar un elemento en el árbol,
para ello se compara éste con el almacenado en la ra´ız, si son iguales se
terminó el trabajo. En caso de ser menor se vuelve a realizar este método,
ahora con el subárbol izquierdo; si es mayor se trabaja con el subárbol
derecho. Notar que es un método recursivo, puesto que la definición se
presta para ello.
/**
* Inserta en el árbol, ignora duplicados.
* @param dato el dato a insertar
*/
public void agregar(Object dato) {
raiz = agregar(dato, raiz);
/**
* Método interno para agregar en un subárbol.
* @param dato elemento a agregar.
* @param nodo la raiz del árbol en donde se agregara el dato.
* @return la nueva raiz.
*/
private NodoArbol agregar(Object dato, NodoArbol nodo) {
if(nodo == null)
nodo = new NodoArbol(dato);
else if([Link](dato, [Link]) < 0)
[Link] = agregar(dato, [Link]);
else if([Link](dato, [Link]) > 0)
[Link] = agregar(dato, [Link]);
else
// No hace nada con los duplicados
return nodo;
M´
etodo 5.17. Método para encontrar el dato con el valor menor. Éste se
encuentra en el nodo que está más a la izquierda del árbol. Se presenta una
versión recursiva para este trabajo.
/**
* Encuentra el menor elemento del árbol.
* @return el menor elemento en el árbol o nulo si está vac´
ıo.
*/
public Object encontrarMin() {
NodoArbol n = encontrarMin(raiz);
return (n == null) ? null: [Link];
}
124
CAP´
ITULO 5. ´
ARBOLES
/**
* Método interno para encontrar el menor elemento en un árbol.
* @param t el nodo ra´
ız del árbol
* @return el nodo que contiene el menor elemento.
*/
private NodoArbol encontrarMin(NodoArbol nodo){
if(nodo == null)
return null;
else if([Link] == null)
return nodo;
return encontrarMin([Link]);
M´
etodo 5.18. Método para encontrar el elemento mayor. En este caso es el
elemento más a la derecha y se utiliza un procedimiento iterativo, aunque
también podr´ıa ser recursivo.
/** Encuentra el mayor elemento del árbol.
* @return el mayor elemento en el árbol o nulo si está vac´
ıo.
*/
public Object encontrarMax() {
NodoArbol n = encontrarMax(raiz);
return (n == null) ? null: [Link];
/** Método interno para encontrar el mayor elemento en un árbol.
* @param nodo el nodo ra´
ız del árbol en donde se buscara el mayor
* @return el nodo que contiene el mayor elemento.
*/
private NodoArbol encontrarMax(NodoArbol nodo) {
if(nodo != null)
while([Link] != null)
nodo = [Link];
return nodo;
}
M´
etodo 5.19. Para determinar si un elemento se encuentra en el árbol o no se
recorre el árbol, si el dato es igual al almacenado ya lo encontró, en caso
contrario se trabaja con el subárbol izquierdo si el dato es menor o bien con
el subárbol derecho si el dato es mayor que el almacenado.
/** Busca un dato en el árbol.
* @param dato elemento a buscar.
* @return el elemento en el árbol o bien null si no está.
*/
5.5. ´
ARBOLES BINARIOS DE B ´
USQUEDA
125
public boolean contiene(Object dato) {
return contiene(dato, raiz);
/**
* Método interno para encontrar un elemento en un árbol.
* @param dato el elemento a buscar.
* @param t el nodo ra´
ız del árbol
* @return el nodo que contiene elemento buscado.
*/
private boolean contiene(Object dato, NodoArbol nodo) {
if(nodo == null)
return false;
if([Link] (dato, [Link]) < 0)
return contiene(dato, [Link]);
else if([Link](dato ,[Link]) > 0)
return contiene(dato, [Link]);
else
return true; // Lo encontró
Para eliminar un elemento del árbol, primero se encuentra la posición del
nodo que lo contiene, si existe tal nodo; para no perder la propiedad de ser
árbol de búsqueda se sustituye el valor almacenado en dicho nodo, ya sea
por el valor mayor en el subárbol izquierdo, o bien, por el valor menor en el
subárbol derecho. Por ejemplo, si se desea eliminar el nodo que tiene el
número 65 en el árbol mostrado en la parte izquierda de la figura 5.9., se
sustituye éste por el nodo con el 62, que es el valor mayor del subárbol
izquierdo (parte derecha de la figura 5.9.) o puede sustituirse por el 68, que
es el menor valor de su subárbol derecho.
50
50
38
65
38
62
25
60
80
25
60
80
30
53
62
68
30
53
68
Figura 5.9. Supresión de un nodo en un árbol binario de búsqueda.
M´
etodo 5.20. El método eliminar implementa el algoritmo antes descrito para
suprimir un elemento de un árbol binario de búsqueda.
126
CAP´
ITULO 5. ´
ARBOLES
/**
* Elimina un elemento del árbol, en caso de no encontrarlo no hace nada.
* @param dato elemento a eliminar
*/
public void eliminar(Object dato) {
raiz = eliminar(dato, raiz);
/**
* Método interno para eliminar de un subárbol.
* @param dato elemento a eliminar.
* @param nodo el nodo raiz del árbol.
* @return la nueva ra´
ız.
*/
private NodoArbol eliminar(Object dato, NodoArbol nodo) {
if(nodo == null)
return nodo;
// El dato no se encuentra
if([Link](dato, [Link]) < 0)
[Link] = eliminar(dato, [Link]);
else if([Link](dato, [Link]) > 0)
[Link] = eliminar(dato, [Link]);
else if([Link] != null && [Link] != null) {
//Tiene dos hijos
[Link] = encontrarMin([Link]).valor;
[Link] = eliminar([Link], [Link]);
else
// A lo más tiene un hijo
nodo = ([Link] != null) ? [Link] : [Link]; return
nodo;
M´
etodo 5.21. El método imprimirArbol sirve para imprimir los datos de un
árbol binario de búsqueda en orden ascendente, para ello hace un recorrido
de éste en inorden.
/**
* Imprime el contenido del árbol ordenado.
*/
public void imprimirArbol() {
if(estaVacio())
[Link]("Árbol vac´
ıo");
else
imprimirArbol(raiz);
5.5. ´
ARBOLES BINARIOS DE B ´
USQUEDA
127
/**
* Método interno para imprimir un subárbol.
* @param nodo nodo ra´
ız del árbol.
*/
private void imprimirArbol(NodoArbol nodo) {
if(nodo != null) {
imprimirArbol([Link]);
[Link]([Link]);
imprimirArbol([Link]);
El tiempo que toma cada una de las operaciones sobre árboles binarios de
búsqueda con la implementación anterior se muestra en la tabla 5.1.
Operación
Tiempo de ejecución
constructores
O(1)
agregar
O( n)
contiene
O( n)
encontrarMax
O( n)
encontrarMin
O( n)
estaVacio
O(1)
eliminar
O( n)
imprimirArbol
O( n)
tamanio
O(1)
vaciar
O(1)
Tabla 5.1. Tiempo de ejecución de los métodos de la clase
ArbolBinarioBusqueda.
Los métodos agregar, contiene, encontrarMax y encontrarMin son de orden
lineal debido a que, en el peor de los casos, el árbol consta de una sola rama
hacia un solo lado. Si se tienen árboles como los mostrados en las figuras de
este cap´ıtulo, el orden es logar´ıtmico, como se verá en la sección 5.6.
5.5.2.
Aplicaci´
on de ´
arboles binarios de b´
usqueda
El problema presentado en esta sección se conoce como empacado en
contenedores o bin packing. El problema consiste en empacar un conjunto
de objetos, cada uno con cierta capacidad (tama˜
no, volumen, peso, etc.) en la menor cantidad de contenedores. Los objetos
a empacar pueden tener capacidad diferente y todos los contenedores tienen
capacidad fija.
Este es un problema clásico con varias aplicaciones prácticas relacionadas
con minimizar
128
CAP´
ITULO 5. ´
ARBOLES
espacio o tiempo. Por ejemplo, minimizar la cantidad de bodegas a alquilar
para guardar cierto tiempo algunos productos o minimizar la cantidad de
veh´ıculos requeridos para trans-portar ciertos productos para su
distribución. En sistemas operativos se utiliza para asignar memoria a
trabajos.
Por ejemplo, se puede suponer que se tiene un almacén con 20 objetos; el
peso de cada uno es 22 , 59 , 68 , 77 , 21 , 15 , 17 , 45 , 27 , 21 , 35 , 5 , 94 ,
19 , 10 , 24 , 30 , 40 y 46 kg, respectivamente, y se necesita almacenarlos en
bodegas con capacidad para 100 kg cada una. El problema consiste en
hacerlo minimizando la cantidad de bodegas utilizadas.
Este problema es NP, es decir, su solución no toma un tiempo polinomial,
por lo tanto se resuelve usando un algoritmo de aproximación. En este caso,
tal algoritmo genera soluciones que usan una cantidad de contenedores
cercana al m´ınimo. Existen cuatro algoritmos de aproximación para este
problema:
1. First Fit (FF). En este caso el objeto se empaca en el primer contenedor
en el cual quepa; generalmente se recorren de izquierda a derecha los
contenedores para encontrarlo.
2. Best Fit (BF). En este caso se considera la capacidad no utilizada del
contenedor. Para cada objeto que se desea almacenar se seleccionan los
contenedores en los cuales puede caber y se almacena en el que tiene la
menor capacidad no utilizada.
3. First Fit Decresing (FFD). Este método es igual al primero, excepto que
antes de empezar a empacar los objetos se acomodan en orden decreciente
de acuerdo con su capacidad.
4. Best Fit Decresing (BFD). Este método es igual al BF en cuanto a la
asignación del contenedor, con la diferencia de que los objetos se ordenan
de acuerdo con su capacidad antes de empezar a empacarlos.
En esta sección se presenta una solución utilizando el método BF. Se trabaja
con un árbol de búsqueda en el que se almacenan los contenedores,
teniendo en cuenta la capacidad disponible de ellos. Al ir llenándolos es
posible que esta capacidad se repita, as´ı que se utilizará un árbol binario de
búsqueda que acepte repetidos.
Ejemplo 5.6. Clase para crear árboles binarios de búsqueda permitiendo
elementos repetidos. Se implementa como una extensión de la clase
ArbolBinarioBusqueda.
/**
* Clase que extiende ArbolBinarioBusqueda para permitir trabajar con
* datos duplicados.
* @author Amparo López Gaona
*/
class ArbolBinarioBusquedaR extends ArbolBinarioBusqueda {
5.5. ´
ARBOLES BINARIOS DE B ´
USQUEDA
129
M´
etodo 5.22. Constructor por omisión; llama al de la superclase para utilizar
el comparador de enteros.
/**
* Constructor por omisión.
*/
public ArbolBinarioBusquedaR () {
super();
M´
etodo 5.23. Constructor que recibe un comparador.
/**
* Inicializa un árbol binario de búsqueda.
* @param c -- objeto comparador usado para colocar elementos en
secuencia.
*/
public ArbolBinarioBusquedaR ([Link] c) {
super(c);
M´
etodo 5.24. El método agregar trabaja igual que el de la superclase, pero
esta vez inserta el nuevo nodo del lado izquierdo cuando el elemento a
insertar es menor o igual que el nodo ra´ız.
/**
* Inserta en el árbol manteniendo el balance e ignorando duplicados.
* @param dato el dato a insertar
*/
public void agregar(Object dato) {
raiz = agregar(dato, raiz);
/*
* Método interno para agregar en un subárbol.
* @param dato elemento a agregar.
* @param nodo la ra´
ız del árbol en donde se agregara el dato.
* @return la nueva ra´
ız.
*/
private NodoArbol agregar(Object elemento, NodoArbol nodo) {
if(nodo == null) {
nodo = new NodoArbol(elemento);
nElementos ++;
} else if([Link](elemento, [Link]) <= 0) [Link] =
agregar(elemento, [Link]);
130
CAP´
ITULO 5. ´
ARBOLES
else
[Link] = agregar(elemento, [Link]);
return nodo;
M´
etodo 5.25. Método para obtener la ra´ız de un árbol.
/**
* Método que devuelve la ra´
ız de un árbol
* @return NodoArbol -- nodo que es la ra´
ız del árbol
*/
public NodoArbol obtenerRaiz() {
return raiz;
}
Para resolver el problema de empacado se requiere tener una clase para los
contenedores.
Cada contenedor debe tener un identificador, su capacidad disponible y una
lista de objetos almacenados en él (al inicio ésta debe estar vac´ıa). Debido
a la sencillez de esta clase se omite su codificación.
Los objetos pueden tener toda la información necesaria para la aplicación
particular, pero deben contar con al menos un atributo para especificar la
capacidad del mismo, por lo tanto se pide que estos objetos implementen la
interfaz Objeto definida como public interface Objeto {
public int capacidad();
Ejemplo 5.7. La clase BinPack tiene el método de empacado. El
constructor de la clase utiliza un comparador para números enteros
proporcionada en esta clase.
/** Clase para empacado usando siempre el contenedor en el que mejor
* quepa cada objeto a empacar
* @author Amparo López Gaona
* @version abril 2011
*/
public class BinPack {
ArbolBinarioBusquedaR arbol;
/**
* Método por omisión crea un árbol binario de búsqueda con duplicados
*/
public BinPack () {
arbol = new ArbolBinarioBusquedaR(new MiComparador());
5.5. ´
ARBOLES BINARIOS DE B ´
USQUEDA
131
M´
etodo 5.26. El método de empacado recibe dos parámetros, el primero es un
arreglo con los objetos que deben empacarse y el segundo es la capacidad
de los contenedores.
/** Método para empacar objetos en contenedores de capacidad predefinida
* @param objetos - Arreglo de objetos que se van a empacar
* @param capContenedor - capacidad de los contenedores
* @return Lista - lista con los contenedores con los objetos almacenados
* en cada uno.
*/
public Lista empacar(Objeto [] objetos, int capContenedor) {
Lista acomodo = new Lista();
int cont = 0;
Contenedor mejor;
for (int i = 0; i < [Link]; i++) {
mejor = menorMayor(objetos[i].capacidad());
if (mejor == null) {
mejor = new Contenedor("contenedor" + ++cont, capContenedor,
objetos[i]);
} else {
[Link](objetos[i]);
[Link](mejor);
int nuevoDisp = [Link]() - objetos[i].capacidad();
[Link](nuevoDisp);
if (nuevoDisp > 0) {
[Link](mejor);
} else {
[Link](mejor);
while (![Link]()){
[Link]([Link]().valor);
[Link]([Link]().valor);
return acomodo;
}
M´
etodo 5.27. El método menorMayor es utilizado por el método empacar
para encontrar el contenedor cuya capacidad es tal que cabe el objeto que se
quiere almacenar, pero también es la menor en el cual sucede esto.
/* Método que regresa el menor elemento mayor o igual que el buscado o
* null si no existe un elemento as´
ı.
132
CAP´
ITULO 5. ´
ARBOLES
* @return Object -- menor elemento mayor o igual que el buscado
* @param cual -- elemento buscado
*/
private Contenedor menorMayor(int capacidad) {
NodoArbol nodo = [Link]();
NodoArbol elementoMejor = null;
while (nodo != null)
if (((Contenedor)([Link])).obtenerDisponible() >= capacidad) {
elementoMejor = nodo;
nodo = [Link];
} else
nodo = [Link];
return (elementoMejor != null) ? (Contenedor)[Link] : null;
Clase 5.4. Para ubicar los contenedores en el árbol se toma en cuenta su
capacidad disponible, por ello se incluye la clase interna MiComparador
que implementa un comparador.
/*
* Clase para comparar la capacidad disponible de dos contenedores.
*/
private class MiComparador implements [Link] {
public int compare(Object o1, Object o2) {
return ((Contenedor)(o1)).obtenerDisponible() -
((Contenedor)(o2)).obtenerDisponible();
5.6.
Arboles balanceados
Al crear un árbol de búsqueda puede suceder que el resultado se vea como
el mostrado en la figura 5.10., en tal caso la búsqueda de algunos elementos
puede tener complejidad mayor de O(log n), por ejemplo, al buscar el
número 20, se requieren siete comparaciones, esto es debido a que la rama
derecha del árbol está más larga que la izquierda, por lo tanto se pierde la
ventaja de usar árboles de búsqueda.
Para garantizar un tiempo óptimo en la búsqueda se requiere que el árbol se
vaya balan-ceando conforme se va construyendo. Un árbol está balanceado
si para todo nodo en el árbol, la diferencia entre la altura de su subárbol
derecho y su subárbol izquierdo es a lo más 1.
Un árbol binario de búsqueda balanceado es, como su nombre indica, de
búsqueda, es decir, existe relación de orden entre sus nodos y además está
balanceado. Estos árboles se conocen como AVL en honor de sus creadores,
Adelson-Velskii y Landis.
5.6. ´
ARBOLES BALANCEADOS
133
10
7
12
16
20
Figura 5.10. Árbol no balanceado.
Clase 5.5. La clase NodoAvl presentada a continuación es una extensión de
la clase NodoArbol.
Debido a que la altura es información muy importante, en los nodos de este
tipo de árboles se incluye. Los constructores llaman a sus equivalentes de la
superclase y agregan la altura con valor igual a cero.
/**
* Clase de nodos para arboles balanceados
* @author Amparo López Gaona
* @version abril 2011
*/
class NodoAvl extends NodoArbol{
int altura;
//Hereda valor, izquierda y derecha
NodoAvl(Object valor) {
this(valor, null, null );
NodoAvl(Object valor, NodoAvl izq, NodoAvl der ) {
super(valor,izq,der);
altura= 0;
Para ilustrar la forma de creación de estos árboles se muestra en el caso en
que se desea incluir los números 1, 2, 3, 5 y 6 en ese orden, que sin
balancear ser´ıa el caso extremo en el que el árbol tiene una sola rama a la
derecha como se muestra en la figura 5.11.
El 1 es la ra´ız, por ser el primer elemento insertado. Al insertar el 2 queda
del lado derecho del 1, sin problema. Al insertar el 3, el árbol quedar´ıa
desbalanceado (lado izquierdo de la figura 5.12.), as´ı que se hace una
rotación a la izquierda, de tal suerte que el 2 queda
134
CAP´
ITULO 5. ´
ARBOLES
Figura 5.11. Árbol no balanceado con los números del 1, 2, 3, 5 y 6.
como ra´ız del nuevo árbol balanceado (lado derecho de figura 5.12.). Para
determinar si el árbol está o no balanceado se toma en cuenta la altura del
mismo, por eso se incluye esta información en las siguientes figuras,
separándola de la información del nodo con dos puntos.
2
2:1
3
1:0
3:0
Figura 5.12. Inserción en un árbol balanceado.
Al insertar el 5 se coloca como hijo derecho del 3 y el árbol sigue
balanceado (lado izquierdo figura 5.13.). Al insertar el 6 quedar´ıa como se
muestra en la parte media de la figura 5.13. por lo que se tiene que hacer
una rotación a la izquierda tomando el 5 como ra´ız para balancear el árbol,
obteniendo el resultado mostrado en el lado derecho de la misma figura.
2:2
2:2
2:2
1:0
3:1
1:0
3:1
1:0
5:1
5:0
5:0
3:0
6:0
6:0
Figura 5.13. Inserción del 5 y del 6 en el árbol balanceado.
En ocasiones no basta una sola rotación para dejar balanceado el árbol. Por
ejemplo al insertar el número 4 el árbol queda desbalanceado, como se
muestra en la parte izquierda de la figura 5.14. Al hacer una rotación a la
derecha queda como se muestra en la parte media de la misma figura. Es
decir, no basta colocar el 3 como ra´ız del nuevo subárbol, para solucionar
el problema se requiere una segunda rotación, esta vez a la izquierda, y con
ello el árbol queda como en la parte derecha de la figura 5.14., es decir,
balanceado.
5.6. ´
ARBOLES BALANCEADOS
135
2:2
2:2
1:0
5:1
1:0
3:2
3:2
3:0
6:0
5:1
2:1
5:1
4:0
4:0
6:0
1:0
4:0
6:0
Figura 5.14. Doble rotación para dejar un árbol balanceado.
M´
etodo 5.28. El método agregar es el encargado de construir el árbol
balanceado. Este método recibe el valor que se desea insertar en el árbol y
llama a un método interno recursivo, que es el encargado de encontrar el
lugar adecuado en el árbol para el nuevo elemento, siguiendo el mismo
algoritmo que en un árbol de búsqueda, al insertar el nuevo nodo verifica
que el árbol esté balanceado, y en caso necesario hace las rotaciones
pertinentes.
/**
* Agregar un nodo en el árbol, ignorando los duplicados
* @param x el elemento a agregar.
*/
public void agregar(Object obj) {
raiz = agregar(obj, raiz);
/**
* Método interno, auxiliar, para agregar en un árbol.
* @param x elemento a agregar.
* @param t nodo ra´
ız del árbol.
* @return la nueva ra´
ız.
*/
private NodoAvl agregar(Object obj, NodoAvl n) {
if(n == null)
n = new NodoAvl(obj);
else if([Link](obj, [Link]) < 0) {
[Link] = agregar(obj, [Link]);
if(altura([Link]) - altura([Link]) == 2)
if([Link](obj, [Link]) < 0)
n = rotarIzq(n);
else { // Doble rotación sobre la izquierda
[Link] = rotarDer([Link]);
n = rotarIzq(n);
136
CAP´
ITULO 5. ´
ARBOLES
else if([Link](obj, [Link]) > 0) {
[Link] = agregar(obj, [Link]);
if(altura([Link]) - altura([Link]) == 2)
if([Link](obj, [Link]) > 0)
n = rotarDer(n);
else {
//Doble rotación sobre la derecha
[Link] = rotarIzq([Link]);
n = rotarDer(n);
else ;
// Encontró un duplicado y no hace nada.
[Link] = max(altura([Link]), altura([Link])) + 1; return n;
/*
* Método privado para obtener la altura de un nodo,
* @return -1 si el nodo es null y la altura del nodo en otro caso.
*/
private int altura (NodoAvl n) {
return (n == null) ? -1 : [Link];
M´
etodo 5.29. El método para rotar a la derecha crea un subárbol con ra´ız, el
hijo izquierdo del nodo pasado como parámetro. Una vez hecha la rotación
actualiza la altura de los nodos involucrados en la misma.
/*
* Método para rotar a la derecha
* @param n -- nodo ra´
ız del subárbol que se va a rotar
* @return NodoAvl -- Nodo ra´
ız del subárbol después de la rotación
*/
private NodoAvl rotarDer(NodoAvl n) {
NodoAvl nraiz = [Link];
[Link] = [Link];
[Link] = n;
[Link] = max(altura([Link]), altura([Link])) + 1; [Link] =
max(altura([Link]), [Link]) + 1; return nraiz;
M´
etodo 5.30. El método para rotar a la izquierda crea un subárbol con ra´ız,
el hijo derecho del nodo pasado como parámetro. Una vez hecha la rotación
actualiza la altura de los nodos involucrados en la misma.
5.7. EJERCICIOS
137
/*
* Método para rotar a la izquierda
* @param n -- nodo ra´
ız del subárbol que se va a rotar
* @return NodoAvl -- Nodo ra´
ız del subárbol después de la rotación
*/
private NodoAvl rotarIzq(NodoAvl n) {
NodoAvl nraiz = [Link];
[Link] = [Link];
[Link] = n;
[Link] = max(altura([Link]), altura([Link])) + 1; [Link] =
max(altura([Link]), [Link]) + 1; return nraiz;
El tiempo que toma cada una de las operaciones sobre árboles binarios de
búsqueda cuando están balanceados con la implementación anterior se
muestra en la tabla 5.2.
El tiempo de ejecución es logar´ıtmico puesto que el árbol está balanceado.
Operación
Tiempo de ejecución
constructores
O(1)
agregar
O(log n)
contiene
O(log n)
encontrarMax
O(log n)
encontrarMin
O(log n)
estaVacio
O(1)
tamanio
O(1)
vaciar
O(1)
Tabla 5.2. Complejidad de los métodos de la clase ArbolBinarioBusqueda si
están balanceados.
5.7.
Ejercicios
1. Crear un tipo de datos para ejemplificar las definiciones de árboles. La
interfaz que debe implementar este TAD es la siguiente:
public interfaz Arbolable {
public int tamanio();
public boolean estaVacio();
public void vaciar();
public Iterator iterator();
138
CAP´
ITULO 5. ´
ARBOLES
public Object padre(Nodo);
public Lista hijos(Nodo);
public boolean esInterno(Nodo);
public boolean esHoja(Nodo);
public boolean reemplazar(Nodo actual, Nodo nuevo);
Donde los métodos deben tener el siguiente comportamiento:
• tamanio() Devuelve la cantidad de nodos en el árbol.
• estaVacio(). Devuelve true si el árbol no tiene nodos o false en otro caso.
• vaciar(). Devuelve un árbol sin nodos.
• iterador(). Devuelve un iterador sobre el árbol, el orden en que se recorra
lo decide el programador.
• padre(Nodo). Devuelve el objeto padre del nodo dado.
• hijos(Nodo). Devuelve una lista con los hijos del nodo dado.
• esInterno(Nodo). Devuelve true si el nodo es interno y false en otro caso.
• esHoja(Nodo). Devuelve true si el nodo es una hoja y false en otro caso.
• reemplazar(Nodo actual, Nodo nuevo). Sustituye el valor del nodo actual
por el nuevo.
2. Dado el árbol de la figura 5.15. contestar las siguientes preguntas.
G
K
Figura 5.15. Árbol de letras.
(a) ¿Cuál nodo es la ra´ız?
5.7. EJERCICIOS
139
(b) ¿Cuántos caminos diferentes de longitud 3 hay?
(c) ¿La sucesión de nodos HGFBACI es un camino?
(d) ¿Cuáles nodos son los ancestros de K?
(e) ¿Cuáles nodos son las hojas?
(f) ¿Cuál es la altura del nodo C?
(g) ¿Cuál es la altura del árbol?
(h) ¿Cuál es la profundidad del nodo C?
(i) ¿Cuál es el hermano a la derecha de D?
(j) ¿Cuántos hijos tiene A?
3. El recorrido en preorden de cierto árbol binario es: GEAIBMCLDFKJH
y en inorden IABEGLDCFMKHJ.
(a) Dibujar el árbol binario.
(b) Dar el recorrido en postorden.
(c) Escribir un método para dar el recorrido en postorden dado el recorrido
en preorden e inorden.
4. Escribir un método para calcular la altura de un árbol cualquiera. Escribir
dos versiones de este método: una recursiva y otra sin recursión.
5. En el programa para adivinar animales incluir los métodos necesarios
para tener la posibilidad de leer un árbol de un archivo y al final de la
sesión guardar el nuevo árbol en el archivo. También incluir un método para
imprimir un árbol binario.
6. Escribir una clase que contenga los siguientes métodos para trabajar con
árboles binarios:
(a) Un método recursivo que permita contar las hojas de un árbol binario.
(b) Un método que determine si dos árboles binarios A y B son idénticos o
no.
(c) Un método que obtenga la copia de un árbol binario A.
(d) Un método que muestre los nodos que están en el nivel n de un árbol
binario.
(e) Un método que muestre los nodos que están entre el nivel n y el m de un
árbol binario.
(f) Un método que devuelva true si el parámetro recibido es un árbol binario
completo y false en otro caso.
(g) Un método para determinar si un árbol binario es de búsqueda o no.
140
CAP´
ITULO 5. ´
ARBOLES
(h) Un método para obtener la frontera de un árbol binario definida como la
secuencia de valores de sus hojas, tomados de izquierda a derecha.
(i) Un método que dados un árbol binario y una lista que contiene un
camino, determine si dicho camino existe en el árbol, teniendo en cuenta
que el camino debe comenzar necesariamente en la ra´ız.
(j) Un método para podar un árbol binario dejando una copia del árbol
original hasta el nivel completo de mayor profundidad. Ejemplo:
52
13
52
13
45
26
78
96
45
26
78
96
68
21
19
41
34
83
Figura 5.16. Poda de un árbol
7. Mostrar gráficamente el resultado de insertar 20, 16, 44, 57, 93, 32, 65,
19, 8 y 17 en un árbol AVL inicialmente vac´ıo.
8. Para cada entrada de datos dibujar el árbol AVL correspondiente: (a) 35,
18, 9, 58, 14, 49, 51, 67, 60.
(b) 24, 14, 6, 35, 59, 17, 21, 32, 4, 7, 15, 22.
(c) 13, 7, 21, 15, 27, 18, 4, 11, 30. Luego eliminar: 13, 4, 15.
9. Escribir un método para eliminar un nodo de un árbol binario
balanceado.
10. Escribir un programa para generar un histograma con la cantidad de
veces que se repite cada una de las palabras contenidas en un texto
almacenado en un archivo.
11. Escribir un programa que muestre las l´ıneas en las que aparece cada
palabra de un texto. El texto puede ser un programa o no, y debe leerse de
un archivo. En la salida las palabras deben estar en orden alfabético.
12. En las pasadas olimpiadas se crearon tres árboles binarios de búsqueda,
en cada uno se tiene información de los medallistas. La información
contenida es nombre de la prueba, nombre del deportista y su nacionalidad.
El árbol oro tiene la información de
5.7. EJERCICIOS
141
los medallistas de oro, en el árbol plata los medallistas de plata y en el árbol
bronce los medallistas de bronce. En cada árbol el criterio de ordenamiento
es el nombre del deportista. Escribir un programa para manejar esta
información, en particular interesa que dados el nombre y nacionalidad de
un deportista se puede encontrar el grupo de medallistas del mismo pa´ıs.
También se requiere saber la cantidad y tipo de medallas obtenidas por un
deportista particular, por un pa´ıs particular y obtener la tabla de posiciones
de los pa´ıses que obtuvieron medallas.
13. Suponer que se tiene un archivo con nombres de personas, su CURP,
dirección y teléfono. Se desea hacer un programa para ordenar los registros
de las personas. La restricción es que se requiere crear un árbol binario de
búsqueda en el que en cada nodo se tenga una estructura con los datos de
las personas cuyo nombre empieza con la misma letra, y ésta subestructura
debe estar en orden ascendente.
14. Una cadena de almacenes tiene dividida su área de operaciones en tres:
norte (1), centro (2) y sur (3). Periódicamente hacen rotación de personal
entre las sucursales de las tres áreas. Se tiene un archivo en el que cada
registro contiene el nombre de un vendedor, su CURP y dos números
enteros. Cada entero puede ser 1, 2, 3. El primer entero es la zona en la se
encuentra trabajando el vendedor y el segundo entero es la zona a la que se
cambiará. En el archivo los registros no tienen ningún orden. Se requiere
escribir un programa que almacene los registros en tres árboles AVL, uno
por cada zona y luego realice el intercambio de registros en los árboles para
saber la nueva distribución de los empleados.
15. Suponer que se tiene un tablero, estilo laberinto, y se desea calcular la
trayectoria menos costosa para atravesarlo. El recorrido debe empezar en la
esquina superior izquierda y terminar en la esquina inferior derecha. Sólo es
posible moverse un cuadrado a la vez en l´ınea recta horizontal o vertical,
pero no diagonal. Cada cuadrado tiene un valor positivo que representa el
costo de pasar por ah´ı.
Por ejemplo, el siguiente tablero tiene su entrada en el cuadrado (0,0) y
salida en (2,2): 0
18
En este ejemplo, la trayectoria de costo menor pasa por las celdas (0,0),
(1,0), (1,1), (1,2) y (2,2) y tiene un costo de 9.
142
CAP´
ITULO 5. ´
ARBOLES
Escribir un programa para obtener la trayectoria de menor costo, donde una
trayectoria se define como una secuencia ac´ıclica de celdas. Se pueden
emplear todas las estructuras de datos necesarias, con la restricción que se
debe incluir un árbol binario de búsqueda.
Cap´ıtulo 6
Colas de prioridad
En ocasiones no es necesario que todos los elementos de una colección se
encuentren ordenados, basta con conocer el elemento mayor. Para
aplicaciones donde la tarea más común es localizar, dentro de una
colección, el elemento con prioridad o valor mayor (menor) para su
proceso; en caso de empate en cuanto a prioridad, se toman de acuerdo con
el orden de llegada, se tiene la estructura de datos conocida como cola de
prioridad, la cual se presenta en este cap´ıtulo.
6.1.
Introducci´
on
Una cola de prioridad es una estructura de datos utilizada cuando se
requiere que los elementos se procesen por orden de prioridad y no por el
orden de llegada ni ningún otro. En otras palabras, el orden de eliminación
de elementos se determina de acuerdo con la prioridad que tengan, a
diferencia de las colas vistas en el cap´ıtulo 4 donde este orden está
determinado por el de llegada. De esta definición se deduce que todo
elemento tiene asignada una prioridad, aunque no necesariamente distinta.
La prioridad puede ser establecida, por ejemplo, conforme a la importancia
de la tarea o quizá al tiempo que requiere para terminar, su beneficio a largo
plazo o bien lo divertida que sea.
Las aplicaciones de la colas de prioridad incluyen aquellas en que se
requiere procesar la información de acuerdo con su urgencia. Por ejemplo,
personas que llegan con una urgencia a un hospital son atendidos aunque
haya otras esperando, en general para problemas de organización de
atención a clientes o planeación de trabajos para la mejor distribución de los
recursos. En los sistemas operativos se utilizan para la ejecución de
procesos que comparten el CPU, asignando prioridad al tipo de proceso y
usuario.
143
144
CAP´
ITULO 6. COLAS DE PRIORIDAD
6.2.
El TAD Cola de prioridad
Las operaciones básicas de una cola de prioridad son: encontrar un
elemento, insertar un nuevo elemento y eliminar un elemento en la cola,
aunque sólo se elimina el elemento con máxima prioridad, definida ésta en
la aplicación. Por lo tanto, la interfaz para las colas de prioridad es la
siguiente:
public interface EncolableConPrioridad {
public boolean estaVacia();
public void vaciar();
public int tamanio ();
public Object obtenerPrimero();
public void eliminar();
public void agregar (Object elemento);
public [Link] iterador();
Las operaciones básicas con las colas de prioridad son:
• estaVacia. Devuelve true si la cola de prioridad está vac´ıa y false en otro
caso.
• vaciar. Elimina todos los elementos de la cola de prioridad, es decir, la
deja vac´ıa.
• tamanio. Devuelve la cantidad de elementos que hay en la cola de
prioridad.
• obtenerPrimero. Devuelve el elemento que tiene la mayor prioridad. El
tama˜no de la cola de prioridad no se altera.
• eliminar. Elimina el elemento que tiene la mayor prioridad y el tama˜no de
la cola de prioridad disminuye en una unidad.
• agregar. Agrega un elemento a la cola de prioridad. El tama˜no de la cola
aumenta en una unidad.
• iterador. Devuelve un iterador sobre la cola de prioridad. El tama˜no de la
cola no se altera.
6.3.
Aplicaciones de colas de prioridad
Como se dijo antes, las aplicaciones de las colas de prioridad son aquellas
que requieren procesar los elementos de acuerdo con su urgencia o
prioridad. En esta sección se presentan tres ejemplos de uso de colas de
prioridad. El primero es un método para ordenar datos, el segundo es un
problema de optimización en la distribución de recursos compartidos y el
tercero es un ejemplo de simulación, amplio campo para el uso de esta
estructura de datos.
6.3. APLICACIONES DE COLAS DE PRIORIDAD
145
6.3.1.
Ordenamiento
Uno de los problemas más trabajados en la computación es definir métodos
eficientes para ordenar datos. Una aplicación de las colas de prioridad es la
creación de un método eficiente para ordenar los elementos de un arreglo,
conocido como heapsort. El algoritmo es muy sencillo, consiste en tomar el
primer elemento de la cola de prioridad (el menor) colocarlo en un arreglo,
eliminarlo de la cola y repetir este par de operaciones hasta que la cola esté
vac´ıa.
Ejemplo 6.1. El método ordenar es una implementación del heapsort.
Devuelve un arreglo con los datos ordenados. Al empezar se hace una copia
de la cola de prioridad original para no alterarla al extraer elementos.
/**
* Método para ordenar utilizando el algoritmo heapsort
* @return Object[] -- arreglo con los objetos ordenados.
*/
public Object[] ordenar() {
Object [] ordenados = new Object[nElementos];
Heap copia = new Heap (this);
for (int i = 0; i < [Link]; i++) {
ordenados[i] = [Link]();
[Link]();
return ordenados;
En este caso, el algoritmo es de O( n log n), que aunque parece menos
eficiente que el algoritmo por recipientes o binsort presentado en el cap
´ıtulo de listas, se debe recordar que aquel algoritmo es para datos con
caracter´ısticas especiales y el algoritmo aqu´ı presentado es para
colecciones de datos de cualquier clase.
Este método se presenta en la sección de aplicaciones para resaltarlo, sin
embargo se incluye en la clase que implementa el TAD para las colas de
prioridad.
6.3.2.
Planeaci´
on de trabajos
Suponer que se tiene un lugar con n máquinas iguales y se deben procesar
m trabajos en ellas. Se conoce la cantidad de tiempo de máquina que
requiere cada trabajo para realizarse.
El problema consiste en hacer una planeación del orden en que los trabajos
se van a realizar, teniendo en cuenta que las máquinas procesan sólo un
trabajo a la vez y una vez asignado el trabajo a una máquina, éste se procesa
completamente en la máquina asignada.
Si la cantidad de trabajos es menor que el número de máquinas disponibles
no existe ningún problema, pues se asigna a cada máquina un trabajo. El
problema está cuando se tienen más
146
CAP´
ITULO 6. COLAS DE PRIORIDAD
trabajos que máquinas, por ejemplo, si se tienen 3 máquinas y 7 trabajos
con 2, 14, 4, 16, 6, 5 y 3 unidades de tiempo necesarias para su respectiva
realización. Una planeación posible es formar todos los trabajos en una cola
o una lista e irlos asignando a cada máquina disponible conforme se
desocupen éstas. As´ı se coloca el trabajo 1 en la máquina 1, el 2 en la 2 y
el 3 en la 3. Cuando han transcurrido 2 unidades de tiempo se desocupa la
primera máquina y en ese punto se le asigna el trabajo 4, etcétera (figura
6.1.). En este caso, la planeación ocupa 18 unidades de tiempo.
M3
T3 (4ut)
T5 (6ut)
T6 (5ut)
M2
T2 (14ut)
T7 (3ut)
M1 T1(2ut)
T4 (16ut)
4
10
14 15
17 18
Figura 6.1. Planeación para tres máquinas.
Si se requiere que la planeación ocupe el menor tiempo posible las
máquinas, para ello se van asignando los trabajos de mayor duración a las
máquinas en la primera que esté desocupada.
As´ı se coloca el trabajo 4, que es el que toma más tiempo, en la máquina 1,
el trabajo 2 en la máquina 2 y el 5 en la 1, después de 6 unidades de tiempo
se coloca el trabajo 6 en la máquina 3, etc. En este caso la planeación queda
como se muestra en la figura 6.2. y sólo ocupa 17 unidades de tiempo.
M1
T4 (16ut)
M2
T2 (14ut)
T7 (3ut)
M3
T5 (6ut)
T6 (5ut)
T3 (4ut)
T1(2ut)
0
6
11
15
17
Figura 6.2. Planeación para tres máquinas usando una cola de prioridad.
El programa que se presenta a continuación ordena los trabajos de mayor a
menor duración utilizando el algoritmo de ordenamiento presentado en la
sección anterior, luego crea una cola de prioridad con las máquinas
utilizando como elemento de comparación el tiempo en que se desocupa
cada una. Después mientras haya trabajos que realizar, toma la primera
máquina que se desocupa y le asigna un nuevo trabajo. La máquina se
vuelve a colocar en la cola de prioridad. Cada vez que se coloca un trabajo
en una máquina se muestra éste y su duración, con lo cual se va
imprimiendo la planeación requerida.
6.3. APLICACIONES DE COLAS DE PRIORIDAD
147
El programa para las planeaciones hace uso de objetos de dos clases, que
por su sencillez no se presentan en esta sección. La clase Trabajo, en la cual
se tiene un identificador del trabajo y su duración con los métodos
necesarios para actualizar y conocer estos datos. La clase Maquina tiene un
identificador para ella y el momento en que se desocupa. Además se
requieren dos clases que implementen la interfaz Comparator, una para
comparar trabajos y la otra para comparar máquinas.
Ejemplo 6.2. Método para organizar trabajos en la menor cantidad de
máquinas posible, para ello utiliza una cola de prioridad.
/** Método para organizar el proceso de trabajos
* @param trab -- arreglo con los trabajos que se desean realizar
* @param nMaq -- entero que indica la cantidad de máquinas disponibles.
*/
public void planear(Trabajo [] trab, int nMaq) {
if ([Link] <= nMaq){ // Hay menos trabajos que máquinas
[Link]("Asigna un trabajo a una máquina diferente"); return;
Heap h = new Heap(trab, [Link], new ComparaTrabajos()); Object []
trb = [Link]();
//Ordena ascendentemente usando el heapsort
Heap heapMaq = new Heap(new ComparaMaquinas());
for (int i = 1; i <= nMaq; i++) {
[Link](new Maquina(""+i, 0));
//Genera la planificacion
for (int i=0; i< [Link]; i++) {
Maquina maq = (Maquina) [Link]();
[Link]();
[Link]("El trabajo "+((Trabajo)trb[i]).obtenerID()+
" se realizo en la maquina"+
[Link]()+ " del tiempo "+[Link]());
[Link]([Link]() +
((Trabajo)trb[i]).obtenerDuracion());
[Link](new Trabajo(((Trabajo)trb[i]).obtenerID(), tLibre,
[Link]()));
[Link]("al tiempo "+[Link]());
[Link](maq);
Ejemplo 6.3. Ejemplo de salida del programa anterior.
148
CAP´
ITULO 6. COLAS DE PRIORIDAD
Maquina 1:
(T4,0,16)
Maquina 2:
(T7,14,17) (T2,0,14)
Maquina 3:
(T1,15,17) (T3,11,15) (T6,6,11) (T5,0,6)
6.3.3.
Simulaci´
on
Suponer que se tiene el problema de determinar la cantidad de cajeros
necesarios en un banco para que los clientes no tengan que esperar
demasiado en la fila ni que los cajeros estén sin trabajo. Una forma de
resolver este problema es contratar cajeros conforme a la necesidad, la otra
es simular el comportamiento del banco y estimar la cantidad de cajeros
necesarios.
En esta sección se presenta la solución tomando la segunda opción.
Una simulación conducida por eventos discretos es aquella en la que los
objetos en el mundo real se modelan como objetos en la simulación con
comportamiento lo más parecido posible al real. Los eventos discretos son
aquellos que ocurren en un instante del tiempo, por ejemplo: oprimir el
botón de un elevador, encender un auto, apagar un auto, la llegada de un
cliente, la salida de un cliente, prender la luz. El traslado de un sitio A a uno
B no es un evento discreto, sin embargo se puede modelar como dos
eventos separados: salir de A y llegar a B; si se asocia un valor de tiempo
con cada evento discreto se puede modelar la duración de esta actividad.
En una simulación conducida por eventos discretos los eventos se
almacenan en una cola de prioridad, de acuerdo al momento en que el
evento debe ocurrir, de tal manera que el menor elemento siempre es el
siguiente evento que será modelado. Todo evento que ocurre puede disparar
otros eventos que a su vez son colocados en la cola.
A grandes rasgos la simulación trabaja como sigue:
1. Inicializar el estado, es decir, todas las variables del sistema.
2. Poner el reloj en cero.
3. Inicializar la cola de prioridad.
4. En tanto la cola de prioridad no esté vac´ıa y el tiempo de simulación no
haya expirado, realizar lo siguiente:
(a) Extraer un evento de la cola.
(b) Actualizar el reloj de acuerdo con la hora marcada en el evento.
(c) Actualizar el estado del sistema en función del estado anterior al evento.
(d) Actualizar la cola.
6.3. APLICACIONES DE COLAS DE PRIORIDAD
149
En esta sección se presenta un marco genérico para programar simulaciones
y un ejemplo de cómo utilizarlo. Como se puede observar, las clases
importantes para una simulación son los eventos y la propia simulación, por
lo tanto es común definir una clase abstracta Evento de la cual se definan
las subclases concretas para una simulación particular.
Clase 6.1. Clase abstracta Evento, que es la ra´ız de la jerarqu´ıa de eventos
en una simulación. Se define el atributo hora, que es una constante en la
cual se marca el momento en que debe ocurrir el evento.
/**
* Clase abstracta que se implementa en las clases concretas de una
simulacion
* @author Amparo López Gaona
*/
public abstract class Evento implements Comparable {
protected final int hora;
M´
etodo 6.1. Constructor que recibe la hora inicial del evento.
/**
* Constructor que recibe la hora inicial del evento
* @param horaInicial -- hora inicial del evento
*/
public Evento (int horaInicial) {
hora = horaInicial;
M´
etodo 6.2. Método compareTo para determinar la relación de orden entre
dos eventos de acuerdo con la hora en que suceden.
/**
* Comparador de las horas entre eventos
* @param obj -- objeto que cuya hora se compara con el evento actual
* @return int -- entero resultado de la comparación. Positivo si el
* evento actual es posterior al parámetro; negativo si es anterior y
* cero si son iguales
*/
public int compareTo (Object o) {
Evento evento = (Evento) o;
if (hora < [Link]) return -1;
if (hora == [Link]) return 0;
return 1;
}
150
CAP´
ITULO 6. COLAS DE PRIORIDAD
M´
etodo 6.3. Método abstracto procesarEvento. Los eventos propios de la
simulación se programan como subclases de Evento y por lo tanto están
obligados a implementar el método procesarEvento.
/**
* Método abstracto
*/
abstract void procesarEvento ();
Clase 6.2. La clase Simulacion proporciona las estructuras para las
actividades de la simulación, éstas son la hora en que se registra un evento y
una cola de prioridad con los eventos por ocurrir. También se tienen los
métodos para registrar los eventos y realizar la simulación. Se tienen dos
constructores, uno que recibe un comparador que se pasa a la cola de
prioridad y el constructor por omisión, en el cual se proporciona un
comparador.
/**
* Clase para hacer simulaciones
* @author Amparo López Gaona
*/
public class Simulacion {
private int horaActual;
private Heap eventos;
/**
* Constructor por omisión
*/
public Simulacion() {
this (new MiComparador());
/**
* Constructor que recibe un comparador
*/
public Simulacion(Comparator cmp) {
horaActual = 0;
eventos = new Heap(cmp);
Clase 6.3. Comparador por omisión para la clase Simulacion en el caso de
que el usuario no proporcione ninguno.
/**
* Comparador para ser usado en caso de que el usuario no proporcione
ninguno
6.3. APLICACIONES DE COLAS DE PRIORIDAD
151
*/
private class MiComparador implements Comparator {
public int compare (Object o1, Object o2) {
Comparable oo1 = (Comparable) o2;
return [Link] (o2);
public boolean equals (Object obj) {
return compare(this, obj) == 0;
M´
etodo 6.4. El método registrarEvento de la clase Simulacion se encarga de
agregar el evento a la cola. El método hora permite conocer la hora que se
está simulando. El método ejecutar toma un evento de la cola, lo procesa y
actualiza la hora de la simulación. Este método es el corazón de la
simulación y trabaja mientras haya elementos en la cola.
/**
* Método para registrar un evento en la simulacion
* @param nuevoEvento -- evento que sera registrado
*/
public void registrarEvento (Evento nuevoEvento) {
[Link](nuevoEvento);
M´
etodo 6.5. Método para obtener la hora actual del evento.
/**
* Método para obtener la hora actual del evento
* @return int -- hora actual
*/
public int hora () {
return horaActual;
M´
etodo 6.6. Método para realizar la simulación, toma el primer elemento de
la cola, actualiza la hora actual y procesa el siguiente evento.
/**
* Metodo ejecutar
*/
public void ejecutar () {
while (! [Link]()) {
152
CAP´
ITULO 6. COLAS DE PRIORIDAD
Evento siguienteEvento = (Evento) [Link]();
[Link]();
horaActual = [Link];
[Link]();
Para el problema del banco se consideran como eventos un cliente que llega
al banco, un cliente que espera en la fila hasta que alguno de los cajeros está
disponible y un cajero atendiendo a un cliente. Se requiere conocer la hora
de llegada de cada cliente y el tiempo estimado de las operaciones que va a
realizar, para ello se utilizará una función generadora de números aleatorios
con cierta distribución, por ejemplo, puede utilizarse una distribución
uniforme para el tiempo de atención y una exponencial para la llegada de
los clientes. Como se especificó al inicio de la sección, se desea saber el
tiempo promedio que el cliente debe esperar, la longitud promedio de la fila
y la cantidad de cajeros necesarios en el banco para que los clientes no
tengan que esperar demasiado ni les falte trabajo a los cajeros.
Ejemplo 6.4. Si se tienen dos cajeros y cuatro clientes con la siguiente
información: Persona: 1 llega al banco a las 1.5. Tiempo de operaciones:
5.7
Persona: 2 llega al banco a las 2.8. Tiempo de operaciones: 1.9
Persona: 3 llega al banco a las 3.3. Tiempo de operaciones: 8.7
Persona: 4 llega al banco a las 9.2. Tiempo de operaciones: 2.7
El funcionamiento de la simulación ser´ıa el siguiente:
• Hora 1.5: La persona 1 llega al banco.
Como los dos cajeros están desocupados, el cajero 1 atiende las operaciones
de la persona 0. Las operaciones toman 5.7 minutos, as´ı que termina a las
7.2.
• Hora 2.8: La persona 2 llega al banco.
El cajero 2 está desocupado, as´ı que atiende a este cliente. Las operaciones
toman 1.9
minuto, por lo tanto termina a las 4.7.
• Hora 3.3: La persona 3 llega al banco.
Ambos cajeros están ocupados, as´ı que esta persona debe esperar en la fila.
• Hora 4.7: El cajero 2 se desocupa.
El cajero 2 atiende a la persona 3. Las operaciones de este cliente toman 8.7
minutos, as´ı que estará ocupado hasta las 13.4.
• Hora 7.2: El cajero 1 se desocupa.
6.3. APLICACIONES DE COLAS DE PRIORIDAD
153
• Hora 9.2: La persona 4 llega al banco.
El cajero 1 está desocupado, as´ı que atiende a la persona 4. Las
operaciones tardan 2.7
minutos, por lo que estará ocupado hasta las 11.9.
• Hora 11.9: El cajero 1 se desocupa.
• Hora 13.4: El cajero 2 se desocupa. Como no hay más clientes la
simulación termina.
Analizando los datos arrojados por la simulación se tiene que el tiempo de
espera, en promedio, es de 0.35, ya que las personas 1 , 2 y 4 fueron
atendidas inmediatamente, es decir, no esperaron nada y la tercera persona
esperó 1.4 unidades de tiempo. Lo cual es buen tiempo de espera. Con
respecto al tiempo de ocio (sin clientes) de los cajeros, se tiene que el cajero
1 estuvo desocupado 1.5 + 2 + 0.1 = 3.6 y el segundo 2.8 + 0 = 2.8, lo que
da un promedio de 3.2.
Si se tuvieran tres cajeros la simulación dar´ıa el siguiente resultado: el
tiempo promedio de espera ser´ıa cero, pero el tiempo de ocio ser´ıa de
5.83.
De los eventos especificados antes se puede deducir que las clases
necesarias para la simulación son Cliente, Cajero y Banco. La estructura de
la clase Cliente consta de un identificador, la hora de llegada y la duración
de las operaciones. También proporciona los constructores necesarios y
métodos para acceso y modificación de los atributos de la estructura. La
clase Cajero tiene como atributos de los cajeros su identificador y variables
necesarias para las estad´ısticas, como son el momento en que inicia su
descanso, el tiempo ocupado y los clientes atendidos por cada cajero.
Ambas clases son muy sencillas y se omite su implementación.
Clase 6.4. La clase Banco tiene una cola de prioridad para formar ah´ı a los
cajeros de acuerdo con la hora en que se desocupan, una cola para formar a
los clientes que esperan un cajero desocupado, un arreglo con los clientes
que llegan al banco, un objeto para la simulación y variables para las estad
´ısticas.
/**
* Clase para la simulacion del flujo de clientes en un banco
* @author Amparo López Gaona
*/
public class Banco {
private static Heap cajeros;
private Cliente [] clientes;
private Simulacion simulacion;
private static Cola cola = new Cola() ;
private static double tiempoDeEspera;
private static int numClientes;
private final int nCajeros;
154
CAP´
ITULO 6. COLAS DE PRIORIDAD
M´
etodo 6.7. El constructor de la clase Banco recibe a los clientes y la
cantidad de cajeros que tendrá el banco, inicializa todas las variables. y
registra la llegada de todos los clientes.
/**
* Constructor para la simulacion de un banco
* @param clien -- Clientes que arriban al banco
* @parm numCajeros -- cantidad de cajeros que hay en el banco
*/
public Banco (Cliente[] clien, int numCajeros) {
clientes = clien;
nCajeros = numCajeros;
cajeros = new Heap(new comparadorCajero()) ;
tiempoDeEspera = 0;
numClientes = 0;
simulacion = new Simulacion();
for (int i=0; i < nCajeros; i++)
[Link](new Cajero());
for (int i = 0; i < [Link]; i++)
[Link](
new Llegada(clientes[i].obtenerHoraDeLlegada(), clientes[i]));
En la clase Banco se definen los eventos de la simulación como clases
privadas que extienden la clase Evento antes definida.
Clase 6.5. La clase Llegada registra la llegada de un cliente y con el método
procesarEvento se determina el cajero que atenderá al cliente, si el cajero
está desocupado se atiende al cliente y si no se activa el evento
EsperarCajeroDesocupado para simular la espera del cliente a que se
desocupe un cajero.
private class Llegada extends Evento {
private Cliente cliente;
/**
* Registra la llegada de un cliente.
* @param double -- hora de llegada de un cliente
* @param Cliente -- cliente que llega al banco
*/
Llegada (double hora, Cliente cl) {
super(hora);
cliente = cl;
6.3. APLICACIONES DE COLAS DE PRIORIDAD
155
/**
* Procesa la llegada del cliente al banco
*/
public void procesarEvento () { //Llegada cliente
if (![Link]()) {
Cajero cajero = (Cajero) [Link]();
[Link]();
double descanso = [Link]();
if (hora < descanso)
hora = descanso;
[Link] (new AtencionCliente(hora, cliente, cajero));
} else //Espera a que se desocupe el primer cajero
[Link] (
new EsperaCajeroDesocupado(hora+0.1, cliente, true));
Clase 6.6. La clase AtencionCliente registra la hora en que un cliente es
atendido por un cajero y actualiza las variables para las estad´ısticas.
private class AtencionCliente extends Evento {
private Cliente cliente;
private Cajero cajero;
/**
* Registra la hora de atención al cliente
* @param double -- hora de atención al cliente
* @param Cliente -- cliente que se atiende en este momento
* @param Cajero -- cajero que atiende al cliente
*/
AtencionCliente (double hora, Cliente cl, Cajero caj) {
super(hora);
cliente = cl;
cajero = caj;
}
/**
* Método que actualiza los datos para las estad´
ısticas y vuelve a
* poner disponible al cajero que atendió al cliente
*/
public void procesarEvento () {
double espera = hora - [Link]();
156
CAP´
ITULO 6. COLAS DE PRIORIDAD
tiempoDeEspera += espera;
//Acumula tiempo de espera de los clientes
numClientes++;
// Otro cliente atendido
// Actualiza datos para la estad´
ıstica
[Link]([Link]());
[Link]();
[Link]([Link]());
//Actualiza hora de descanso
[Link](hora+[Link](
)); [Link](cajero);
Clase 6.7. La clase EsperaCajeroDesocupado se encarga de poner en cola
de espera a un cliente hasta que un cajero se desocupe. Una vez que hay un
cajero disponible para atender al cliente se dispara el evento
AtencionCliente.
private class EsperaCajeroDesocupado extends Evento {
/**
* Pone un cliente en la cola en espera de que se desocupe un cajero
* @param double -- hora en que entra a la cola de espera un cliente
* @param Cliente -- cliente que espera a que lo atiendan
* @param boolean -- para indicar si el cajero es uno nuevo en la cola
*/
EsperaCajeroDesocupado (double hora, Cliente cliente, boolean nuevo) {
super (hora);
if (nuevo)
[Link](cliente);
/**
* Método que pone en la cola de espera a un cliente hasta que un cajero
* se desocupe.
*/
public void procesarEvento () { //Espera cajero desocupado Cliente cliente
= (Cliente) [Link]();
if ([Link]()) {
[Link](new EsperaCajeroDesocupado(
hora+0.1, cliente, false));
return;
Cajero cajero = (Cajero) [Link]();
[Link]();
[Link]();
6.4. IMPLEMENTACI ´
ON DEL TAD COLA DE PRIORIDAD
157
double descanso = [Link]();
if (hora <= descanso) //El cajero está desocupado hora = descanso;
[Link](new AtencionCliente(hora, cliente, cajero));
}
Clase 6.8. Clase para determinar, cuando hay dos cajeros, cuál se desocupa
primero.
private class comparadorCajero implements [Link] {
public int compare (Object uno, Object dos) {
Cajero c1 = (Cajero) uno;
Cajero c2 = (Cajero) dos;
if ([Link]() < [Link]()) return -1;
else if ([Link]() > [Link]()) return 1;
return 0;
public boolean equals (Object obj) {
return compare(this, obj) == 0;
Una vez definida la clase Banco, lo único que queda es programar una clase
que contenga un método main, en el cual se definan los datos necesarios
para la simulación, se arranque ésta y se muestren las estad´ısticas.
6.4.
Implementaci´
on del TAD Cola de prioridad
Existen varias posibilidades de implementar una cola de prioridad: una
forma es utilizar una lista, donde se insertan los elementos al final, con lo
cual la inserción es rápida y de orden constante. Para encontrar el elemento
menor se recorre la lista, pero esto es una tarea de orden O( n). En resumen,
esta es una forma sencilla de implementar las colas de prioridad, pero no es
la más eficiente.
Una segunda opción para la implementación de una cola de prioridad es
utilizar una lista ordenada, aunque la localización y eliminación del
elemento de mayor prioridad es rápida, de orden O(1), se presenta un
problema en la inserción de datos, pues mantener la lista ordenada puede
ser de orden O( n).
158
CAP´
ITULO 6. COLAS DE PRIORIDAD
La tercera opción es con un árbol binario de búsqueda. El problema es que
para encontrar el menor elemento hay que recorrer todo el lado izquierdo,
tarea de orden O(log n), que no está mal pero puede mejorarse.
Una cuarta posibilidad es utilizar un árbol binario completo con ciertas
caracter´ısticas. Un árbol binario completo es un árbol binario en el que
todos los niveles del árbol, excepto el último, están llenos y los elementos
del último nivel están colocados de izquierda a derecha sin dejar huecos
entre ellos.
En la figura 6.3. se tienen dos árboles que no son completos. El primero
porque no está lleno hasta el penúltimo nivel. El segundo porque aunque el
penúltimo nivel del árbol está lleno, en el último nivel se tiene un hueco
entre el nodo I y el J, pues el nodo E no tiene hijos.
A
B
Figura 6.3. Ejemplos de árboles binarios incompletos.
En la figura 6.4. se tiene un árbol binario completo.
B
C
Figura 6.4. Ejemplo de árbol binario completo.
Entre las caracter´ısticas de un árbol binario completo está el hecho de que
su altura es siempre n debido a que está balanceado, esto sugiere que las
operaciones de búsqueda, inserción y supresión tomarán en el peor caso
O(log n).
6.4. IMPLEMENTACI ´
ON DEL TAD COLA DE PRIORIDAD
159
Una propiedad importante de un árbol binario completo es que puede
representarse eficientemente en un arreglo, en el cual dado el ´ındice de un
nodo es fácil determinar su padre y sus hijos (si los tiene). En la posición
cero se tiene la ra´ız del árbol. El hijo izquierdo de cualquier nodo está en la
posición 2 n + 1 y el hijo derecho está en la 2 n + 2. El padre de un nodo
que está en la posición par n se encuentra en la posición n/ 2 − 1 y si el
nodo está en una posición impar el padre está en la posición ( n − 1) / 2.
El arreglo correspondiente al árbol binario completo de la figura 6.4. se
presenta en la figura 6.5. Nótese que el arreglo no tiene localidades
intermedias vac´ıas.
CDE
GHI
9 10
Figura 6.5. Representación de un árbol completo en un arreglo.
Un heap es un árbol binario completo en el cual el valor de cada nodo es
menor o igual que el de sus hijos. Por tanto, el nodo ra´ız es el menor
elemento (es decir, el de mayor prioridad) del árbol.
Se pueden tener varios heaps con el mismo conjunto de datos, porque no
hay restricción en cuanto a la ubicación de los elementos que se tiene en los
árboles binarios de búsqueda.
En la figura 6.6. se muestran dos heaps para los números 1, 2, 4, 8, 7, 5, 6,
9, 10, 11. Un arreglo ordenado es un heap, pero un heap no siempre es un
arreglo ordenado.
11
5
10
79
10
9 11
Figura 6.6. Ejemplo de dos heaps con los mismos datos.
Una forma elegante y eficiente de implementar colas de prioridad es
mediante un heap.
Cuando un heap se implementa trabajando con el valor m´ınimo se le llama
min-heap, mientras que si se trabaja con el valor máximo se le llama max-
heap.
Clase 6.9. Clase para crear el TAD Heap para trabajar colas de prioridad
usando heaps.
Requiere de un arreglo en donde se mantiene el heap de un comparador y
un contador de elementos.
160
CAP´
ITULO 6. COLAS DE PRIORIDAD
/**
* Clase para crear y trabajar colas de prioridad utilizando heaps.
* @author Amparo López Gaona.
*/
class Heap implements EncolableConPrioridad{
private Object [] elementos;
private Comparator prueba;
private int nElementos;
M´
etodo 6.8. Constructor que recibe un comparador para los elementos de la
cola de prioridad.
/**
* Constructor que recibe un comparador
* @param Comparator -- comparador para la prioridad de los elementos
*/
public Heap(Comparator c) {
this(c, 100);
M´
etodo 6.9. Constructor que recibe un comparador para los elementos de la
cola de prioridad y el máximo de elementos permitido en ella.
/**
* Constructor que recibe un comparador y el tama~
no máximo del heap
* @param Comparator -- comparador para la prioridad de los elementos
* @param int -- tama~
no máximo del heap
*/
public Heap(Comparator c, int tam) {
prueba = c;
elementos = new Object[(tam > 0) ? tam : 100];
nElementos = 0;
M´
etodo 6.10. Constructor para crear una cola de prioridad a partir de un
arreglo de objetos, también se requiere de un comparador. Para hacer su
trabajo llama al método interno crearHeap que se presenta adelante.
/**
* Constructor que crea una cola de prioridad a partir de un arreglo
* @param Comparator -- comparador para la prioridad de los elementos
* @param Object [] -- arreglo de objetos que formarán el heap.
*/
6.4. IMPLEMENTACI ´
ON DEL TAD COLA DE PRIORIDAD
161
public Heap(Comparator c, Object [] datos) {
prueba = c;
nElementos = [Link];
elementos = new Object[nElementos*2];
for (int i = 0; i < nElementos; i++)
elementos[i] = datos[i];
crearHeap(elementos);
M´
etodo 6.11. Constructor de copia. Éste construye una copia del heap que
recibe como parámetro.
/**
* Constructor de copia
* @param Heap -- heap del cual se creará una copia
*/
public Heap(Heap hep) {
nElementos = [Link];
elementos = new Object[nElementos];
cmp = [Link];
for (int i = 0; i < nElementos; i++)
elementos[i] = [Link][i];
M´
etodo 6.12. El método para determinar si la cola de prioridad está vac´ıa
sólo verifica si el número de elementos en ella es diferente de cero.
/**
* Método para determinar si la cola de prioridad está vac´
ıa
* @return boolean -- Devuelve true si la cola está vac´
ıa y false en
otro caso
*/
public boolean estaVacia() {
return
nElementos == 0;
M´
etodo 6.13. Método para vaciar una cola de prioridad, para ello
simplemente indica que la cantidad de elementos en ella es cero.
/**
Método para eliminar todos los elementos de la cola de prioridad.
*/
162
CAP´
ITULO 6. COLAS DE PRIORIDAD
public void vaciar() {
nElementos = 0;
M´
etodo 6.14. El método para saber la cantidad de elementos almacenados en
la cola de prioridad devuelve el valor de la variable definida para ello.
/**
* Método para conocer la cantidad de elementos que tiene la cola
* de prioridad.
* @return int -- número de elementos en la cola de prioridad
*/
public int tamanio() {
return
nElementos;
M´
etodo 6.15. El método para obtener el primer elemento de la cola de
prioridad no altera el estado de ésta.
/**
Método para obtener el primer elemento de la cola de prioridad
* @return Object -- el primer elemento en la cola de prioridad
*/
public Object obtenerPrimero() {
return elementos[0];
Para insertar un nuevo elemento en la cola de prioridad se coloca al final
del árbol. Con esto se sigue teniendo un árbol binario completo, sin
embargo puede no ser un heap si el elemento es menor que el padre. Por lo
tanto, repetidamente se intercambia el nodo que tiene el nuevo elemento
con su padre hasta tener un heap. Este método es de orden O(log n).
Por ejemplo, si se tiene la situación en que se requiere insertar el número 3
en el heap que se muestra en la parte izquierda de la figura 6.7. se coloca
como hijo del 8, y como es se deben intercambiar. Ahora el 3 queda como
hijo del 7 y como es menor se intercambian y queda en su lugar, pues el 3
es mayor que el 2.
M´
etodo 6.16. El método agregar tiene la implementación del algoritmo
descrito. Recibe el objeto que deberá colocar como nuevo elemento en la
cola de prioridad. Si el arreglo se llena se hace crecer como en el método
1.23.
/**
* Metodo para agregar un nuevo elemento en la cola de prioridad
* @param valor -- Objeto que se va a insertar
6.4. IMPLEMENTACI ´
ON DEL TAD COLA DE PRIORIDAD
163
10
13
16
10
13
16
10
13
16
15
12 11
15
12 11
15
12 11
Figura 6.7. Inserción de un elemento en un heap.
*/
public void agregar(Object valor) {
int posicion = nElementos;
// Va agregar el dato al final
int posPadre = (nElementos -1)/2;
Object valorPadre = null;
if (posicion == [Link]) {
crecerArreglo();
elementos[posicion] = valor;
if (posPadre >=0)
valorPadre = elementos[posPadre];
while((posicion > 0) && ([Link](valor, valorPadre) < 0)) {
elementos[posicion] = valorPadre;
posicion = posPadre;
posPadre = (posicion -1)/2;
if (posPadre >= 0)
valorPadre = elementos[posPadre];
elementos[posicion] = valor;
nElementos++;
Notar que insertar un elemento en una cola de prioridad toma un tiempo de
O(log n) debido a que la máxima cantidad de intercambios posibles es el
número de niveles del árbol, es decir, 1+log2( n).
En esta estructura de datos el único elemento que se puede borrar es el
primero, y esto lo hace el método eliminar, sustituyendo la ra´ız por el
último elemento del árbol. Sin embargo,
164
CAP´
ITULO 6. COLAS DE PRIORIDAD
esto puede alterar la condición de ser un heap, con lo cual se va recorriendo
el árbol hacia abajo para encontrar su lugar v´ıa el método ajustarHeap.
Por ejemplo, si se requiere eliminar la ra´ız del heap mostrado en la parte
izquierda de la figura 6.8., se coloca el 8 como ra´ız. Debido a que es mayor
que sus hijos se intercambia con el 3, que es el menor hijo. Como es mayor
que sus hijos, se intercambia con el 7 y termina (figura 6.8.).
10
7
13
16
10
13
16
10
13
16
15
12 11
15
12 11
15
12 11
9
10
13
16
15
12 11
Figura 6.8. Supresión del elemento menor en un heap.
M´
etodo 6.17. El método eliminar tiene la implementación del algoritmo para
eliminar el elemento de menor prioridad de una cola con prioridad, el cual
es el único que se puede eliminar de ésta.
/** Metodo para eliminar el elemento de menor prioridad
*/
public void eliminar() {
int ultimaPos = tamanio() - 1;
if (ultimaPos != 0)
// Elimina el primer elemento
elementos[0] = elementos[ultimaPos];
nElementos--;
ajustarHeap(0,nElementos);
}
6.4. IMPLEMENTACI ´
ON DEL TAD COLA DE PRIORIDAD
165
El método ajustarHeap se encarga de restablecer el heap, para ello se
examinan los dos hijos del nodo que está en la posición indicada (pos). Si el
menor de los hijos es mayor que el padre se tiene un heap. En caso
contrario se deben intercambiar ambos valores. Al hacer esto, la nueva ra´ız
es menor que ambos elementos. Ahora es necesario continuar examinando
el subárbol cuya ra´ız es el elemento donde se modificó el valor en el
intercambio anterior. La posición del hijo menor es el nuevo valor de la
variable pos y el ciclo continúa. El proceso termina cuando el valor en
cuestión encuentra su ubicación (siendo el menor de los dos hijos) o bien se
llegó a un lugar sin hijos.
M´
etodo 6.18. Método interno para restablecer la propiedad de heap en un
arreglo, según lo descrito en el párrafo anterior.
/*
* Método privado para ajustar un heap
* @param pos -- posición del nodo a partir del cual va a verificar que
se tenga un heap
* @param tamanio -- tama~
no del arreglo
*/
private void ajustarHeap(int pos, int tamanio) {
Object valor = elementos[pos];
while (pos < tamanio) {
int posHijo = pos *2 + 1;
if (posHijo < tamanio) { //Encuentra la pos del hijo menor.
if (((posHijo + 1) < tamanio) &&
[Link](elementos[posHijo+1], elementos[posHijo]) <0) posHijo++;
if ([Link](valor, elementos[posHijo]) < 0) {
elementos[pos] = valor;
return;
} else {
elementos[pos] = elementos[posHijo];
pos = posHijo;
} else {
elementos[pos] = valor;
return;
M´
etodo 6.19. El método crearHeap es un método interno que crea un heap a
partir de un arreglo de objetos, para ello se llama al método ajustarHeap con
la mitad de los elementos.
Se empieza desde la mitad pues de la mitad más uno todos son hojas y por
definición una hoja es un heap.
166
CAP´
ITULO 6. COLAS DE PRIORIDAD
/*
* Método para crear un heap a partir de un arreglo.
* @param a -- arreglo con los objetos que se almacenaran en el heap
*/
private void crearHeap(Object[] a) {
int max = nElementos;
for (int i = max/2; i >=0 ; i--)
ajustarHeap(i,max);
Este método privado se utiliza también para el constructor que recibe un
arreglo de objetos.
El tiempo que toma cada una de las operaciones sobre las colas de prioridad
con la implementación anterior se muestra en la tabla 6.1..
Operación
Tiempo de ejecución
constructores
O(1)
constructor que recibe un arreglo
O(log n)
constructor que recibe un heap
O( n)
agregar
O(log n)
ajustarHeap
O(log n)
crearHeap
O( n log n)
estaVacia
O(1)
eliminar
O(log n)
heapsort
O( n log n)
obtenerPrimero
O(1)
tamanio
O(1)
vaciar
O(1)
Tabla 6.1. Tiempo de ejecución de los métodos de la clase Heap.
6.5.
Ejercicios
1. ¿Qué es un heap y qué es una cola de prioridad?
2. Dibujar un heap en el cual se van agregando los siguientes elementos en
el orden en que aparecen listados 6 , 50 , 11 , 25 , 42 , 20 , 104 , 76 , 19 , 55
, 88 , 2.
3. Eliminar tres elementos del heap construido en la pregunta anterior y
dibujarlo para mostrar cómo queda una vez eliminados los tres elementos.
6.5. EJERCICIOS
167
4. Dado un árbol binario completo con 15 nodos, ¿cuántos nodos habrá que
mover, en el peor de los casos, para convertirlo en un heap?
5. Agregar los métodos siguientes a la implementación del TAD
EncolableConPrioridad: (a) decrementar(p, n), el cual disminuye el valor
del elemento en la posición p del arreglo en n unidades positivas.
(b) incrementar(p, n), el cual incrementa el valor del elemento en la
posición p del arreglo en n unidades positivas.
(c) borrar(p), el cual elimina el elemento colocado en la posición p.
(d) imprimir(), el cual imprime el heap como un árbol.
6. Una cola puede implementarse usando una cola de prioridad y un
contador. Cada vez que se agrega un elemento, el contador se incrementa y
se agrega al elemento. El contador sirve como prioridad a los elementos.
Implementar la interfaz Encolable del cap´ıtulo 4 usando esta idea. ¿Se
puede implementar una pila con el mismo enfoque?
7. La implementación de colas con prioridad con un heap en un arreglo es
muy eficiente, pero no garantiza que los elementos con la misma prioridad
se extraigan exactamente en el mismo orden en que fueron insertados.
Proponer una implementación alternativa de colas con prioridad que
preserve el orden de inserción independientemente de la eficiencia de la
misma.
8. Escribir un método para encontrar el elemento mayor en un min-heap.
Especificar la complejidad del método.
9. Escribir un método para determinar si un árbol binario es un heap o no.
¿Cuál es el orden de ejecución del método?
10. En el cap´ıtulo 4 se presentó una aplicación consistente en asignar
tiempo de procesador a procesos en una computadora de un sólo
procesador. Resolver el mismo problema considerando ahora que los
procesos tienen una prioridad de ejecución. En este caso el procesador se va
asignando siempre al proceso que tenga la mayor prioridad y necesite más
tiempo del mismo; si el proceso no termina se vuelve a formar.
11. Suponer que se desea abrir una cafeter´ıa y es necesario decidir la
cantidad de sillas que se necesitan para que no falten ni sean tantas que
haya varias vac´ıas. Hacer un simulación del comportamiento de una cafeter
´ıa para poder obtener la información solicitada.
12. Hacer un programa de simulación de un banco en el cual los clientes
llegan y se forman en la caja cuya fila está más corta. Comparar tiempo de
espera por parte de los clientes, tiempo desocupado de los cajeros y número
de clientes atendidos por cajero contra los tiempos obtenidos en la
simulación en donde los clientes están en una sola fila.
Cap´ıtulo 7
Tablas de dispersi´
on
En los cap´ıtulos anteriores se ha destacado la importancia de las búsquedas
en el desarrollo de programas, y se ha mostrado la forma de ir reduciendo la
complejidad de tal tarea, disminuyendo la cantidad de comparaciones
necesarias para encontrar el dato buscado o determinar que no está en el
universo de búsqueda. Lo deseable es tener una estructura de datos en la
cual no hubiera necesidad de hacer tales comparaciones, es decir, que en un
solo acceso se obtuviera el dato. En este cap´ıtulo se presenta la estructura
de datos llamada tabla de dispersión o de hash, en la que el tiempo de
acceso a los elementos es constante, se describen los principales problemas
con ella y cómo resolverlos as´ı como unas aplicaciones de su uso.
7.1.
Introducci´
on
Suponer que se necesita hacer un programa que constantemente busque y
localice los datos de una persona, para determinar si se tiene registro de ella
o no. Los datos de cada persona son, entre otros, el nombre y un
identificador numérico de seis d´ıgitos. Se tiene información registrada de
10 000 personas.
De acuerdo con lo estudiado en cap´ıtulos anteriores pueden surgir varias
propuestas de solución: una ser´ıa colocar los datos en un arreglo, ordenarlo
y utilizar búsqueda binaria. El problema con esta solución es que es
necesario hacer varias comparaciones antes de encontrar el elemento
buscado, además de la complejidad de ordenar los registros de cada
persona. Otra solución es utilizar un árbol binario de búsqueda, en cuyo
caso la complejidad de la búsqueda es O(log( n)).
Para localizar los datos con un solo acceso se pueden almacenar en un
arreglo de 1 000 000
localidades y utilizar el identificador de cada persona como ´ındice para el
arreglo. Esta solución desperdiciar´ıa mucho espacio. Otra posibilidad es
construir y aplicar una función al identificador de tal manera que devuelva
un número entre 1 y 10 000, y ese número utilizarlo como ´ındice para un
arreglo de 10 000 elementos. Ésta es la función de dispersión.
169
170
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
Se ilustra la solución al problema con un conjunto reducido de datos.
Suponer que se tienen los siguientes 8 registros:
Nombre
Otros datos
identificador
Alfredo
...
985926
Alejandra
...
970876
Alicia
...
980962
Ángela
...
986075
Andrea
...
970728
Alberto
...
994593
Arcelia
...
996327
Amparo
...
967341
Tabla 7.1. Datos registrados de 8 personas.
Una buena función ser´ıa calcular el residuo de la división del identificador
entre 8, con lo cual se tiene un número entre 0 y 7, y éste puede utilizarse
como ´ındice de un arreglo, como se muestra a continuación.
Nombre
Otros datos
id
id % 8
Alfredo
...
985926
Alejandra
...
970876
Alicia
...
980962
Ángela
...
986075
Andrea
...
970728
Alberto
...
994593
Arcelia
...
996327
Amparo
...
967341
Tabla 7.2. Datos registrados de 8 personas con el resultado de una función.
Una función de dispersión f es una función que transforma un atributo o
conjunto de atributos de un elemento perteneciente a una colección, en un
número entero positivo entre cero y el n que se utilizará como ´ındice para
un arreglo de tama˜
no n (figura 7.1.). El dato o
combinación de datos que se utilizan para aplicar la función se conoce
como llave. La llave puede ser un número, una cadena, un registro, etcétera.
Si en el ejemplo anterior se decidiera que la llave fuera un atributo no
numérico, se tiene que hacer algo más que calcular el residuo de la división
para poder mapearla en un número entre 0 y 7. Analizando un poco los
nombres se puede ver que todos tienen distinta la tercera
7.1. INTRODUCCI ´
ON
171
Colección
de llaves
f(llave)
Tabla
.
y
n−1
Figura 7.1. Mapeo de llaves a ´ındices de una tabla.
letra, as´ı que se puede asociar su valor ( a = 0 , b = 1 , c = 2 , etcétera) y
obtener el residuo de la división del valor de la letra entre el tama˜
no del arreglo, en este caso 8. Con lo cual se
obtiene un número diferente para cada nombre, como puede verse en la
siguiente tabla.
Nombre
Letra
Valor
Valor % 6
Alfredo
Alejandra
5
5
Alicia
Ángela
Andrea
Alberto
Arcelia
3
3
Amparo
16
Tabla 7.3. Resultado de la función de dispersión sobre cadenas.
Una tabla de dispersión es una estructura de datos en la cual los elementos
se pueden insertar, suprimir y encontrar en un tiempo constante. Esto es
inmejorable. Sin embargo, con esta técnica no es posible determinar una
relación de orden entre los elementos de la colección, as´ı pues no se puede
determinar el elemento menor ni el mayor, ni recorrerla en un orden
determinado por sus elementos. Para implementar una tabla de dispersión se
requiere un arreglo y una función de dispersión. En principio, debe asignar
valores diferentes
172
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
a elementos diferentes. Una función de dispersión perfecta es aquella que
cubre todo el rango de valores determinados por el tama˜
no de la tabla.
El procedimiento para almacenar/acceder a un elemento en la tabla consiste
en tomar un elemento de la colección, aplicarle la función de dispersión y
almacenarlo/recuperarlo de la posición indicada como resultado de la
función.
7.2.
El TAD Tabla de dispersi´
on
La interfaz para el TAD que permite trabajar con la estructura de datos tabla
de dispersión es la siguiente:
public interface Dispersable{
public boolean estaVacia ();
public void vaciar();
public int tamanio ();
public void agregar (Object valor);
public boolean contiene (Object valor);
public Object obtener (Object valor);
public void sustituir (Object llave, Object actual, Object nuevo); public
[Link] iterador();
Las propiedades que se esperan de los métodos de una tabla de dispersión
son:
• agregar. Agrega a la tabla el objeto especificado como el parámetro. La
cantidad de elementos en la tabla se incrementa en una unidad.
• contiene. Regresa true si el elemento con la llave proporcionada está en la
tabla y false en otro caso. Este método no cambia el estado de la tabla.
• iterador. Devuelve un iterador sobre la tabla.
• estaVacia. Devuelve true si la tabla está vac´ıa y false en otro caso. Este
método no cambia el estado de la tabla.
• obtener. Si la tabla no está vac´ıa, devuelve el valor almacenado, cuya
llave corresponde a la proporcionada como parámetro. En otro caso
devuelve null. Este método no cambia el estado de la tabla.
• sustituir. Si el elemento, no la llave, a sustituir se encuentra en la tabla, lo
sustituye por el segundo parámetro. Esta operación no altera la cantidad de
elementos en la tabla.
7.3. FUNCIONES DE DISPERSI ´
ON
173
• tamanio. Devuelve un entero que indica la cantidad de elementos
almacenados en la tabla.
• vaciar. Elimina todos los elementos de la tabla. El tama˜no de la tabla
después de esta operación es cero.
La técnica de hashing o dispersión es sencilla, sin embargo tiene un par de
problemas: el primero es crear la función de dispersión y el segundo
determinar qué hacer en caso de que dos elementos distintos obtengan el
mismo valor. En las siguientes secciones se abordan estos problemas.
7.3.
Funciones de dispersi´
on
Como se explicó en la sección anterior, almacenar un dato en una tabla de
dispersión involucra dos pasos: transformar la llave en un valor entero y
mapear el resultado a un ´ındice válido en la tabla de dispersión. Para
minimizar las colisiones, se aconseja que el tama˜
no de la tabla
sea un número primo.
Algunos métodos frecuentes para crear una función de dispersión son los
siguientes: 1. Residuo de una división. Una función de dispersión sencilla,
sobre todo si el valor de la llave es un entero, es simplemente calcular el
residuo de la división de la llave entre el tama˜
no de la tabla, (dato % tamanioTabla), a menos que tengan una mala
distribución.
Ejemplo de una muy mala distribución es cuando el tamanioTabla = 10 y
todos o la mayor´ıa de los datos terminan con el mismo d´ıgito.
Si la llave es una cadena de caracteres se puede obtener un valor numérico
de ella sumando el valor de cada carácter. Una vez hecho esto se divide
entre el tama˜
no de la
tabla y se toma el residuo de esta división.
public static int hash(String elemento, int tamanioTabla) {
int valor = 0;
for (int i=0; i < [Link](); i++)
valor += [Link](i);
retiran valor % tamanioTabla;
Si el tama˜
no de la tabla es grande y el de la llave peque˜
no esta función puede no ser
buena porque habr´ıa mucho espacio desperdiciado. Por ejemplo, si el
tama˜
no de la
tabla es el número primo 10 007, las cadenas tienen como máximo ocho
caracteres y el valor máximo para los caracteres ascii es 127, entonces la
función da valores entre 0
y 1016, que es el resultado de 127*8.
174
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
2. Folding. Involucra dividir las llaves en dos o más partes y luego
combinarlas para obtener la dirección en la tabla. Por ejemplo, para mapear
la llave 25936715 en un rango de 0 a 9999, se puede dividir el número en
dos partes, 2593 y 6715, y luego sumarlos, con lo cual se tiene el 9308, que
ser´ıa el valor de la llave. Este método es muy útil cuando las llaves son
muy grandes.
Si la llave no es numérica puede tomarse parte de un atributo y parte de
otro, por ejemplo, la letra inicial del nombre de una persona, más algunos
caracteres de su dirección, más el número de casa, etcétera. Es rápido y
sencillo y permite transformar llaves no numéricas a valores enteros.
3. Cadenas de caracteres. Multiplicar cada carácter de una cadena por una
potencia de 37 y sumarlo. Expresada formalmente esta regla queda como: n
elemento[ i] × 37 i
i=0
con n = tama˜
no de la cadena
public static int hash(String elemento, int tamanioTabla) {
int valor = 0;
for (int i=0; i < [Link](); i++)
valor = 37*valor + [Link](i);
valor %= tamanioTabla;
if (valor < 0)
valor+=tamanioTabla;
return valor;
Al final del método se pregunta si el valor calculado es negativo, porque
podr´ıa tenerse un valor tan grande que se produzca un overflow y aparezca
como número negativo.
En este caso no es una desventaja, al contrario, se realiza una operación más
sobre los datos.
4. Trabajar con los primeros tres caracteres de la cadena y obtener un
módulo.
public static int hash(String elemento, int tamanioTabla) {
return ([Link](0) + 27 * [Link](1) +
729 * [Link](2)) % tamanioTabla;
}
El 27 es debido a que hay 27 letras distintas en el alfabeto, incluyendo el
espacio en blanco. El 729 es 27*27.
7.4. MANEJO DE COLISIONES
175
5. Función con corrimientos. Toma cada carácter de una cadena de
caracteres y acumula su valor multiplicándolo por 2, sólo que no utiliza la
multiplicación, si no corrimientos.
Al final divide esta suma entre el tama˜
no de la tabla y se devuelve el residuo de esta
división.
public static int hash(String elemento, int tamanioTabla) {
int valor = 0;
int i= [Link]();
while(i > 0)
valor = (valor << 1) + (int)[Link](--i); return valor %
tamanioTabla;
Toda función de dispersión depende de los datos a los que se va a aplicar;
en esta sección sólo se presentaron algunas posibilidades generales.
7.4.
Manejo de colisiones
El segundo problema con las funciones de dispersión es qué hacer cuando
al aplicar la función a dos elementos diferentes se obtenga el mismo valor.
Por ejemplo, ¿Qué sucede si ahora desea ingresar al conjunto de las
personas de la sección anterior a una llamada Ana Mar´
ıa después
de haber insertado a Ángela, Alberto, Andrea y Alfredo utilizando la
función que toma la tercera letra del nombre?
f( AnaMaria) = ( a − a) %8 = 0
esto implica que a Ángela y a Ana Mar´
ıa les toca el mismo valor de dispersión, es decir se tiene una colisión.
En general existen dos enfoques para resolver este problema: 1. Cada
elemento se almacena en una localidad diferente de la tabla de dispersión.
Cuando dos elementos colisionan se busca una localidad vac´ıa en la cual
almacenar el segundo valor que obtuvo igual valor entero. Aqu´ı hay una
serie de técnicas para buscar el lugar apropiado.
• La más sencilla es buscar hacia las localidades con ´ındice mayor hasta
encontrar el primer lugar vac´ıo. Ejemplo:
Al intentar incluir a Ana Mar´ıa en la localidad 0 de la tabla mostrada en
7.4. la que le corresponde de acuerdo con su función, se busca el primer
lugar desocupado, en este caso se tiene en la posición 2. Este método se
llama direccionamiento abierto lineal.
176
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
Ángela
1
Alberto
...
...
Andrea
...
Alfredo
...
Tabla 7.4. Tabla de dispersión.
• Doble dispersión. En este caso se tiene una segunda función de dispersión
que se utiliza sólo en caso de colisiones. Ejemplo: f(i) = i* hash 2 (X)
donde una buena función es R − ( x % R) con R un número primo menor
que el tama˜no de la tabla.
Con este enfoque, al buscar un elemento se empieza en la posición dada por
la función f( k), si el elemento no es el buscado se continúa buscando
sucesivamente en la tabla (como si fuera circular) hasta que se encuentre el
elemento, se encuentre una posición vac´ıa o se vuelva a la posición
proporcionada por la función. En estos dos últimos escenarios el elemento
no se encuentra en la tabla.
2. Considerar que cada elemento en la tabla es una colección denominada
cubeta. Aqu´ı el agregar un nuevo elemento consta de dos pasos:
(a) Encontrar la cubeta en la que se insertará el elemento.
(b) Almacenarlo en ella.
Una función de dispersión es uniforme si a cada cubeta le corresponde la
misma cantidad de llaves. Por ejemplo, si se tienen 11 cubetas y llaves en el
rango de [0 , 98], una función uniforme es aquella que coloca
aproximadamente 9 de estas llaves en cada cubeta. Para buscar un elemento
se llega a la cubeta localizada en la posición dada por la función f ( k), y ah
´ı se busca el elemento.
La eficiencia de los métodos para resolver colisiones depende, en general,
del factor de carga que es la cantidad de elementos en la tabla entre el tama˜
no de ella. Con una buena
función el costo promedio de búsquedas es casi constante conforme el
factor se incrementa de 0 a 0.7, después de esto la probabilidad de
colisiones y el costo de su manejo se incrementa.
Por otro lado, conforme el factor de carga se acerca a cero, se tiene una
peque˜
na mejora en
el costo de búsqueda a cambio de un desperdicio de memoria.
Una buena función de dispersión deber´ıa minimizar las colisiones, ser
rápida y fácil de calcular usando toda la información proporcionada en la
llave y tener un alto factor de carga para un conjunto de llaves.
7.5. APLICACIONES DE TABLAS DE DISPERSI ´
ON
177
7.5.
Aplicaciones de tablas de dispersi´
on
Las tablas de dispersión se usan en aplicaciones donde se requiere que las
operaciones de insertar y localizar sean en tiempo constante y los cambios
no sean frecuentes. Sin embargo, hay que cuidar aspectos como el factor de
carga para tener este tiempo constante y la elección/definición de la función
de dispersión, de tal forma que ésta sea fácil de calcular y tenga buena
distribución, es decir, que tenga una m´ınima cantidad de colisiones.
Algunas de las aplicaciones de las tablas de dispersión son las siguientes: en
los compiladores se usa una tabla de dispersión, denominada tabla de s
´ımbolos, para almacenar las variables declaradas. Se utilizan porque las
únicas operaciones ejecutadas son insertar y localizar los identificadores.
En los verificadores de ortograf´ıa es muy útil, puesto que se requiere un
acceso rápido (en tiempo constante) al diccionario, y éste no tiene muchas
actualizaciones.
En esta sección se presentan como aplicaciones un algoritmo para
ordenamiento para números enteros, una aplicación en el área de qu´ımica y
otra para trabajar con puntos en el plano cartesiano.
7.5.1.
Radix sort
Al igual que la técnica de la sección anterior, esta es una técnica para
ordenar enteros positivos, sólo que ahora su valor está menos restringido e
ilustra el uso de cubetas.
Si se desea ordenar la lista { 624, 762, 852, 426, 197, 987, 269, 146, 415,
301, 730, 78, 593 } se deben ordenar sucesivamente los elementos sobre las
posiciones de los d´ıgitos de derecha a izquierda. Esto se logra colocando
los elementos en cubetas, donde el
´ındice para la cubeta está dado por la posición del d´ıgito que será
ordenado. Una vez que se han examinado todos los d´ıgitos, se tiene
ordenada la colección.
En la tabla 7.5. el contenido de cada cubeta durante cada uno de los pasos
en la secuencia del algoritmo.
Durante la primera pasada se colocan los elementos en la cubeta
correspondiente al d´ıgito con que terminan, por lo tanto, todos los
elementos de un d´ıgito quedan en su lugar. Luego se toman de las cubetas
y se regresan a un arreglo para conservar el orden relativo impuesto por la
primera pasada. En la segunda pasada se colocan en la cubeta
correspondiente de acuerdo con el segundo d´ıgito, se regresan al arreglo,
etcétera.
Ejemplo 7.1. Método para implementar el algoritmo de ordenamiento
radixsort. Utiliza un arreglo de listas para cada cubeta.
/**
* Método radixsort
* @param datos -- arreglo de enteros que contiene las llaves.
*/
public void radixsort(int[] datos) {
178
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
Cubeta
Paso 1
Paso 2
Paso 3
730
301
78
301
415
146,197
762, 852
624, 426
269
593
730
301
4
624
146
415, 426
415
852
593
426, 146
762, 269
624
197, 987
78
730, 762
78
987
852
9
269
593,197
987
Tabla 7.5. Resultado del radix sort, paso a paso.
boolean continuar = true;
int divisor = 1;
Lista[] cubetas = new Lista[10];
for (int i = 0; i < 10; i++)
cubetas[i] = new Lista();
while(continuar) {
continuar = false;
for (int i = 0; i < [Link]; i++) {
int indice = (datos[i] / divisor) %10;
if (indice > 0) continuar = true;
cubetas[indice].agregar(new Integer(datos[i]));
//Regresa los elementos al arreglo
divisor *= 10;
int i = 0;
for (int j = 0; j <10; j++)
while(!cubetas[j].estaVacia()){
Integer ival = (Integer)cubetas[j].primerElemento();
cubetas[j].eliminarPrimero();
datos[i++] = ival;
7.5. APLICACIONES DE TABLAS DE DISPERSI ´
ON
179
7.5.2.
C´
alculo de peso molecular
Un problema común en qu´ımica es calcular el peso molecular de una
sustancia tal como H2O, para ello se debe multiplicar el peso molecular del
hidrógeno por 2 y sumarle el peso molecular del ox´ıgeno.
Para obtener estos pesos moleculares es conveniente utilizar una tabla de
dispersión para agilizar la localización de los elementos y obtener sus
pesos. Este es uno de esos raros casos en que de antemano se conoce el
valor de todas las llaves. A continuación se presenta un programa para
resolver este problema utilizando el tipo abstracto de datos Dispersable.
Ejemplo 7.2. La clase ElementoQ tiene la información de cada elemento qu
´ımico, constructores y métodos para obtener el valor de cada atributo. El
método interesante de esta clase es hashCode, uno de los que se heredan de
la clase Object y en este caso se sobreescribe para mapear el s´ımbolo en un
arreglo de 103 localidades, correspondiente a la cantidad de elementos qu
´ımicos existentes.
/**
* Clase con información de los elementos qu´
ımicos
* @author Amparo López Gaona
*/
public class ElementoQ {
private String nombre;
private String simbolo;
private int posicion;
private double peso;
/**
* Constructor para un elemento qu´
ımico
* @param n -- nombre del elemento
* @param s -- s´
ımbolo del elemento
* @param pos -- posición en la tabla periódica del elemento
* @param p -- peso molecular del elemento
*/
public ElementoQ(String n, String s, int pos, double p) {
nombre =
n;
simbolo = s;
posicion = pos;
peso = p;
/** Método para obtener el peso molecular de un elemento
* @return double -- peso del elemento
*/
public double obtenerPeso() {
return peso;
180
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
/**
* Método para obtener el s´
ımbolo de un elemento
* @return double -- s´
ımbolo del elemento
*/
public String obtenerSimbolo() {
return simbolo;
/**
* Método para obtener un valor de dispersión para un elemento qu´
ımico
* @return int -- valor de dispersión del elemento
*/
public int hashCode() {
int hashC = posicion;
for(int i=0; i<[Link](); i++)
hashC += ([Link](i) -’A’);
return hashC % 103;
/**
* Método para obtener el nombre y s´
ımbolo de un elemento
*/
public String toString() {
return nombre+ "("+simbolo+")";
Ejemplo 7.3. La clase PesoMolecular permite calcular el peso molecular de
fórmulas qu´ımicas.
En un archivo [Link] se tiene para cada elemento su posición en la
tabla qu´ımica, su nombre, s´ımbolo y peso molecular. Se toma el s´ımbolo
como llave. Son 103 elementos qu´ımicos, pero se crea una tabla con
capacidad un poco mayor para minimizar las colisiones.
/**
Clase para calcular el peso molecular de formulas qu´
ımicas.
Se requiere del archivo [Link] que tiene la información de
cada elemento qu´
ımico.
@author Amparo López Gaona
*/
public class PesoMolecular {
Dispersable elementos;
public PesoMolecular() {
elementos = new Dispersable(111);
7.5. APLICACIONES DE TABLAS DE DISPERSI ´
ON
181
public void llenarTablaElementos() {
String[] campos;
String nombre,
simbolo;
int posicion, posHash;
double peso;
try{
BufferedReader in = new BufferedReader (new
FileReader("[Link]")); String linea = [Link]();
while(linea != null) {
campos = [Link](",");
nombre = campos[0];
posicion = [Link](campos[1]);
simbolo = campos[2];
peso = [Link](campos[3]);
[Link](new ElementoQ(nombre, simbolo, posicion, peso));
linea = [Link]();
} catch (Exception e) {
[Link]("ERROR"+e);
M´
etodo 7.1. El método calcularPeso recibe como parámetro una fórmula y
calcula su peso molecular.
El parámetro es una cadena donde cada elemento qu´ımico con la cantidad
de átomos se separa de otro por un punto. Por ejemplo H2.0. Este método
extrae cada pareja, si no hay un número se asume que es uno. Con el s
´ımbolo del elemento se accede a la tabla de dispersión y se obtiene su peso
atómico, el cual se va sumando.
/**
* Método para calcular el peso molecular de una formula qu´
ımica
* @param formula -- cadena con la formula qu´
ımica
* @return double -- peso molecular de la formula recibida
*/
public double calcularPeso(String formula) {
double total = 0.0;
String[] campos = [Link]("\\.");
int pos; boolean hayDigito;
for (int i = 0; i < [Link]; i++) {
pos = 0; hayDigito = false;
while (pos < campos[i].length() && !hayDigito)
182
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
if ([Link](campos[i].charAt(pos++))){
pos--;
hayDigito = true;
String simbolo = campos[i].substring(0, pos);
double peso = obtenerPeso(simbolo);
String atomos = campos[i].substring(pos, campos[i].length()); total+=
([Link]() == 0)? peso :[Link](atomos)*peso;
}
return total;
M´
etodo 7.2. El método obtenerPeso es un método auxiliar que recupera de la
tabla de dispersión la lista de elementos qu´ımicos con el mismo número de
dispersión que el elemento buscado.
/*
* Calcula el peso de un elemento qu´
ımico
* @param simbolo -- cadena con un s´
ımbolo qu´
ımico
* @return double -- calcula el peso de un s´
ımbolo
*/
private double obtenerPeso(String simbolo) {
ElementoQ el = new ElementoQ(simbolo);
Lista elems = [Link](el);
[Link] it = [Link]();
while([Link]()) {
el = (ElementoQ)[Link]();
if ([Link]().equals(simbolo))
return [Link]();
[Link]("El simbolo "+simbolo+" no está en la tabla"); return 0;
Ejemplo 7.4. Un ejemplo de ejecución de este programa es el siguiente: El
peso de Be2.O.N4 es 90.06
El peso de H2.O es 18.02
El peso de [Link] es 383.21
El peso de N.H4.N.O3 es 80.06
El peso de N.Na4.Ag45.O3 es 5008.120000000001
7.5. APLICACIONES DE TABLAS DE DISPERSI ´
ON
183
El peso de Fe es 55.85
El peso de Fe2.O3 es 159.7
El peso de Ca3.P.O4.P.O4 es 310.2
7.5.3.
Informaci´
on de puntos
Suponer que se requiere dibujar c´ırculos de distintos colores y tama˜
nos, con centro en cual-
quier punto del plano cartesiano; lo normal es tener un arreglo
bidimensional para representar el plano. Si se tiene un plano de 200 × 200,
se requiere una matriz con capacidad para almacenar 40000 objetos. Sin
embargo, sólo se requiere información de una cantidad peque˜
na de
puntos, por ejemplo 50. En este caso se puede utilizar una tabla de
dispersión para almacenar y recuperar rápidamente cada punto con su
información asociada.
Suponer que se tiene una clase para trabajar con puntos en el plano
cartesiano, con sus dos coordenas, métodos para asignarles valor, para
recuperar los valores, para determinar si son iguales y para una
representación en forma de cadena.
Ejemplo 7.5. Clase que extiende los puntos antes mencionados con
información adicional de los c´ırculos como color, tama˜
no y función de dispersión.
import [Link];
public class PuntoHash extends Punto {
private Color color;
private int diametro;
private int tamanioTabla;
M´
etodo 7.3. Constructor de un punto con color y del grueso deseado.
/**
* Constructor de un punto con color y diametro
* @param x -- coordenada x del punto
* @param y -- coordenada y del punto
* @param color -- color del punto
* @param diametro -- diametro del punto
* @param tamanioTabla -- tama~
no de la tabla donde se almacena el punto
*/
public PuntoHash(int x, int y, Color color, int diametro, int tamanioTabla ) {
super(x, y);
[Link] = color;
[Link] = diametro;
[Link] = tamanioTabla;
M´
etodo 7.4. Método para conocer el color de un punto.
184
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
/** Método para conocer el color de un punto
* @return Color -- color del punto
*/
public Color obtenerColor() {
return color;
M´
etodo 7.5. Método para conocer el diámetro de un punto.
/** Método para conocer el diámetro de un punto
* @return int -- diámetro del punto
*/
public int obtenerDiametro() {
return diametro;
M´
etodo 7.6. Método para asignar color a un punto.
/**
* Método para asignar color a un punto
* @param color -- nuevo color del punto
*/
public void asignarColor( Color color ) {
[Link] = color;
M´
etodo 7.7. Método para conocer el color de un punto.
/**
* Método para asignar diámetro a un punto
* @param diametro -- nuevo diámetro del punto
*/
public void asignarDiametro(int diametro) {
[Link] = diametro;
M´
etodo 7.8. Método para calcular el valor de dispersión de un punto. Divide
entre el tama˜
no de la tabla la sumas de las coordenadas y el diamétro del punto.
/**
* Método para calcular el valor de dispersión de un punto
* @return int -- valor de dispersión del punto.
*/
7.5. APLICACIONES DE TABLAS DE DISPERSI ´
ON
185
public int hashCode() {
return (obtenerX() + obtenerY() + obtenerDiametro()) % tamanioTabla;
Ejemplo 7.6. En la clase Puntotes se crea la tabla de dispersión y se
almacenan o recuperan de ella los puntos según se requiera.
import [Link].*;
import [Link];
/**
* Clase para almacenar/recuperar puntos con color y grosor de una
* tabla de dispersión
* @author Amparo López Gaona
*/
public class Puntotes extends Frame {
private Dispersable tabla;
private Random rand;
M´
etodo 7.9. En el constructor por omisión se crean 50 puntos, en posiciones
aleatorias dentro del marco de 200 × 200; el color puede ser verde, azul,
rojo o negro.
public Puntotes() {
PuntoHash punto;
int x;
int y;
int diametroP;
Color colorP;
Color color[] = {[Link], [Link], [Link], [Link]}; tabla =
new TablaDeDispersionA(50);
rand = new Random();
for( int i = 0; i < 50; i++ ) {
x = [Link]( [Link]() % 200 );
y = [Link]( [Link]() % 200 );
colorP = color[i%4];
diametroP = ([Link]() % 20) + 1 ;
punto = new PuntoHash(x, y, colorP, diametroP, 53);
[Link](punto);
resize( 200, 200 );
}
186
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
M´
etodo 7.10. El método paint recupera los valores de la tabla de dispersión y
los grafica en un lienzo de 200 × 200.
/**
* Método para graficar los puntos recuperados de una tabla de dispersión.
*/
public void paint( Graphics g ) {
PuntoHash p;
for ([Link] it = [Link](); [Link](); ) {
p = (PuntoHash) [Link]();
[Link]( [Link]() );
[Link]( [Link](), [Link](), [Link](),
[Link]() );
7.6.
Implementaci´
on de tablas de dispersi´
on
En esta sección se presentan dos implementaciones del TAD tabla de
dispersión de acuerdo con la interfaz Dispersable una con direccionamiento
cerrado y otra con cubetas.
7.6.1.
Utilizando cubetas
Todas las clases son subclases de la clase Object con lo cual heredan, entre
otros, los métodos equals y toString. Existe otro método que también es
heredado y factible de sobreescribirse, es el método hashCode. Si no se
sobrescribe, Java proporciona su propio método, que puede no ser el más
adecuado para la aplicación particular.
Clase 7.1. En la clase TablaDeDispersion se asume que el método
hashCode de la clase Object está sobreescrito y lo utiliza para implementar
la interfaz Dispersable con cubetas de una manera muy elegante, porque
dentro de cada cubeta, representada por una lista, se usan los métodos de las
listas.
import [Link];
/**
* Clase para crear tablas de dispersión con cubetas
* @author Amparo López Gaona
*/
public class TablaDeDispersion implements Dispersable {
private Lista [] cubetas;
private int nElementos;
M´
etodo 7.11. Constructor de una tabla de dispersión del tama˜
no indicado.
7.6. IMPLEMENTACI ´
ON DE TABLAS DE DISPERSI ´
ON
187
/**
* Constructor para una tabla de dispersión del tama~
no indicado
* @param n la cantidad de cubetas en la tabla
*/
public TablaDeDispersion (int n) {
cubetas = new Lista[sgtePrimo(n)];
for (int i = 0; i < [Link]; i++)
cubetas[i] = new Lista();
nElementos = 0;
M´
etodo 7.12. Constructor por omisión para una tabla de dispersión. La crea
con 101
cubetas.
/**
* Constructor por omisión para una tabla de dispersión con 101 cubetas
*/
public TablaDeDispersion () {
this(100);
M´
etodo 7.13. Método para verificar si la tabla de dispersión está vac´ıa, para
ello utiliza la variable nElementos, y si ésta tiene valor cero significa que la
tabla está vac´ıa, en otro caso no lo está.
/**
* Verifica si la tabla está vac´
ıa
* @return true si la tabla está vac´
ıa y false en otro caso
*/
public boolean estaVacia () {
return nElementos == 0;
}
M´
etodo 7.14. Método para conocer la cantidad de elementos almacenados en
la tabla de dispersión.
/**
* Determina la cantidad de elementos en la tabla
* @return int -- cantidad de elementos en la tabla
*/
public int tamanio () {
return nElementos;
188
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
M´
etodo 7.15. Método para dejar sin elementos una tabla de dispersión. Es
suficiente dar valor de cero a la variable nElementos y limpiar cada cubeta,
asignándole una lista vac´ıa a cada una.
/**
* Elimina todos los elementos de la tabla.
*/
public void vaciar () {
for (int i = 0; i < tamanio(); i++)
cubetas[i] = new Lista ();
nElementos = 0;
M´
etodo 7.16. Método para agregar un elemento a la tabla de dispersión.
/**
* Agrega un elemento a la tabla
* @param val objeto que se insertará en la tabla
*/
public void agregar (Object val) {
cualCubeta(val).agregar(val);
nElementos++;
M´
etodo 7.17. Método para determinar si un elemento está en la tabla de
dispersión o no.
/**
* Determina si una tabla contiene un valor particular
* @param val -- elemento que se busca
* @return true -- si la tabla contiene un valor particular y false en
* otro caso.
*/
public boolean contiene (Object val) {
return cualCubeta(val).contiene(val);
M´
etodo 7.18. Método para eliminar un valor de la tabla de dispersión.
/**
* Elimina un valor de la tabla
* @param val Objeto que será eliminado de la tabla
*/
public void eliminar (Object val) {
cualCubeta(val).eliminar(val);
nElementos--;
7.6. IMPLEMENTACI ´
ON DE TABLAS DE DISPERSI ´
ON
189
M´
etodo 7.19. Método para obtener los elementos en la tabla que tienen la
misma llave.
/**
* Devuelve los elementos que tienen la misma llave
* @param val -- llave del elemento que se busca
* @return Lista -- lista con los elementos que tienen la misma llave.
*/
public Lista obtener (Object val) {
return cualCubeta(val);
M´
etodo 7.20. Método privado para determinar con qué cubeta se trabajará.
/*
* Método interno para determinar en qué cubeta se realizará una
* operación.
*/
private Lista cualCubeta (Object val)
return cubetas[[Link]([Link]()) % [Link]];
}
M´
etodo 7.21. Método para crear un iterador sobre la tabla de dispersión.
/**
* Iterador de la tabla
* @return Iterator -- que mantiene los elementos de la tabla
* @see [Link]
*/
public [Link] iterador () {
return new miIteradorHash();
Clase 7.2. Clase privada que implementa el iterador. Es muy interesante,
pues se trata de un iterador anidado. En el método se recorre todo el arreglo
y para cada localidad crea un iterador que corre sobre la lista incluida.
/*
* Clase privada que implementa el iterador.
*/
private class miIteradorHash implements [Link] {
[Link] itTabla;
int ind;
190
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
public miIteradorHash () {
ind = 0;
itTabla = cubetas[0].iterador();
public boolean hasNext() {
if ([Link]()) return true;
while (++ind < [Link]) {
itTabla = cubetas[ind].iterador();
if ([Link]())
return true;
return false;
public Object next() {
return [Link]();
public void remove() {
throw new IllegalStateException();
M´
etodo 7.22. Método para encontrar un número primo mayor que un entero
dado.
/** Método interno para encontrar un número primo mayor que un entero
dado
* @param n número positivo inicial
* @return int - número primo mayor o igual a n
*/
private int sgtePrimo(int n) {
if (n <= 100) {
int[] primos =
{2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67, 71,97,101};
for (int i = 0; i <[Link]; i++)
if(n < primos[i])
return primos[i];
if(n % 2 == 0) n++;
for(; !esPrimo(n); n += 2)
;
return n;
7.6. IMPLEMENTACI ´
ON DE TABLAS DE DISPERSI ´
ON
191
M´
etodo 7.23. Método para verificar si un número es primo.
/**
* Método interno para verificar si un número es primo.
* @param n -- número que se quiere verificar que sea primo.
* @return boolean -- true si el número es primo y false en otro caso.
*/
private boolean esPrimo(int n)
if (n == 2 || n == 3)
return true;
for(int i = 3; i * i <= n; i += 2) {
if(n % i == 0)
return false;
return true;
La complejidad de los métodos del TAD tabla de dispersión con cubetas
según la implementación presentada en esta sección se muestra en la tabla
7.6.
Operación
Tiempo de ejecución
constructor
O( n)
agregar
O(1)
contiene
O( m)
estaVacia
O(1)
eliminar
O( m)
hasNext
O(1)
iterador
O(1)
next
O(1)
obtener
O(1)
tamanio
O(1)
vaciar
O( m)
Tabla 7.6. Tiempo de ejecución de los métodos de la clase
TablaDeDispersion.
El constructor es de orden lineal debido a que se crean n listas. Los métodos
de complejidad m se refieren a la cantidad de elementos en la cubeta que
con una buena función de dispersión debe ser cercana a 1, es decir, pueden
considerarse de orden constante.
192
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
7.6.2.
Direccionamiento abierto
Con esta técnica todos los elementos se almacenan en la misma tabla. Al
ocurrir una colisión se busca otro lugar disponible dentro de la tabla, de
acuerdo con ciertos criterios, y ah´ı se inserta el nuevo dato.
Clase 7.3. La clase TablaDeDispersionA tiene el algoritmo para resolver
colisiones en el método agregar. En este caso sólo se tiene un arreglo para
almacenar los elementos y un contador para estos.
/**
* Clase para crear tablas de dispersión usando el método lineal para
resolver
* las colisiones
* @author Amparo López Gaona
*/
public class TablaDeDispersionA implements Dispersable {
private Object[] elementos ;
int nElementos;
M´
etodo 7.24. Constructor para una tabla de dispersión del tama˜
no especificado.
/**
* Crea una tabla de dispersión del tama~
no especificado
* @param tam -- cantidad inicial de elementos permitida
*/
public TablaDeDispersionA (int tam) {
elementos = new Object[sgtePrimo(tam)];
nElementos = 0;
M´
etodo 7.25. Constructor por omisión para una tabla de dispersión. La crea
con 101
cubetas.
/**
* Crea una tabla de dispersión para 100 elementos
* @param tam -- cantidad inicial de elementos permitida
*/
public TablaDeDispersionA () {
this(100);
M´
etodo 7.26. Método para verificar si la tabla de dispersión está vac´ıa.
/**
* Verifica si la tabla está vac´
ıa
7.6. IMPLEMENTACI ´
ON DE TABLAS DE DISPERSI ´
ON
193
* @return true si la tabla está vac´
ıa y false en otro caso
*/
public boolean estaVacia () {
return nElementos == 0;
M´
etodo 7.27. Método para conocer la cantidad de elementos almacenados en
la tabla de dispersión.
/**
* Determina la cantidad de elementos en la tabla
* @return int -- cantidad de elementos en la tabla
*/
public int tamanio () {
return nElementos;
M´
etodo 7.28. Método para dejar sin elementos una tabla de dispersión.
/**
* Elimina todos los elementos de la tabla.
*/
public void vaciar () {
for (int i = 0; i < [Link]; i++)
elementos[i] = null;
nElementos = 0;
M´
etodo 7.29. Método para agregar un elemento a la tabla de dispersión. En el
método agregar se tiene un algoritmo para resolver las colisiones llamado
prueba lineal; podr´ıa usarse una prueba cuadrática, que consiste en ir
sumando los cuadrados consecutivos al ´ındice que se está probando (n+1,
n+2, n+4, n+9, etc). Un tercer método consiste en calcular otro valor de
dispersión, generalmente entre un número primo relativo con respecto al
tama˜
no de
la tabla.
/**
* Agrega un elemento a la tabla
* @param val objeto que se insertará en la tabla
*/
public void agregar (Object val) {
if (nElementos + 1 >= [Link])
throw new Exception("Arreglo lleno");
194
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
int indice = [Link]([Link]()) % [Link]; while
(elementos[indice] != null)
//Resolución de colisiones
if (++indice >= [Link]) indice = 0;
elementos[indice] = val;
nElementos++;
M´
etodo 7.30. Método para determinar si un elemento está en la tabla de
dispersión o no.
/**
* Determina si una tabla contiene un valor particular
* @param val -- elemento que se busca
* @return true -- si la tabla contiene un valor particular y false en
* otro caso.
*/
public boolean contiene (Object val) {
int indice = [Link]([Link]()) % [Link]; int indice =
indiceIni;
while (elementos[indice] != null) {
if ([Link](elementos[indice]))
return elementos[indice];
if (++indice >= [Link]) indice = 0;
if (indiceIni == indice) break;
return false;
M´
etodo 7.31. Método para crear un iterador sobre la tabla de dispersión.
/**
* Iterador de la tabla
* @return Iterator -- que mantiene los elementos de la tabla
* @see [Link]
*/
public [Link] elementos ()
{ return new MiIterador(); }
Clase 7.4. Clase privada que implementa el iterador.
7.6. IMPLEMENTACI ´
ON DE TABLAS DE DISPERSI ´
ON
195
/*
* Clase privada que implementa el iterador.
*/
private class MiIterador implements [Link] {
private int indice = -1;
public boolean hasNext () {
while (++indice < [Link]) {
if (elementos[indice] != null)
return true;
return false;
public Object next () { return elementos[indice]; }
public void remove() {
throw new IllegalStateException();
En esta forma de trabajar las tablas de dispersión no está permitido eliminar
elementos, pues de otra manera podr´ıa causar problemas al momento de
tratar de localizar un elemento por encontrar un hueco.
La complejidad de los métodos del TAD Tabla de dispersión con
direccionamiento abierto según la implementación presentada se encuentra
en la tabla 7.7.
Operación
Tiempo de ejecución
constructores
O(1)
agregar
O( n)
contiene
O( n)
estaVacia
O(1)
hasNext
O( n)
iterador
O(1)
next
O(1)
tamanio
O(1)
vaciar
O( n)
Tabla 7.7. Tiempo de ejecución de los métodos de la clase
TablaDeDispersionA.
196
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
7.7.
Ejercicios
1. Suponer que en un arreglo se tienen 31 números enteros distintos en
orden ascendente.
(a) Calcular el número de comparaciones necesarias para localizar los 31
valores en el arreglo, usando una búsqueda binaria.
(b) Si los enteros se almacenan en una tabla de dispersión con k cubetas,
con una función que distribuye los enteros en cubetas tan uniformemente
como sea posible.
Determinar el menor valor de k para el cual el número de comparaciones
para encontrar los 31 valores en la tabla de dispersión es menor que el
encontrado en el punto anterior.
2. Suponer que el tablero para el juego del gato se representa como un
arreglo de enteros donde se almacenan sólo tres posibles valores 0 para
casilla vac´ıa, 1 para la equis y 2
para el c´ırculo. Suponer que se tiene la siguiente función para los tableros.
public int hashCode () {
int suma = 0;
for (int renglon = 0; renglon < 3; renglon++)
for (int columna = 0; columna < 3; columna++)
suma += renglon*columna*tablero[renglon][columna];
return suma;
¿Es una buena función de dispersión para cerca de 5 000 cubetas?
Considerar que hay 19 683 tableros diferentes. Justificar la respuesta.
3. Escribir una función de dispersión para almacenar objetos de la clase
Intervalo en una tabla de dispersión. La clase tiene enteros para indicar el
valor menor del intervalo y el superior, además de los métodos para
manipularlos. Suponer que todos los intervalos están contenidos en el
intervalo [0, 10]. La función deberá distribuir los valores de acuerdo con el
tama˜
no de la tabla.
4. Escribir un programa que permita detectar faltas de ortograf´ıa. Para ello
se debe leer un archivo con las palabras bien escritas. Crear una tabla de
dispersión para esas palabras, para agilizar la detección de una falta
buscando una palabra le´ıda contra la tabla diccionario. El programa sólo
debe reportar el error.
5. Escribir un programa para verificar la autenticidad de la contrase˜
na correspondiente a
un usuario. El programa deberá solicitar un nombre de usuario y una
contrase˜
na, ésta
se debe buscar en una tabla de dispersión y enviar un mensaje indicando si
es correcta o no. Ejemplos:
7.7. EJERCICIOS
197
Login: maria
Password: mmariia
¡¡Password correcto!!
Login: jorge
Password: sksksks
¡¡Password incorrecto!!
6. Escribir un método eficiente para encontrar el primer carácter no repetido
dentro de una cadena. Por ejemplo, el primer carácter no repetido de la
palabra cocos es la ese, el primer carácter no repetido de la palabra amiga es
la eme. Especificar la eficiencia del algoritmo en términos de la función O.
7. Escribir un programa para determinar la cantidad de veces que aparece
cada palabra en un texto. El resultado debe aparecer en orden alfabético.
Por ejemplo, con el texto Averigua las palabras más repetidas en
los textos llenos de palabras, es decir
el número de veces que aparecen las
palabras en los textos.
Una fracción de la salida del programa ser´ıa:
averigua 1
en 2
las 2
los 2
palabras 3
...
8. Escribir un programa que permita almacenar en una tabla de dispersión
los datos de los empleados en una compa˜
n´ıa. De cada empleado se tiene su nombre, RFC, departamento y sueldo.
Utilizar como llave el RFC. Suponer que hay un máximo de mil empleados
y no se permite más del 13 % de colisiones. Utilizar ambas
implementaciones vistas en este cap´ıtulo y calcular cuántos accesos se
requieren, para cada una, en promedio, para localizar a un empleado
determinado.
9. Escribir un programa para crear y consultar un directorio telefónico,
utilizando el número de teléfono como llave para una función de dispersión.
El programa debe localizar los datos de una persona a partir de su número
telefónico.
198
CAP´
ITULO 7. TABLAS DE DISPERSI ´
ON
10. Escribir un programa para manejar una tabla de identificadores para
programas en Java. El programa debe poder localizar los identificadores,
indicando para cada identificador, el número de l´ınea en que se declara, el
tipo de dato que le corresponde a ese identificador el número de l´ınea en
que se utiliza y el método o clase a la que pertenece.
En este caso, se tiene la restricción de que sólo es posible declarar datos al
inicio de los métodos y clases.
Ap´
endice A
Recursi´
on
En este apéndice se presenta brevemente el tema de recursión incluyendo
backtracking. Se presenta la programación recursiva del algoritmo de
búsqueda binaria y un programa para colocar reinas en un tablero de ajedrez
sin que se agredan.
Cuando un método involucra, en ciertas circunstancias, llamadas a s´ı
mismo, se dice que éste es recursivo. Las soluciones recursivas son muy
utilizadas en matemáticas. Por ejemplo, f
si x < = 0
( x) =
2 f ( x − 1) + x 2 si x > 0
En este caso se define f ( x) en términos de f ( x− 1). Aunque se define el
cálculo en términos de s´ı misma eventualmente se llega al fin, porque se
aplica la misma función a un valor distinto, en este caso uno menor. Por
ejemplo, f (3) = 2 ∗ f(2) + 3 ∗ 3, as´ı que para calcular f(3) es necesario
primero calcular f(2) = 2 ∗ f(1) + 2 ∗ 2, para ello se necesita calcular f(1) =
2 ∗f(0)+1 ∗ 1 y finalmente f(0) = 0, al regresar sustituyendo los valores de
f(0) , f(1) y f (2) se tiene que f (3) = 21.
Esta fórmula escrita en Java queda:
public int f(int x) {
return (x <= 0) ? 0 : 2*f(x-1) + x*x;
La primera parte de la instrucción condicional se denomina caso base y es
el que permite terminar la recursión, siempre y cuando el resto del método
trabaje para llegar al caso base.
Por ejemplo, con el siguiente método:
public int calculoRecursivo ( int n) {
return (n == 0) ? 0 : calculoRecursivo(n/3 + 1) +n - 1;
¿cuál es el valor de calculoRecursivo(3)?
199
200
AP ´
ENDICE A. RECURSI ´
ON
calculoRecursivo(3) = calculoRecursivo(3/3 + 1) + 3 - 1; calculoRecursivo
(2) = calculoRecursivo (2/3 + 1)+ 1; calculoRecursivo (1) =
calculoRecursivo(1/3 + 1) + 0; calculoRecursivo(1) ...
Aqu´ı, aunque aparentemente se llegar´ıa al caso base, no sucede, pues
calculoRecursivo(1) está definido en términos de calculoRecursivo(1), lo
cual es una definición circular y por tanto sin fin.
Un algoritmo recursivo consta de:
1. Al menos un caso trivial o base. Éste no vuelve a ejecutar el algoritmo y
se realiza cuando se cumple cierta condición.
2. El caso general, que vuelve a llamar al algoritmo con un caso más peque˜
no del mismo.
Una tarea muy común en programación es buscar elementos dentro de una
colección de datos. La forma más sencilla de hacerlo es comparar el dato
buscado con cada uno de los elementos de la colección en forma secuencial.
El orden de este algoritmo es O( n), porque en el peor de los casos se tienen
que comparar todos los elementos para descubrir que el dato no está en la
colección.
En caso de que la colección esté ordenada puede usarse un algoritmo más
rápido, denominado búsqueda binaria, para hacer las búsquedas. El
algoritmo consiste en comparar el elemento de la mitad del arreglo con el
buscado, si ambos valores son iguales se termina la búsqueda puesto que el
elemento está en la colección. Si el elemento buscado es menor, entonces se
sabe (porque el arreglo está ordenado) que se encuentra en la primera mitad
del arreglo, en otro caso se encuentra en la mitad superior del arreglo. Este
proceso de ir dividiendo el arreglo de búsqueda en mitades se repite hasta
encontrar el elemento, o bien, determinar que el elemento buscado no se
encuentra.
/**
* Método para localizar un dato en un arreglo ordenado
* @param datos -- arreglo de enteros en donde está el espacio de búsqueda
* @param buscado -- elemento a buscar
* @return int -- posición del elemento encontrado o -1 si no está
*/
public int busquedaBinaria(int [] datos, int buscado) {
int inf = 0, sup = [Link] - 1;
int mitad;
while (inf <= sup) {
mitad = (inf + sup)/2;
//calcula la mitad del arreglo
201
if (datos[mitad] < buscado)
//mitad inferior
inf = mitad + 1;
else if (datos[mitad] > buscado)
// mitad superior
sup = mitad - 1;
else
return mitad;
// Lo encontró
return -1;
Un algoritmo tiene complejidad log( n) si toma un tiempo constante dividir
el tama˜
no del
problema en una fracción. El log n( x) es aproximadamente igual al número
de veces que x puede ser dividido entre n. As´ı el log2( x) es,
aproximadamente, igual al número de veces que x puede ser dividido entre
2. Se dice que es aproximado porque la función logaritmo tiene un valor
fraccionario que se ignora en el análisis de algoritmos.
El cuerpo del ciclo de la búsqueda binaria es de orden O(1). Falta
determinar cuántas veces se repite. Las iteraciones empiezan con sup - inf =
n-1 y terminan cuando esta diferencia es mayor o igual a 1. Cada vez, esta
diferencia se reduce en la mitad. Ejemplo si la diferencia es de 1024, en la
siguientes iteraciones será: 512 , 64 , 32 , 16 , 8 , 2 , 1 , 0 , − 1. Por tanto, el
algoritmo es de O(log2( n)).
Otra forma de escribir este algoritmo, aunque no se gana en eficiencia, es de
manera recursiva.
/**
* Método recursivo para localizar un dato en un arreglo ordenado
* @param datos -- arreglo de enteros en donde está el espacio de búsqueda
* @param inicio -- posición inicial para la búsqueda
* @param fin -- posición final para la búsqueda
* @param buscado -- elemento a buscar
* @return int -- posición del elemento encontrado o -1 si no está
*/
public int busquedaBinaria(int [] datos, int inicio, int fin, int buscado) {
int mitad = (inicio + fin)/2;
if (inicio > fin)
return false;
else if (datos[mitad] < buscado)
return busquedaBinaria(a, mitad + 1, fin, buscado);
else if (datos[mitad] > buscado)
return busquedaBinaria(a, inicio, mitad - 1 , buscado); else
return mitad;
// Lo encontró
202
AP ´
ENDICE A. RECURSI ´
ON
Al escribir un método recursivo se debe considerar lo siguiente: 1. Cómo
resolver el problema en términos de él mismo pero para un grupo menor de
datos.
2. Cómo disminuye el tama˜
no del problema en cada llamada recursiva.
3. Cuál es el caso base.
4. Cómo se llega al caso base.
A.1.
B´
usqueda con retroceso
La búsqueda con retroceso o backtracking es una técnica de programación
que consiste en permitir elegir entre varias posibilidades un camino de
solución y en caso de no ser correcto regresar al punto donde se tomó la
decisión. Trabaja construyendo las posibles soluciones candidatas de
manera sistemática. En general, dado una solución candidata s: 1. Se
verifica si s es solución. Si lo es, se hace algo con ella, dependiendo del
problema.
2. Se construyen todas las posibles extensiones de s, y se llama
recursivamente al algoritmo con todas ellas.
Ejemplo A.1. Encontrar una solución al problema de colocar 8 reinas en un
tablero de ajedrez de tal manera que no se “coman” unas a otras. Recordar
que las reinas se mueven en l´ınea recta a lo largo del renglón, columna o
diagonal en la que están.
Un esbozo de solución es el siguiente:
• Colocar la primera reina en cualquier posición.
• Colocar la segunda reina en un lugar en que no pueda ser atacada por la
primera.
• Colocar la tercera reina en un lugar en que no pueda ser atacada por la
primera ni por la segunda reinas.
• Colocar sucesivamente el resto de las reinas. Si en algún momento no
puede colocarse una reina, ir deshaciendo movimientos de las reinas
colocadas y, a partir de esta reoganización, continuar colocando el resto de
las reinas.
En general, para colocar la i-ésima reina se hace lo siguiente: 1. Inicializar
la selección de posiciones para la i-ésima reina.
2. Intentar la siguiente selección hasta que se tenga éxito o no haya más
posiciones:
A.1. B ´
USQUEDA CON RETROCESO
203
(a) Si está a salvo colocarla.
(b) Si i < 8 intentar colocar la reina i + 1. Si no se puede, eliminarla.
No es conveniente utilizar una matriz para representar el tablero porque
complica la revisión de casillas ocupadas y esta es la operación más
frecuente. Se debe elegir una representación que facilite esta revisión tanto
como sea posible. Cada reina debe estar en una columna diferente y que la
elección de una posición para la i-ésima reina está restringida a la i-ésima
columna. Por lo tanto, la i es el ´ındice de la columna y el proceso de
selección es para colocarla en el renglón j.
En la diagonal de derecha a izquierda todas las localidades tienen la misma
suma de sus coordenadas ( i, j), por eso el rango de valores para esta suma
es de 2 a 16. La otra diagonal tiene la propiedad que la diferencia entre i y j
es constante, aqu´ı el rango va de − 8 a 8.
Como consecuencia de lo anterior, en lugar de utilizar una matriz para
representar el tablero, se utilizan cuatro arreglos: el arreglo columna para
especificar en cuál columna hay una reina. Los otros tres arreglos son de
booleanos para indicar cuál diagonal y cuál renglón están desocupados.
/**
* Programa para colocar reinas en un tablero de ajedrez sin que se ataquen
* unas a otras.
* Objetivo: ilustrar la técnica de backtracking.
* @version: Adaptación del algoritmo de Niklaus Wirth por Amparo López
Gaona
*/
class Reinas {
private int [] columna;
//columna[i] posición de la reina en la columna i.
private boolean [] renglonDesocupado; //si renglonDesocupado[j] = true
// no hay reina en el renglón j
private boolean [] diagonalDIDesocupada; //si diagonalDIDesocupada[k]
== true
//ninguna reina ocupa la diagonal k de der-izq
private boolean [] diagonalIDDesocupada; //si diagonalIDDesocupada[k]
== true
// ninguna reina ocupa la diagonal k de izq-der.
final int tamanio;
boolean respuesta;
M´
etodo A.1. Constructor que recibe la cantidad de reinas que se van a colocar
en un tablero, dependiendo de esta cantidad es el tama˜
no del tablero. La cantidad m´ınima de
reinas es 4.
/**
* Constructor que recibe la cantidad de reinas que se van a colocar
* @param n -- entero con la cantidad de reinas
*/
204
AP ´
ENDICE A. RECURSI ´
ON
public Reinas(int n) {
if (n < 4) n = 4;
tamanio = n;
respuesta = false;
columna = new int[tamanio];
renglonDesocupado = new boolean[tamanio];
diagonalDIDesocupada = new boolean[tamanio*2-1];
diagonalIDDesocupada = new boolean[tamanio*2];
for (int i =0; i < [Link]; i++)
columna[i] = -1;
for (int i =0; i < [Link]; i++) renglonDesocupado[i] =
true;
for (int i = 1; i < [Link]; i++)
diagonalDIDesocupada[i] = true;
for (int i = 0; i < [Link]; i++)
diagonalIDDesocupada[i] = true;
M´
etodo A.2. El método intentar trata de colocar recursivamente cada reina,
empezando por la primera. Cada vez que coloca una reina se llama a s´ı
mismo con la siguiente reina para colocarla. Si no puede colocar la reina
restablece el estado en que estaba el tablero antes de la llamada e intenta
colocarla en otra posición. Esto es el retroceso o backtracking.
/** Método que trata de colocar la i-ésima reina
* @param i -- número de reina que se desea colocar
*/
public void intentar(int i) {
int j= 0;
while (!respuesta && j<tamanio) {
respuesta = false;
if (renglonDesocupado[j] && diagonalDIDesocupada[i+j]
&& diagonalIDDesocupada[i-j+tamanio-1]) {
colocarReina(i,j);
if (i < (tamanio-1)) {
// Trata de colocar otra reina
intentar(i+1);
if (! respuesta)
//Deshace la jugada
quitarReina(i,j);
} else respuesta = true;
j++;
A.1. B ´
USQUEDA CON RETROCESO
205
M´
etodo A.3. El método colocarReina marca en el tablero los lugares en los
que no puede colocar otra reina a partir de que se colocó una reina en la
posición ( i, j).
/**
* Marca los lugares peligrosos para otras reinas, si se coloca ésta
* en la posición (i,j)
*/
private void colocarReina(int i, int j) {
columna[i] = j;
renglonDesocupado[j] = false;
diagonalDIDesocupada[i+j] = false;
diagonalIDDesocupada[i-j+tamanio-1] = false;
M´
etodo A.4. El método quitarReina deshace el trabajo realizado por el
método colocarReina, es decir, quita la marcas de peligro que se ten´ıan al
colocar a una reina en la posición ( i, j).
private void quitarReina(int i, int j) {
renglonDesocupado[j] = true;
diagonalDIDesocupada[i+j] = true;
diagonalIDDesocupada[i-j+tamanio-1] = true;
M´
etodo A.5. El método resolverReinas trata de colocar las reinas, a partir de
la primera, llamando al método intentar. Si colocan todas, muestra la
solución y en caso contrario avisa que no tiene solución el problema.
/**
* Método para resolver el problema de colocar n-reinas en un tablero de
n*n
*/
public void resolverReinas() {
intentar(0);
if (respuesta) {
[Link]("La solución es ");
for (int i=0; i < tamanio; i++)
[Link](columna[i]+" ");
[Link]();
} else
[Link]("No hay solución ");
M´
etodo A.6. Método que crea un objeto de la clase Reinas e intenta colocar
las que se deseen.
206
AP ´
ENDICE A. RECURSI ´
ON
public static void main(String[] pps) {
if ([Link] != 1) {
[Link]("Forma de uso: Reinas numeroDeReinas");
} else {
int nReinas = (Integer(pps[0])).intValue();
Reinas reinitas=new Reinas(nReinas);
[Link]();
Ap´
endice B
Algoritmos de ordenamiento
En este apéndice se presentan algunos algoritmos de ordenamiento con un
análisis de su eficiencia.
B.1.
Burbuja
El algoritmo más popular, que no el más eficiente, para ordenar es el
conocido como algoritmo de la burbuja. Este algoritmo recorre varias veces
el arreglo que se desea ordenar, en cada recorrido va comparando dos
elementos adjuntos y si no están en el orden adecuado los intercambia.
Recorre varias veces el arreglo hasta que no haya intercambios, lo que
significa que ya está ordenado.
El algoritmo de la burbuja se implementa con dos ciclos anidados, en el
ciclo interno se comparan los elementos uno contra otro para colocar el
elemento mayor en la parte baja del arreglo. Cada iteración reduce la
cantidad de veces que se tiene que hacer la iteración interna. Al final de
cada iteración interna, el elemento mayor se coloca en su posición.
/** Algoritmo de la burbuja para ordenar datos
* @param datos -- arreglo de datos enteros que se desea ordenar
*/
public void burbuja (int [] datos) {
int n = [Link];
for (int i = n - 1; i >0; i--)
for (int j = 0; j < i; j++)
if (datos[j] > datos[j+1]) {
// Están desordenados por tanto
int tmp = datos[j];
// los intercambia
datos[j] = datos[j+1];
datos[j+1] = tmp;
207
208
AP ´
ENDICE B. ALGORITMOS DE ORDENAMIENTO
Para calcular el orden de ejecución de este algoritmo, se toma en cuenta que
el ciclo interno se realiza n − 1 veces la primera vez, en la segunda n − 2,
etc., hasta hacerlo una sola vez. Es decir, se realiza n( n + 1) / 2 = ( n 2 + n)
/ 2. Por otro lado, el cuerpo del ciclo interno toma un tiempo constante en
su ejecución. Por tanto, el orden del algoritmo es O(( n 2 + n) / 2) = O( n 2).
B.2.
Inserci´
on
A diferencia del algoritmo de la burbuja, con este algoritmo se van
ordenando subarreglos, primero los dos primeros datos, luego los tres
primeros, luego los cuatro primeros, etc., tomando un elemento y buscando
su ubicación dentro de cada subarreglo.
/**
* Algoritmo de inserción para ordenar datos
* @param datos -- arreglo de datos enteros que se desea ordenar
*/
void insercion (int [] datos) {
for (int i = 1; i < [Link]; i++) {
int elemento = datos[i];
int j = i-1;
while (j >= 0 && elemento < datos[j]) {
datos[j+1] = datos[j];
j--;
}
datos[j+1] = elemento;
// Coloca el elemento en su posición
El ciclo externo se ejecuta n− 1 veces, el ciclo interno puede terminar
pronto, pero en el peor de los casos debe intercambiar todos los elementos
(esto ocurre cuando el arreglo está en orden inverso). Por tanto en el peor
caso, las iteraciones internas se realizan 1 + 2 + 3 + ... + ( n − 1) veces, es
decir n( n − 1) / 2 veces. Por tanto el algoritmo es de orden O( n 2).
B.3.
Quick sort
Este algoritmo fue desarrollado por C. Antony R. Hoare en 1960. La idea es
sencilla y bonita.
Se toma un elemento, denominado pivote, y se coloca en el arreglo de tal
suerte que todos los elementos de su izquierda sean menores que él y todos
los elementos de su derecha mayores.
Al hacer esto, el pivote queda en el lugar que le corresponde en el arreglo
ordenado y los otros elementos están la parte del arreglo correcta. Por
ejemplo, si se tiene un arreglo con los valores {
2,97,17,37,12,46,10,55,80,42,39 } y se toma como pivote el número 37, el
B.3. QUICK SORT
209
arreglo quedará: { 12,10,17,2,37,97,46,55,80,42,39 }. Con lo cual la tarea
de ordenar el arreglo se convierte en hacer lo mismo para dos subarreglos
(el de la izquierda y el de la derecha).
Clase B.1. Clase para ordenar un arreglo de datos utilizando el algoritmo
quicksort.
/**
* Clase que implementa el algoritmo del quicksort para ordenamiento
*/
class QuickSort {
private int[] datos;
/**
* Constructor que recibe un arreglo
* @param a -- arreglo de enteros que se desea ordenar
*/
public QuickSort(int[] a) {
datos = a;
M´
etodo B.1. Método para colocar el pivote.
El algoritmo consiste en empezar por colocar el elemento pivote al inicio
del arreglo, intercambiándolo con el elemento que ah´ı se encuentra, luego
se recorre el arreglo buscando elementos que no cumplan la condición (a la
izquierda los menores y a la derecha los mayores); al encontrar uno del lado
izquierdo mayor que el pivote y uno del lado derecho menor que él, se
intercambian. Esto se hace hasta que se hayan revisado todos los elementos
del arreglo.
/*
* Método privado para colocar el pivote para ordenar datos
* @param inicio -- ´
ındice a partir de donde se recorre el arreglo
* @param fin -- ´
ındice hasta donde se recorre el arreglo
* @param pos -- posición del pivote
*/
private int pivote(int inicio, int fin, int pos) {
int izq = inicio +1;
int der = fin;
intercambiar(inicio, pos);
// Pone el pivote en la posición inicial
while (izq < der)
if (datos[izq] < datos[inicio])
izq++;
else if ( datos[--der] < datos[inicio])
intercambiar(izq, der);
210
AP ´
ENDICE B. ALGORITMOS DE ORDENAMIENTO
intercambiar(inicio, --izq); // Pone el pivote en su lugar return izq;
M´
etodo B.2. Método privado para intercambiar dos datos del arreglo.
private void intercambiar(int i, int j){
int tmp = datos[i];
datos[i] = datos[j];
datos[j] = tmp;
M´
etodo B.3. Método para ordenar un arreglo de datos utilizando el quicksort.
/**
* Método para ordenar un arreglo de datos
* @param inf -- ´
ındice inferior del arreglo
* @param sup -- ´
ındice superior del arreglo
*/
public void quickSort(int inf, int sup) {
if (inf >= sup) return;
// El arreglo ya está en orden.
int indicePivote = (inf+sup)/2;
// Toma el elemento de la mitad como pivote
indicePivote = pivote(inf, sup, indicePivote);
// ordena los dos subarreglos
quickSort(inf, indicePivote);
quickSort(indicePivote + 1, sup);
Para calcular la eficiencia del algoritmo se considera el pivote en el centro
del arreglo, con lo cual cada vez se trabaja con la mitad de elementos. La
primera llamada recursiva es con dos arreglos de la mitad de elementos,
para cada una se hacen dos llamadas con dos arreglos de la mitad de la
mitad de elementos, etc. Esto lleva a tener un algoritmo de O(log n) para la
parte del pivote, y como esto se realiza n veces, se tiene el algoritmo es de
orden O( n log n).
Ap´
endice C
Normas de estilo en Java
Este apéndice contiene una recopilación de las gu´ıas de estilo seguidas en
los programas de este libro, recomendadas por la gente de Sun para
organizar y dar formato al código fuente de los programas en Java. La
documentación completa se encuentra en la dirección
[Link] Archivos
con código Java.
– Cada archivo debe contener una clase o interfaz y el orden en que los
elementos deben aparecer en el archivo es:
∗ Comentario de inicio que incluya el nombre del programa, el objetivo,
autor y versión.
∗ Instrucciones import.
∗ Declaración de la clase o interfaz.
Elementos de una clase.
– Comentario de inicio.
– Encabezado de la clase.
– Variables estáticas.
– Variables de instancia.
– Métodos. Primero los constructores.
Nombres de identificadores.
– Elegir nombres con significado en el contexto y de fácil lectura. Por
ejemplo, es mejor pesoNeto que pesNet y que p o pn.
211
212
AP ´
ENDICE C. NORMAS DE ESTILO EN JAVA
– Los identificadores de variables empiezan con minúsculas y si constan de
más de una palabra, cada una después de la primera empieza con
mayúscula,sin espacios en blanco entre ellas.
– Los identificadores de clases, interfaces y paquetes empiezan con
mayúscula.
– Los identificadores de constantes se escriben sólo con mayúsculas. Si
llevan más de una palabra, éstas se separan con guión bajo.
Modificadores de variables.
– En general las variables deben tener visibilidad privada.
– En una interfaz no se utilizan modificadores.
– Es preferible utilizar variables con modificador final a utilizar literales.
Declaraciones.
– Incluir sólo una declaración por l´ınea.
– Tratar de asignar valor inicial en la declaración a menos que su valor se
vaya a calcular después.
– Colocar las declaraciones sólo al inicio de los bloques.
– Evitar declaraciones locales que oculten declaraciones externas.
Alineación (indentación).
– Alinear el código de cualquier bloque con cuatro espacios.
– Colocar la llave de inicio de un bloque al final de la instrucción iterativa,
condicional o encabezado de método y colocar la llave de fin del bloque
alineada con la instrucción que contiene la llave de inicio. Ejemplo: if
(valor > 0) {
instrucción 1;
instrucción 2;
...
instrucción n;
– Alinear cada cláusula case con respecto a la instrucción switch.
– Utilizar l´ıneas en blanco en donde se quiera llamar la atención de
caracter´ısticas del programa.
– Dejar una l´ınea en blanco antes de iniciar un método.
213
– Dejar una l´ınea en blanco entre las declaraciones y las instrucciones.
– Dejar un espacio después de la coma que separa cada parámetro.
– Dejar un espacio antes y otro después de cada operador binario.
– No dejar espacio en blanco después de un paréntesis izquierdo ni antes de
uno derecho, pero s´ı antes de un paréntesis izquierdo.
– No dejar espacio antes de un punto y coma.
– No poner espacios entre los corchetes de un arreglo.
Comentarios.
– Suponer que el lector de la documentación no sabe lo que hace el
programa.
– Asegurar que los comentarios sean precisos y concisos.
– Mantener actualizados los comentarios.
– Cada archivo fuente debe contener un bloque de documentación con
información acerca del contenido y del autor.
– Cada clase, interfaz y método debe tener un comentario que describa su
propósito.
– No esperar a terminar el programa para documentarlo.
Ap´
endice D
El programa javadoc
El programa javadoc tiene como propósito generar documentación para los
programas escritos en Java. Para obtener esta documentación el programa
javadoc busca los comentarios escritos entre los s´ımbolos /** y */.
La documentación que genera son documentos escritos en HTML, para que
puedan ser vistos a través de cualquier navegador. Esta documentación
queda legible, ordenada y consistente para todas las clases.
En este apéndice se describen las marcas permitidas dentro de los
comentarios para javadoc y utilizadas en este libro. La documentación
completa se encuentra en la dirección
[Link]
[Link] Las marcas utilizadas en el libro son:
• @author nombre. Para especificar el autor del programa. De esta marca
puede haber más de una en un programa, una por cada autor.
• @param nombre descripción. Para especificar cada parámetro de un
método.
• @return descripción. Para especificar el valor que devuelve un método.
• @see referencia. Para especificar que en el código de esa clase se hace
referencia a objetos de otras clases.
• @version descripción. Para especificar la versión de la clase; ésta se ha
usado para incluir la fecha de realización de la clase.
• @throws clase descripción. Para especificar la clase de excepción que se
puede disparar en ese método y en qué situación.
215
216
AP ´
ENDICE D. EL PROGRAMA JAVADOC
Para utilizar esta herramienta, la clase debe estar precedida de la palabra
public. Desde el sistema operativo se debe teclear la instrucción: javadoc
[Link].
con lo cual se genera una serie de archivos con la documentación, en
particular uno con el mismo nombre de la clase y extensión html.
En la primera parte del documento .html se resalta la palabra class, si la
documentación es para una clase y si se trata de una interfaz está resaltada
la palabra interface. Si estos archivos están en un paquete, la
documentación empieza con esta información aunque con letra más peque˜
na que las palabras antes descritas.
En la segunda parte de la documentación se tiene el nombre de la clase y se
presenta un árbol con la jerarqu´ıa de clases de donde desciende la clase en
cuestión, empezando por la clase Object. Luego se especifica las interfaces
que implementa.
En la tercera parte está el encabezado, en Java, de la clase, el comentario
que se incluyó para la misma y la indicación de ver otras clases utilizadas
en la clase que se está documentando.
El autor y la versión no aparecen a menos que el el documento se hubiera
generado con la siguiente instrucción:
javadoc -version -author CuentaCré[Link]
En la siguiente parte de la documentación se encuentran las variables
protegidas, ya sean de la superclase o bien las definidas en esta clase.
En la siguiente parte de la documentación se tiene un resumen de cada uno
de los constructores de la clase. Luego se incluye una tabla que en la
primera columna tiene el tipo de valor que devuelve el método y en la
segunda la firma y abajo de ella el comentario acerca del propósito del
método. La tabla tiene tantos renglones como métodos tenga la clase. Cada
método aparece subrayado, porque en la siguiente sección se tiene más
información acerca de los métodos y se puede ir a ella directamente a través
de esta liga.
La siguiente sección contiene una lista con los métodos que hereda de cada
una de sus superclases, incluyendo todos los métodos de la clase Object.
Finalmente se tiene el detalle de cada método, en el cual se incluye su
firma, los parámetros que recibe y el tipo de valor que devuelve. A estas
descripciones se puede llegar desde los enlaces mencionados anteriormente.
Este formato se sigue para todas las clases e interfaces que utilicen la forma
de comentarios y etiquetas requeridas por el programa javadoc.
Ap´
endice E
La clase StreamTokenizer
En este apéndice que presenta una recopilación de los métodos de la clase
StreamTokenizer utilizada en varios ejemplos del libro.
La clase StreamTokenizer permite extraer elementos, denominados tokens,
de un flujo de datos. Los elementos que puede reconocer son
identificadores, números, cadenas entre comillas y varios estilos de
comentarios.
El constructor de esta clase recibe un flujo de lectura que puede ser de
cualquier subclase de la clase abstracta Reader.
El método nextToken extrae el siguiente elemento del flujo y devuelve una
constante que especifica el tipo de elemento extra´ıdo:
1. TT EOL indica que se ha llegado al final de una l´ınea.
2. TT EOF indica que se ha llegado al final del archivo.
3. TT WORD indica que el elemento le´ıdo es una palabra. En este caso
almacena la palabra en el atributo sval.
4. TT NUMBER indica que el elemento le´ıdo es un número, en cuyo caso
lo almacena en el atributo nval.
Otros métodos que se tienen en la clase StreamTokenizer son:
• commentChar(int ch) para especificar que su parámetro, un carácter, se
debe considerar inicio de un comentario de una l´ınea.
• eolIsSignificant(boolean) determina si el fin de l´ınea se debe tratar como
elemento.
• lineno() regresa el número de l´ınea dentro del flujo.
• lowerCaseMode(boolean) si el parámetro es true el elemento se convierte
a minúsculas automáticamente.
217
218
AP ´
ENDICE E. LA CLASE STREAMTOKENIZER
• nextToken() lee el siguiente elemento en el flujo y devuelve un entero
indicando de qué tipo es.
La documentación completa de esta clase se encuentra en la página
[Link]
tml
Bibliograf´ıa
[Budd00]
Budd, T. Classic Data Structures in Java. Addison Wesley, 2000.
[Carr06]
Carrano, F. Data Structures and Algorithms with Java. Prentice Hall, 2a.
ed., 2006.
[DaJW11] Dale, N., Joyce, D. y Weems, C. Object Oriented Data
Structures Using Java.
James & Barlett Computer Science, 2011.
[Drak06]
Drake, P. Data Structures and Algorithms in Java. Prentice Hall, 2006.
[Gaon11]
Gaona, A. L. Introducci´
on al desarrollo de programas con Java. Las Prensas de Ciencias, 2a. ed.,
2011.
[GoGo08] Goldman, S. y Goldman, K. A Practical Guide to Data
Structures and Algorithms Using Java. Chapman & Hall/CRC, 2008.
[Hors09]
Horstmann, C. Big Java. Wiley Sons, 2009.
[HoSa04]
Horowitz, E. y Sahni, S. Fundamentals of Data Structures in Pascal.
Pitman, 2a.
ed., 2004.
[Jenk98]
Jenkins, M. Abstract Data Types in Java. McGraw Hill, 1998.
[Prei00]
Preiss, B. Data Structures and Algorithms with Object Oriented Design
Patterns in Java. J Wiley, 2000.
[Sahn04]
Sahni, S. Data Structures, Algorithms, and Applications In Java. Silicon
Press, 2a. ed., 2004.
[Weis07]
Weiss, M. Data Structures and Algorithm Analysis in Java. Addison
Wesley, 2a.
ed., 2007.
[Weis09]
Weiss, M. Data Structures and Problem Solving Using Java. Addison
Wesley, 4a.
ed., 2009.
[Wirt76]
Wirth, N. Algorithms + Data Structures = Programs. Prentice-Hall, 1976.
219
´
Indice alfab´
etico
árbol, 105
búsqueda
AVL, 132
binaria, 200
altura, 106
con retroceso, 202
binario, 107
secuencial, 200
nivel, 106
backtracking, 202
ra´ız, 105
clase
árbol binario
ArbolBinarioBusquedaR, 128
de búsqueda, 119
ArbolBinarioBusqueda, 121
balanceado, 132
Cola, 97
completo, 158
Conjunto, 5
de búsqueda
Heap, 159
con repetidos, 128
Lista, 45
frontera de un, 140
NodoArbol, 108
NodoAvl, 133
algoritmo
Nodo, 44
acomodo de trenes, 88
Pila, 76
adivinar animales, 115
StreamTokenizer, 5
búsqueda binaria, 200
TablaDeDispersionA, 192
balanceo de paréntesis, 65
TablaDeDispersion, 186
binsort, 40
cola, 83
evaluación de expresiones, 67, 112
circular, 101
heapsort, 145
de prioridad, 143
inserción, 208
colisión, 175
ordenamiento por recipientes, 40
complejidad de los algoritmos, 16
quicksort, 208
cubeta, 176
radix sort, 177
round-robin, 85
direccionamiento abierto lineal, 175
salida laberinto, 72
eficiencia de algoritmos, 15
salida rápida de laberinto, 92
estructura de datos, 2
simulación, 148
eventos discretos, 148
torres de Hanoi, 63
análisis de algoritmos, 15, 16
factor de carga, 176
221
222
INDICE ALFAB ´
ETICO
FIFO, 83
ordenamiento
función de dispersión, 170, 171
binsort, 40
uniforme, 176
burbuja, 207
perfecta, 172
en recipientes, 40
heapsort, 145
heap, 159
inserción, 208
heapsort, 145
quicksort, 208
radix sort, 177
interfaz
Apilable, 60
pila, 59
ArbolBuscable, 120
prueba cuadrática, 193
Conjuntable, 2
prueba lineal, 193
Dispersable, 172
EncolableConPrioridad, 144
recorrido en inorden, 110
Encolable, 84
recorrido en postorden, 110
Iterator, 11
recorrido en preorden, 110
Listable, 34
recorrido por niveles, 111
iterador, 11
recursión, 199
caso base, 199
LIFO, 59
lista, 33
simulación, 148
con 2 referencias, 51
subárbol, 105
ligada, 44
llave, 170
tabla de dispersión, 171
TAD, 2
max-heap, 159
árbol binario de búsqueda, 120
min-heap, 159
con repetidos, 128
cola, 84
nodo, 44
cola de prioridad, 144
ancestro, 106
conjunto, 2
cabecera, 46
heap, 159
descendiente, 106
lista, 34
externo, 105
pila, 60
grado de un, 105
tabla de dispersión, 186
hijo, 105
tipo abstracto de datos (TAD), 2
hoja, 105
tipo de datos, 1
interior, 105
tope de una pila, 60
padre, 105
notación infija, 67
notación O grande, 16
notación postfija, 67
notación prefija, 67
Estructuras de datos con con Java
editado por la Facultad de Ciencias
de la Universidad Nacional Autónoma de México,
se terminó de imprimir el 20 de octubre de 2017
en los talleres de Impresos Vacha S. A. de C. V.
José María Bustillos No. 59. Col. Algarín
C.P. 52170. Metepec. Estado de México.
El tiraje fue de 500 ejemplares.
Está impreso en papel Creamy book de 60 g.
En su composición se utilizó tipografía Computer Modern de 11:13.5, 14:16
y 16:18 puntos de pica.
Tipo de impresión: offset.
El cuidado de la edición estuvo a cargo de
Patricia Magaña Rueda