Documentos de Académico
Documentos de Profesional
Documentos de Cultura
de estructuras
de datos
Bibliotecas
de colecciones
Jordi Álvarez Canal
P06/75001/00583
Módulo 9
© FUOC • P06/75001/00583 • Módulo 9 Diseño de estructuras de datos
Índice
Introducción ............................................................................................ 5
Objetivos ................................................................................................... 7
Resumen .................................................................................................... 44
© FUOC • P06/75001/00583 • Módulo 9 Diseño de estructuras de datos
Solucionario ............................................................................................. 47
Glosario ..................................................................................................... 49
Bibliografía .............................................................................................. 50
Anexo.......................................................................................................... 51
© FUOC • P06/75001/00583 • Módulo 9 5 Diseño de estructuras de datos
Introducción
Ya hemos estudiado las colecciones básicas que con seguridad deberéis utilizar
de modo habitual si os dedicáis a desarrollar aplicaciones. También hemos
presentado distintas variantes para sus implementaciones.
Normalmente, cuando diseñamos la implementación de estas colecciones es- Las ventajas de utilizar una biblioteca
de colecciones, y de bibliotecas de
clases en general, ya se comentan en el
pecializadas o TAD, intentaremos maximizar su reusabilidad. Siempre que po- módulo “Tipos abstractos
de datos” de esta asignatura.
damos “mapearemos la abstracción” para buscar una colección disponible en
nuestra biblioteca de colecciones, y evitar esfuerzos innecesarios. La mayoría
de las veces, sin embargo, esto no será posible y deberemos dedicar un esfuerzo
a diseñar e implementar la nueva abstracción. Mantendremos, en cambio, el
criterio de maximizar la reusabilidad con elementos ya desarrollados y proba-
dos, como las colecciones de nuestra biblioteca de TAD preferida.
Otro tema del que no hemos hablado en los otros módulos es el diseño de las
bibliotecas de colecciones. No es lo mismo diseñar una aplicación que diseñar
una biblioteca que deben utilizar otros desarrolladores. Existe un conjunto de
factores que es necesario tener en cuenta con el objetivo de maximizar el ren-
dimiento ofrecido a los usuarios potenciales. Una biblioteca de colecciones
debe ofrecer un conjunto de abstracciones utilizables independientemente del
dominio y bastante versátiles para ser usadas en varias condiciones. En el caso de
encontrarnos en situaciones especiales en las que no podamos usar las abstrac-
ciones proporcionadas, el diseño interno de la biblioteca debe permitir extenderla
adecuadamente y proporcionar así un TAD adecuado a la situación sin necesidad
de partir de cero. La biblioteca debe tener una curva de aprendizaje razonable para
© FUOC • P06/75001/00583 • Módulo 9 6 Diseño de estructuras de datos
Objetivos
2. Ser capaces de elegir tanto los TAD adecuados para satisfacer un conjunto
de requisitos funcionales (operaciones), como las implementaciones para
satisfacer un conjunto de requisitos no funcionales (eficiencia, limitación
de espacio, etc.).
5. Dada una biblioteca de colecciones, saber evaluar las ventajas y los incon-
venientes a partir del conjunto de factores estudiados.
2) Es necesario identificar las restricciones impuestas por el problema. Así, Los patrones de diseño y, más
concretamente, los patrones de
asignación de responsabilidad (Larman,
por ejemplo, podemos desarrollar un TAD que se utilizará desde un gestor de 2003) son elementos que pueden ayudar a
decidir la funcionalidad de un nuevo TAD.
procesos de un sistema operativo. Esto puede requerir que todas las operaciones Este tema se trata en la asignatura
Ingeniería del software orientado a objetos.
de este TAD sean lo más eficientes posible y descarten costes altos (por ejemplo,
lineales o superiores). Esta información constituye una restricción de eficiencia
que es necesario tener en cuenta al diseñar el TAD. También podemos tener la
necesidad de desarrollar un TAD que, entre otras cosas, deba almacenar los
alumnos en activo de un colegio. Imaginemos que el número de alumnos en ac-
tivo del colegio es siempre parecido porque el colegio dispone de una cantidad
de aulas fija. Esta información forma parte de las características de los datos (que Requisitos
no funcionales
forman parte de las restricciones impuestas por el problema), y puede ser crítica
a la hora de elegir la implementación de una colección. Como en el punto an- En el campo de la ingeniería
del software, las restricciones
terior, las restricciones del problema nos pueden llegar por un enunciado, por impuestas por el problema
corresponden a lo que se suele
nuestro conocimiento del contexto o del dominio de aplicación, o por la discu- denominar requisitos no
funcionales.
sión con el cliente o con el resto del equipo de desarrollo.
3) Una vez tenemos clara la funcionalidad y signatura del TAD, así como las
restricciones impuestas por el problema, pasamos al diseño de la estructura
de datos. Esta fase se concreta en dos partes diferenciadas. En primer lugar, es
necesario hacernos una visión general de todas las colecciones necesarias para
implementar el TAD y de su interacción, si es que la hay. Denominaremos blo-
© FUOC • P06/75001/00583 • Módulo 9 10 Diseño de estructuras de datos
Cada una de las tres partes de una dirección tiene unas propiedades bastan-
te diferentes:
Objetivos
Funcionalidades
Eficiencia
Algunos de los tipos de objeto para los cuales necesitaremos construir tipos
Ejemplo
(continuación)
de datos los podemos deducir porque aparecen en la signatura de alguna de
las operaciones: Dominio, DireccionIP, topLevelDomain. Otros –Entidad y Host–
los extraemos del enunciado y posiblemente nos faciliten las cosas en la im-
plementación.
c) RES1C: Dentro del marco general del problema, deberemos ser capaces de
decidir qué se espera que haga nuestra solución (es decir: debemos concretar del
todo cuál es el problema que se debe solucionar). En concreto, deberemos en-
tender qué operaciones se quieren hacer (signatura) y qué se quiere que
haga cada una (funcionalidad). Identificaremos una serie de puntos clave re-
ferentes a RES1C, de manera que posteriormente nos sea más fácil relacionar las
decisiones de diseño con las funcionalidades requeridas.
Para el caso concreto del ejemplo, el enunciado ya nos proporciona las ope-
Ejemplo
raciones completamente definidas. Tenemos operaciones para lo siguiente: (continuación)
Una vez que tenemos claras las funcionalidades y la signatura del TAD, debemos
hilar un poco más fino e identificar los puntos clave del enunciado que hacen
referencia, primero, a las características de los datos y, segundo, a la eficiencia:
El enunciado del ejemplo dice que “el número de servidores será siempre pe-
Ejemplo
queño para todas las entidades”. Esto puede condicionar posteriormente una (continuación)
mantenga más o menos constante. Si el número de elementos tiene una gran va-
riabilidad y elegimos una representación con vector, habrá momentos en los que
podremos tener buena parte de sus posiciones libres y, por lo tanto, estar derro-
chando mucho espacio. En cambio, si elegimos una implementación en memoria
dinámica, ocuparemos siempre la memoria necesaria y liberaremos espacio para
que lo utilicen otros programas cuando no nos haga falta.
relacionadas, ya que elegir una representación con vector para las entida-
des implicará malgastar espacio.
una cota superior podemos utilizar una implementación que utilice una re-
presentación con vector; mientras que si no tenemos esta cota superior, es- Podéis ver las representaciones con
vector en el apartado 6 del módulo
“Contenedores secuenciales” de esta
taremos obligados a emplear representaciones en memoria dinámica o a asignatura.
Identifiquemos los siguientes puntos clave que hacen referencia a los tres
subapartados de características de los datos:
• PC4: lo más habitual es que las entidades sólo tengan un servidor; aunque
pueden tener tantos como quieran, el número de servidores será siempre
pequeño.
Documentación implícita
• PC6: aunque varíen de tanto en tanto, consideraremos que los dominios
No es necesario que las restric-
de primer nivel nunca superarán los 2.000. ciones sobre la eficiencia estén
especificadas en un documen-
to o enunciado. Muchas veces
2) RES2B. El problema nos impondrá unas restricciones sobre la eficiencia estarán implícitas en nuestro
sentido común.
de las operaciones del TAD. La eficiencia que nosotros podamos ofrecer en las
© FUOC • P06/75001/00583 • Módulo 9 16 Diseño de estructuras de datos
• PC7: es necesario buscar una correcta estructura de datos que nos per-
mita hacer posible todas las operaciones de la manera más eficiente.
• PC8: cada vez que hacemos una consulta (de un dominio) es necesario
modificar la estructura de datos con el fin de que, en sucesivas consul-
tas, se minimice el coste para acceder a los servidores consultados más
recientemente. La modificación debe ser O(1).
A partir de los puntos clave obtenidos en las dos fases anteriores y del conoci- Al final de este subapartado
explicaremos más en concreto en qué
consiste una decisión de diseño.
miento de los TAD que podemos utilizar (utilizaremos los de la biblioteca de
clases de la asignatura), debemos elegir qué TAD queremos utilizar y cómo los
combinaremos para resolver nuestro problema. Es decir, esta fase consiste en
tomar una serie de decisiones sobre el diseño solución, y que denominaremos
decisiones de diseño.
El conocimiento de las propiedades de los TAD es básico para esta tarea. Por
otro lado, la experiencia de haber visto otros diseños resulta de mucha ayuda,
ya que permite mapear necesidades expresadas mediante los puntos clave con
estructuras de colecciones.
© FUOC • P06/75001/00583 • Módulo 9 17 Diseño de estructuras de datos
Es evidente que cuando leáis esto por primera vez, tendréis muy poca ex-
periencia en diseño de estructuras de datos. En estas condiciones, un pri-
mer diseño puede ser bastante laborioso, y es probable que cometáis errores
importantes. Eso no os ha de desanimar, ya que todo el trabajo hecho os
servirá sin duda para facilitar y cuidar más los diseños posteriores. a
Hay dos tareas básicas que deberemos hacer en esta tercera fase:
Pero también podríamos proponer una estructura con tres bloques imbrica-
dos, de modo que tendríamos un bloque para los dominios de primer nivel,
otro bloque para las entidades, y un tercero para los servidores.
Figura 1
3) Si todos los puntos clave están cubiertos, perfecto: ¡ya hemos acabado!
4) Si queda algún punto clave por cubrir, intuid si añadiendo algún nuevo
bloque a la estructura podríamos cubrirlo. Si es así, añadid el nuevo bloque y
vayamos a 2.
5) Si quedan puntos por cubrir y no intuís que añadiendo nuevos bloques los
podamos cubrir, volved a 1. Volver a 1 puede querer decir partir de cero (sa-
biendo, sin embargo, que la estructura actual no nos lleva a ninguna parte), o
bien partir de la estructura actual, cambiando alguno de los bloques o bien
modificando la organización.
Ahora que ya hemos presentado las ideas básicas sobre la definición de lo que
denominamos bloques del diseño, podemos concretar la noción de decisión de
diseño.
El punto 2 de este proceso dice que debemos tomar decisiones de diseño para
concretar cada uno de los bloques de la estructura, pero no dice cómo hacerlo.
A continuación, expondremos algunas ideas generales que nos deben ayudar
© FUOC • P06/75001/00583 • Módulo 9 19 Diseño de estructuras de datos
Funcionalidad
Muchas veces nos pedirán operaciones que supongan hacer cálculos no trivia-
les sobre los datos originales. Estos cálculos representan un tiempo de cálculo
que preferiríamos ahorrarnos. Hacer estos cálculos con antelación y mantener
los datos precalculados en nuestra estructura puede suponer un ahorro de
tiempo considerable.
Esto implica que cada vez que modifiquemos los datos deberemos rehacer
también los cálculos, y esto añade complejidad a nuestros algoritmos. Pero
muchas veces, hacer estos cálculos para mantener los datos “actualizados” será
más rápido que hacer los cálculos cada vez que queremos obtener los datos y,
por lo tanto, podemos salir ganando en eficiencia.
3) Recorridos ordenados
Datos
Eficiencia
Como regla general, y siempre que no se invalide alguno de los puntos clave, de-
beremos dar prioridad a aquellas operaciones que se ejecuten un número más ele-
vado de veces. Normalmente, se trata de las operaciones de consulta del TAD.
© FUOC • P06/75001/00583 • Módulo 9 21 Diseño de estructuras de datos
1) Búsqueda eficiente
Muchas veces, el propio enunciado nos pedirá alcanzar como máximo un cos-
te determinado para una operación concreta. A partir de la estructura en blo-
ques que tengamos pensada, deberemos elegir los TAD adecuados que nos
permitan alcanzar el coste pedido u optimizar al máximo la operación.
Primera aproximación
Ahora vamos a ver cómo concretamos este bloque. Nos piden optimizar las
operaciones de acceso, alta y baja (PC9). En total tendremos muchos domi-
nios (PC5 + PC6), de modo que es necesario una implementación que rea-
lice estas operaciones en tiempos mejor que lineal respecto al número total
© FUOC • P06/75001/00583 • Módulo 9 22 Diseño de estructuras de datos
Hemos concretado el bloque, pero todavía quedan puntos clave por cubrir;
por lo tanto, no podemos dar por buena la solución. Vamos a ver cómo cu-
brimos el resto de puntos clave. Según PC2, necesitamos consultar el nú-
mero de visitas para los 10 dominios más visitados. Para hacerlo:
Figura 3
Hemos añadido un nuevo bloque al de la figura 2. El nuevo bloque es un vector ordenado por número de visitas que nos
permite guardar los 10 dominios más visitados.
Ahora, tal y como vemos en la figura 3, ya tenemos una estructura con dos
bloques, y continuamos cubriendo puntos clave.
Actividad
Justificad la motivación de los encadenamientos dobles en este caso (siempre que quera-
mos que las supresiones de los elementos no tengan coste lineal).
Figura 4
Figura 4
Segunda aproximación
En la estructura que teníamos hasta ahora hay un único bloque para todos
los dominios. Esto nos complica la realización de PC3. En las ideas genera-
© FUOC • P06/75001/00583 • Módulo 9 25 Diseño de estructuras de datos
les que habíamos expuesto, comentabámos que una posibilidad era hacer
corresponder un bloque al conjunto de elementos por recorrer.
Así pues, podríamos tener un bloque para los dominios de primer nivel,
otro para las entidades (de hecho, un bloque para cada conjunto de enti-
dades correspondiente a un dominio de primer nivel) y, finalmente, un
bloque para los servidores de cada entidad.
Figura 5
Veamos a continuación cómo concretamos los tres bloques nuevos con los
que hemos reemplazado el antiguo megabloque. El bloque correspondiente a
los dominios de primer nivel se puede implementar como una tabla de disper-
sión, ya que por PC6 sabemos que varían poco y nunca superarán los 2.000
© FUOC • P06/75001/00583 • Módulo 9 26 Diseño de estructuras de datos
Con todo esto, hemos concretado todos los bloques y hemos llegado a una
combinación de TAD que encontramos representada en la figura 6.
Figura 6
Hemos cubierto todos los puntos clave excepto uno: el PC8, del que toda-
vía no hemos hablado. El punto PC8 nos dice que es necesario modificar
la estructura cada vez que accedemos a ella para –en sucesivas consultas–
minimizar el coste de acceso a los servidores consultados más recientemen-
te. Y la modificación se ha de hacer en O(1).
En primer lugar, debemos ver cómo varía el tiempo de acceso para los di-
ferentes servidores de la misma entidad. Dado que los representamos con
una lista, que presenta un acceso secuencial, tardaremos menos si el servi-
dor es el primero de la lista, y más si es el último.
Con esto, tenemos cubiertos todos los puntos clave y las decisiones de di-
seño tomadas parecen no invalidar las anteriores (lo comprobaremos de
manera rigurosa en la fase 4). Por lo tanto, damos por bueno el resultado
obtenido por la fase 2 y continuamos.
1) Realizar la implementación.
Veamos, a continuación, cómo queda esta descripción para el caso que tra- Ejemplo
(continuación)
bajamos como ejemplo. Con el objetivo de hacer más inteligibles, claras y
concisas las explicaciones, tomaremos las siguientes convenciones:
1.5. Implementación
Es necesario destacar una serie de puntos. En primer lugar, el código debe ser tan
claro e independiente de implementaciones concretas como sea posible. Con este Encontraréis la implementación
del ejemplo como recurso
electrónico de la asignatura.
objetivo, únicamente haremos referencia a las clases que corresponden a la imple-
mentación concreta cuando creemos una instancia, cuando la extendamos, o
bien cuando sea realmente imprescindible. Por contra, deberemos hacer siempre
referencia a la interfaz que representa el TAD.
Por otro lado, como sabéis, la biblioteca de TAD de la asignatura usa tipos para-
métricos o genéricos. La definición de estructuras de datos complejos con tipos
paramétricos puede resultar demasiado complicada, ya que fácilmente nos po-
demos encontrar con clases que tienen parámetros que a la vez son clases que
tienen otros parámetros, que... En casos como éstos es una buena práctica defi-
Revisad como ejemplo las clases
DiccionarioTLD o DiccionarioEntidades.
nir nuevas clases sin parámetros equivalentes a las clases con parámetros. De
este modo, podemos hacer referencia a nombres de clases simples en lugar de
tener nombres complicados y, al mismo tiempo, mantenemos todas las ventajas
del tipaje estricto y sin necesidad de conversiones (casting) que nos proporcio-
nan los tipos paramétricos.
Hasta este momento hemos presentado los TAD usados más habitualmente
para guardar colecciones de elementos y sus implementaciones más clásicas.
Dado que se trata tanto de abstracciones como de implementaciones amplia-
mente usadas en el mundo del desarrollo de software, resulta muy útil dispo-
ner de bibliotecas que las proporcionen sin necesidad de desarrollarlas desde
cero cada vez. Ahora bien, una biblioteca de estas características se puede di-
señar de muchas maneras diferentes. ¿Qué propiedades son deseables en una
biblioteca de TAD? ¿Qué la hace más útil para los desarrolladores? ¿Y qué la
puede hacer tediosa de utilizar? En este apartado, ofrecemos una visión gene-
ral que aborda estas preguntas.
2.1.3. Extensibilidad
2.1.4. Eficiencia
2.1.5. Fiabilidad
También es muy importante que una biblioteca de colecciones sea fiable. Cual-
quier biblioteca debe estar, evidentemente, libre de errores. Esto no significa, sin
embargo, que no se pueda producir una situación de error de programación den-
tro del código de la biblioteca. Esta situación la puede provocar un error de pro-
gramación en la aplicación usuaria de la biblioteca. Si esto sucede, es importante
que el código de la biblioteca avise de la situación de error y el aviso sea lo más
clarificador posible para el programador de la aplicación, de modo que le permita
encontrar con la mayor facilidad posible la causa del error.
El tema de la fiabilidad es realmente crítico, en el ámbito de las bibliotecas de Podéis recordar la programación por
contrato en el módulo “Tipos
colecciones. Es posible trabajar con la misma instancia de una colección desde abstractos de datos” de esta asignatura.
diferentes partes de una aplicación. Esto significa que podemos, por ejemplo,
acceder por un lado a los elementos de la colección y, al mismo tiempo, por
otro, dar elementos de alta o de baja. Compaginar de modo concurrente el ac-
ceso a los elementos de una colección con su modificación no suele estar pre-
visto en los contratos ofertados por las colecciones. Darse cuenta de este tipo
de situaciones conflictivas y avisar de la situación de error adecuada en cada
caso es realmente complejo, y las bibliotecas no siempre lo consiguen.
© FUOC • P06/75001/00583 • Módulo 9 33 Diseño de estructuras de datos
2.1.6. Usabilidad
En la misma linea, debe ser razonablemente fácil aprender a usar una biblio-
teca de colecciones. El esfuerzo necesario para aprender se ha de ver recom-
pensado por el beneficio que se extraiga. En este sentido, la simplicidad es un
valor que es necesario tener en cuenta y del cual únicamente nos debemos
apartar cuando esté justificado por algún otro motivo que potencie alguna de
las otras propiedades deseables.
2.1.8. Homogeneidad
jerarquía será paralela a la de TAD, y no debe ser necesariamente conocida por En la Java Collections
Framework (JCF) se pueden
los usuarios de la biblioteca si no necesitamos utilizar la extensibilidad de la apreciar fácilmente las dos je-
rarquías: la de TAD (encabeza-
biblioteca. La complejidad de la jerarquía de implementaciones estará deter- da por la interfaz Collection) y
minada por el buen uso de la orientación a objetos, y no incide en la simpli- la de implementaciones (enca-
bezada por la clase abstracta
cidad de la biblioteca para los usuarios. AbstractCollection).
Aparte de todas las propiedades de diseño, tanto para la interfaz como para la
implementación que hemos comentado hasta ahora, hay otros aspectos rela-
cionados principalmente con su implementación y que pueden dar un valor
añadido para los usuarios de la biblioteca. A continuación, comentaremos dos
muy importantes: la persistencia y la concurrencia.
2.2.1. Persistencia
Por lo tanto, en ocasiones puede ser muy interesante guardar instancias de colec-
ciones en memoria persistente. Programar los algoritmos necesarios para guardar
en memoria persistente una colección tiene sus dificultades. Entre otros aspectos
técnicos que conviene tener en cuenta, es necesario revisar las referencias a ob-
jetos (direcciones de memoria en realidad) en un medio diferente a la memoria
primaria del ordenador; y, al hacerlo, es necesario ir también con cuidado y de-
tectar los ciclos de referencias a objetos. El esfuerzo necesario para programar este
tipo de algoritmos puede ser importante, así que es muy útil que la biblioteca
misma proporcione la persistencia. La mayoría de las colecciones no proporcio-
nan, sin embargo, esta funcionalidad y dejan la programación al usuario.
2.2.2. Concurrencia
Se podría dar el caso de que dos hilos de ejecución accediesen a una misma ins-
tancia de colección y, mientras uno intentase añadir un elemento a la colección,
el otro intentase borrar otro. Este tipo de accesos concurrentes a la misma instan-
cia de una colección son peligrosos y pueden acarrear problemas de ejecución
que normalmente generan situaciones inconsistentes en los datos. La documen-
tación de la biblioteca debe ser muy clara respecto al tipo de accesos concurrentes
que son permisibles en una colección (esta información forma parte del contrato
de uso de la colección). En la mayoría de los casos, es únicamente permisible rea-
lizar varias operaciones concurrentes sobre la misma colección si todas son de
lectura; es decir, si ninguna de ellas modifica el estado de la colección.
A lo largo del texto hemos presentado las estructuras de datos básicas, y en este
módulo hemos puesto el énfasis en un estudio más global tanto de la combina-
ción de estas estructuras, como de su agrupación en bibliotecas. Llegados a este
punto, es el momento de repasar algunas de las bibliotecas de colecciones más
conocidas y usadas. De esta manera, podréis relacionar tanto los conocimientos
teóricos adquiridos hasta ahora, como los más específicos de las bibliotecas tra-
bajadas en el texto (la de la asignatura y, más brevemente, la Java Collections)
con otras bibliotecas que presentan cada una un diseño particular a unos obje-
tivos básicos, que no siempre son los mismos.
Esta biblioteca está desarrollada en Java y forma parte del mismo JDK propor-
cionado por Sun Microsystems. Por este motivo, es ampliamente usada dentro
del mundo Java y se ha convertido en casi un estándar, gracias a una buena
integración con otros aspectos cubiertos por el API de Java (muchos elementos
del JDK usan colecciones u otros elementos de la Java Collections Framework).
Como TAD auxiliares que toman parte en el diseño global de la biblioteca, encon-
tramos los representantes para las interfaces java.util.Iterator y java.util.Comparator.
El primero implementa el concepto de iterador, ampliamente trabajado también
en esta asignatura. La interfaz Iterator proporciona una operación adicional de-
nominada remove que pretende proporcionar una solución al problema de con-
sistencia provocado cuando se borra un elemento de una colección mientras al
mismo tiempo se itera. Esta operación está definida como opcional en el Javadoc
del JDK y, por desgracia, la documentación no deja claro qué iteradores concretos
la proporcionan y cuáles no.
Esta técnica es también usada por otras bibliotecas (no únicamente en el ám-
bito de las estructuras de datos) y permite proporcionar sistemas más homo-
géneos, con el inconveniente de convertir comprobaciones que se realizarían
en tiempo de compilación (por lo tanto, más fáciles de encontrar) en compro-
baciones que se realizan en tiempo de ejecución.
La biblioteca proporciona también la implementación de un conjunto de al- Para una definición del término patrón
de diseño y una descripción tanto del
patrón template method, como de otros,
goritmos clásicos. Estos algoritmos han sido definidos usando el patrón de dis- podéis recurrir a los materiales de la
asignatura Ingeniería del software orientado
seño template method, que permite adaptar fácilmente algoritmos generales a a objetos y a su bibliografía recomendada.
Por otro lado, la jerarquía de interfaces proporciona siempre dos interfaces di-
ferentes para cada colección. En primer lugar, proporciona una interfaz con la
versión “inspeccionable” de la colección, que ofrece únicamente métodos
para consultarla, pero no para modificarla. Y, como extensión de ésta, se pro-
porciona una segunda interfaz con la versión completa de la colección, que
añade a la primera métodos para modificarla.
énfasis en la eficiencia. Ofrece las colecciones básicas, como secuencias, colas con
prioridad, conjuntos, árboles, diccionarios, etc.; y otras especializadas que cu-
bren el trabajo con grafos, problemas de redes, cálculos geométricos, criptografía
y optimización combinatoria, entre otros. Para su uso en estos ámbitos, se pro-
porcionan tipos numéricos de precisión arbitraria. Adicionalmente, existen mul-
titud de paquetes opcionales que extienden la biblioteca para una gran variedad
de dominios. Por todo esto, se trata de una biblioteca que disfruta de una amplia
aceptación en el desarrollo de software en C++.
El uso de tipos paramétricos no se limita a eso y permite a LEDA utilizar un me- Programación genérica
canismo muy interesante mediante el que desvincula las partes genéricas de la im-
Este uso de los tipos paramétri-
plementación de un TAD de las partes más concretas. En cierta manera, se trata cos no se ha utilizado en estos
materiales y permite entrever
de una variante de la dualidad interfaz-implementación que ya conocemos. su potencia.
LEDA proporciona dos mecanismos diferentes de iteración para recorrer los ele- Macros iterativos
mentos de una colección. Por un lado, proporciona un conjunto de macros que
Este mecanismo es posible gra-
funcionan de modo similar a la construcción iterativa for del lenguaje. Estos ma- cias a la flexibilidad del lengua-
je C++, heredada de C.
cros nos permiten definir una variable local en la macro que adquiere el valor En un lenguaje como Java,
no sería posible.
de los diferentes elementos de la colección. A partir de aquí, podemos definir un
cuerpo de la macro de modo que se ejecuten una serie de operaciones sobre la
variable (y, por lo tanto, sobre cada uno de los elementos). Este mecanismo no
es tan versatil como el uso de iteradores, pero sirve para necesidades de recorrido
sencillas y evita la creación explícita de objetos adicionales.
(wrappers) para hacer compatibles sus iteradores con los iteradores de la STL, una
biblioteca que se comenta a continuación.
La Standard Template Library (STL) es una biblioteca desarrollada en C++ que pro-
porciona un conjunto de estructura de datos muy flexibles gracias al uso intensi-
vo de tipos paramétricos. Esta biblioteca forma parte de la C++ Standard Library
y es parte, por lo tanto, del estándar que define el mismo lenguaje.
Para la STL, el concepto de iterador es la generalización del concepto de apunta- Al contrario de lo que sucede en
Java, en C++ sí que existen los
dor; o lo que es lo mismo: un apuntador es un tipo concreto de iterador (que per- apuntadores como tales.
Los iteradores son el mecanismo que permite desvincular los algoritmos del tipo
de contenedores a los que son aplicables. Los algoritmos funcionan como plan-
tillas parametrizadas con el tipo de los iteradores. Cada algoritmo realiza una se-
rie de operaciones sobre sus parámetros. Estas operaciones imponen una serie
de restricciones sobre el tipo de los parámetros al especificar los métodos que,
como mínimo, deben tener definidos.
Así, por ejemplo, no podríamos utilizar un algoritmo que usa el método M so-
bre un parámetro si la clase (o tipo) de este parámetro no tiene definido el mé-
todo M: simplemente obtendríamos un error de compilación.
Este mecanismo es un uso muy interesante de los tipos paramétricos, uno de los
pilares de lo que se ha denominado programación genérica, y que aporta un mo-
delo similar al proporcionado por las interfaces de Java, pero sin la necesidad de
definir ningún elemento que corresponda a la interfaz. De algún modo, son las
© FUOC • P06/75001/00583 • Módulo 9 42 Diseño de estructuras de datos
necesidades del mismo algoritmo las que marcan la “interfaz” que deben imple-
mentar los tipos de parámetros (pero sin que esté definida de modo explícito).
A pesar de ser la única de las bibliotecas vistas aquí que está disponible para
más de un lenguaje, es necesario tener presente que las diferentes versiones de
la biblioteca no son, de hecho, equivalentes.
En opinión del mismo Grady Booch (el autor de la parte conceptual de la biblio-
teca), la semántica de cada lenguaje de programación debe influir necesaria-
mente en las decisiones arquitectónicas tomadas al diseñar el software. Por otra
parte, obtendríamos abstracciones que o bien no sacan partido de las capacida-
des únicas del lenguaje, o bien nos llevarían a mecanismos que no pueden ser
eficientemente implementados. Las tres versiones de Booch Components com-
parten, pues, la misma filosofía y diseño desde el punto de vista abstracto, pero
tienen también algunas diferencias importantes para las características de los
lenguajes usados. La versión comentada aquí es la de Ada95.
Para cada uno de los TAD, los Booch Components pueden ofrecer varias for-
mas de gestionar el espacio: acotada (basada en un vector estático), dinámica
(basada en un vector en el que la medida se puede adaptar), y no acotada (la
gestión de la memoria se hace elemento a elemento). Adicionalmente, existe
la posibilidad de utilizar un gestor de memoria externo a la biblioteca y que
el mismo usuario proporcione.
La jerarquía propuesta por los Booch Components tiene una desventaja im-
portante respecto a las otras bibliotecas comentadas: si bien el tema de la ges-
tión de memoria es muy flexible, los TAD proporcionados sólo tienen una
implementación, lo que resta flexibilidad a la biblioteca y no ofrece a sus
usuarios las ventajas de la dualidad interfaz-implementación, que tanto
hemos loado en estos materiales.
Como las otras bibliotecas, los Booch Components permiten iterar sobre los ele-
mentos de los contenedores o bien mediante el concepto ya trabajado en estos
materiales de iterador, o bien mediante la posibilidad de proporcionar un pro-
cedimiento que se ejecute para todos los elementos de un contenedor. Observad
que este segundo mecanismo de iteración, a pesar de ser menos flexible, puede
ser conveniente por su simplicidad en muchas ocasiones.
Resumen
Una vez vistas las estructuras de datos básicas en los otros módulos, en la pri-
mera parte de éste hemos visto qué estructuras son adecuadas en función de
los requisitos impuestos por el problema. El apartado propone una metodolo-
gía para, de la manera más sistemática posible, llevar a cabo las decisiones de
diseño adecuadas en el proceso de solución de un problema.
Ejercicios de autoevaluación
1. Nos podemos encontrar con muchas situaciones en las que nos interesaría modificar el
contenido de los elementos de una cola con prioridad. Como consecuencia de este cam-
bio, puede cambiar la prioridad del elemento modificado y, por lo tanto, el orden de los
elementos dentro de la cola. Una manera sencilla de proporcionar esta funcionalidad es,
en primer lugar, borrar el elemento, después de modificarlo y, una vez modificado, volver-
lo a introducir.
Aunque se podría proporcionar un algoritmo más inteligente que reordenase directamen-
te la posición, el algoritmo descrito tiene un coste logarítmico. Como consecuencia adi-
cional, se pierde la posición de la cola asociada al elemento, y en ciertas situaciones podría
ser muy interesante conservarla.
Así pues, con el objetivo básico de practicar la extensión de TAD e implementaciones se
os pide que extendáis el comportamiento de la clase ColaConPrioridad de la biblioteca de
clases de la asignatura para que permita modificar la prioridad de los elementos ya intro-
ducidos en la cola, conservando el objeto posición correspondiente y proporcionar, si es
posible, un algoritmo un poco más inteligente que el explicado más arriba. Se os pide que
defináis las operaciones adecuadas y que propocionéis el diseño detallado de la implemen-
tación. Opcionalmente, también podéis realizar la implementación.
2. Una cadena de gasolineras quiere poner en marcha una campaña de fidelización basada
en dos actuaciones diferentes. Por un lado, ofrece un descuento del 3% a todos aquellos
clientes que consuman más de 50 euros en un mes (el descuento se aplicará únicamente
sobre la cantidad que exceda de los 50 euros). Y, por otro, ofrece un descuento adicional
del 2% a los 1.000 clientes que más hayan gastado en gasolina durante el mes anterior.
Se os pide que:
a) Defináis un TAD que permita a la cadena de gasolineras calcular el descuento que debe
aplicar cada vez que un cliente se provea de combustible.
b) Diseñéis el TAD por bloques usando como bloques básicos los proporcionados por la
biblioteca de clases de la asignatura.
c) Describáis de manera detallada las operaciones del TAD, e incluyáis el coste asintótico.
3. Repetid el apartado b del ejercicio anterior utilizando la Java Collections Framework del
JDK. ¿Qué diferencias existen entre las dos bibliotecas que sean relevantes para la resolu-
ción del problema? ¿Cómo podrían afectar al diseño propuesto?
5. Añadid al TAD definido en el ejercicio anterior la posibilidad de consultar, para una asig-
natura y curso, todos los alumnos que han obtenido una nota dentro de un intervalo de
notas determinado. Actualizad el diseño para tener en cuenta las (o las) nuevas operaciones.
6. Repetid el diseño del TAD del ejercio 5, pero ahora, utilizando la Java Collections Fra-
mework. ¿Qué diferencias existen entre las dos bibliotecas que sean relevantes para la re-
solución del problema? ¿Cómo podrían afectar al diseño propuesto?
7. Una cadena de supermercados está poniendo en marcha una iniciativa para incentivar a
los trabajadores consistente en premiar tanto al trabajador del mes, como al estableci-
miento del mes. El trabajador del mes será aquel que haya facturado un volumen de ventas
mayor de entre todo el personal que trabaja en las cajas registradoras, y el establecimiento
del mes también será aquel que haya facturado un volumen de ventas mayor.
Se os pide:
a) Definir las operaciones de un TAD que permita a la cadena de supermercados gestionar
esta iniciativa.
b) Diseñar el TAD desde la perspectiva de los bloques usando la biblioteca de clases de la
asignatura.
c) Describir de manera detallada las operaciones del TAD, incluyendo el coste asintótico.
8. La cadena de supermercados del ejercicio anterior quiere modificar la iniciativa del traba-
jador del mes de manera que, en lugar de un único trabajador del mes para toda la cadena
de supermercados, haya un trabajador del mes para cada establecimiento. Modificad la de-
finición y el diseño del TAD definido en el apartado anterior de manera adecuada.
Podéis ver la colección Conjunto en el
apartado 4 del módulo “Tipos
9. Dotad de persistencia a la implementación (ConjuntoVectorImpl) de la colección Conjunto abstractos de datos” de esta asignatura.
desarrollada en el apartado 4 del módulo “Tipos abstractos de datos”. Para hacerlo, revisad
© FUOC • P06/75001/00583 • Módulo 9 46 Diseño de estructuras de datos
la documentación sobre la interfaz Serializable en el javadoc del JDK, aplicando los cono-
cimientos extraídos con el objetivo expresado en este ejercicio. ¿Es necesario modificar la
interfaz de la colección Conjunto?
11. Revisad en el Javadoc del JDK las jerarquías de interfaces y clases de la JCF. Comparadla
con la única jerarquía de la biblioteca de colecciones de la asignatura.
a) Intentad establecer una equivalencia entre los nodos de las jerarquías de la JCF y los
de la biblioteca de clases de la asignatura.
b) Evaluad la posibilidad de desdoblar la unica jerarquía de la biblioteca de colecciones
de la asignatura en dos jerarquías (una de interfaces y otra de implementaciones); estu-
diad las ventajas y los inconvenientes en función de los factores comentados en el apar-
tado 2.
12. Buscad en Internet información sobre la JDSL (opcionalmente, podéis descargar la biblio-
teca misma y echar un vistazo a su código). Comparad la abstracción posición, con la co-
rrespondiente de la biblioteca de la asignatura, y describid las similitudes y las diferencias.
Solucionario
1. Lo que queremos hacer es proporcionar una extensión de ColaConPrioridad, que podemos
denominar ColaConPrioridadActualizable, que permita actualizar el orden en el que están
almacenados los elementos dentro de la cola cuando el valor de alguno de estos cambia.
Aparte del comportamiento a introducir, hay un tema de diseño muy importante que se
debe trabajar: en una ColaConPrioridad, únicamente se introducen y extraen elementos;
pero no ofrece acceso a las posiciones en las que están guardados los elementos. Un acceso
a los elementos por valor nos obligaría a recorrer todos los elementos de la cola, y provo-
caría un coste lineal (sería mucho mejor el algoritmo sencillo descrito en el enunciado).
De alguna manera, necesitamos acceder rápidamente a la posición correspondiente a un
elemento a partir de éste. Lo podríamos hacer mediante la incorporación de un dicciona-
rio; pero esto tendría varios inconvenientes, principalmente añadir un nuevo bloque den-
tro de la cola y decidir dentro de la implementación cuál es la clave con la que accedemos
al diccionario. Esto nos llevaría a la necesidad de definir una extensión del TAD para cada
comportamiento particular del diccionario en cuestión, algo que preferimos evitar.
En casos como éste podemos crear una extensión del TAD que hace público el sistema po-
sicional inherente a la implementación del TAD. Es decir, cada vez que se introduce un
elemento a la cola, notificaremos al cliente de algún modo la posición asociada al elemen-
to. Posteriormente, cuando un elemento cambie de valor, el cliente será responsable de
decir a la cola cuál es la posición correspondiente al elemento que se debe reordenar. Este
diseño, al delegar esta responsabilidad en el cliente, es mucho más modular y flexible.
Con todo esto, podremos ampliar la interfaz de ColaConPrioridadActualizable con una nue-
va operación: void actualizarPosicion(Posicion<E> posicion).
Esta operación se ejecutará cuando el usuario de la cola con prioridad modifique el valor
del elemento correspondiente en posición; y deberá reordenar el elemento, manteniendo
la posición asociada y asumiendo que el resto de elementos siga ordenado.
A continuación, examinemos detalladamente la implementación que se debe extender
Las posibilidades
para decidir cómo podemos reaprovechar al máximo su comportamiento. La clase Cola- de reutilización
ConPrioridad tiene dos métodos protegidos denominados hundirElemento y subirElemento
que se encargan de ordenar un elemento. Podemos utilizar estos métodos para la imple- No siempre será tan sencillo
mentación de actualizarPosicion. reutilizar el máximo de código
Ahora veamos cómo podemos proporcionar una interfaz adecuada para hacer público el de la clase base. Eso dependerá
sistema posicional de una ColaConPrioridad. del diseño de la clase base, que
Una solución ampliamente utilizada dentro del mundo OO, y más en concreto en el mun- la mayoría de las veces ya está
do Java, es la aplicación del patrón observador. Este patrón consiste en: determinado y no podemos
cambiar.
a) Definir una interfaz Observador que contendrá métodos del estilo notificarXXX (en el
que XXX corresponde a un tipo de acontecimiento).
b) Todos los objetos que quieran ser observadores de estos acontecimientos deberán im-
plementar la interfaz Observador.
c) El objeto guardará un conjunto de observadores, a los que notificará el acontecimiento me-
diante la ejecución de los métodos definidos en el punto a para los observadores guardados.
• constructor()
– Crea el TAD inicialmente sin datos de clientes.
• double repostar(String nifCliente,double cantidadEuros)
– Actualiza la cantidad de euros gastada por el
cliente en el mes. Da de alta el cliente si es nece-
sario; y devuelve el descuento aplicado a la tran-
sacción en euros.
• void principioDeMes()
– Actualiza los mil clientes a los que se ha de apli-
car el descuento adicional del 2% a partir de este
momento.
© FUOC • P06/75001/00583 • Módulo 9 48 Diseño de estructuras de datos
Para el diseño, tendremos suficiente con un diccionario en el que la clave será el identifi-
cador del cliente (su NIF) y el elemento almacenado serán los datos del cliente: el NIF, la
cantidad de dinero gastado durante el mes actual, y un booleano que especifique si ha sido
uno de los 1.000 clientes que más ha gastado durante el mes anterior.
Implementaremos este diccionario con un AVL, ya que así no imponemos ningún límite al
número de clientes que debemos almacenar (el enunciado no nos da ninguna cota). Con
esto, tendremos acceso logarítmico a los clientes, y podremos realizar la operación repostar
en tiempo logarítmico. Esta operación interesa que sea lo más eficiente posible y que, aun-
que el enunciado no lo especifica, se supone que se realizará con una cierta frecuencia.
La operación principioDeMes se realizará una vez al mes y, por lo tanto, no es necesario optimi-
zarla al máximo. Se puede implementar haciendo un recorrido del diccionario de clientes,
quedándonos con los 1.000 que más han gastado. Para quedarnos con los 1.000 que más han
gastado de modo eficiente podemos usar una cola con prioridad que la propia operación prin-
cipioDeMes puede crear de modo interno, y destruirla una vez haya acabado el trabajo.
Figura 8
• constructor: O(1)
– Crea AVL vacío (O(1)).
• repostar: O(log numeroClientes)
– Busca el cliente en el AVL (O(log numeroClientes)).
– Si no lo encuentra, lo introduce con la cantidad especificada y el booleano en falso
(O(log numeroClientes).
– Si lo encuentra, actualiza la cantidad gastada por el cliente (O(1)).
– Calcula el descuento que se debe aplicar a partir de los datos asociados al cliente y la can-
tidad gastada, y lo devuelve (O(1)).
• principioDeMes: O(numeroClientes · log ncDescuento)
– Crea una cola con prioridad de 1.000 posiciones, en la que el elemento más prioritario
será el que haya gastado una cantidad menor (O(1)).
– Recorre el AVL de cliente (O(numeroClientes · log ncDescuento). Para cada cliente:
a. Si la cola no está llena, la introduce (O(log ncDescuento)).
b. Si ya está llena, mira el elemento más prioritario. Si éste ha gastado menos que el
cliente actual, lo borra e introduce al cliente actual (O(log ncDescuento)).
3. Existe una diferencia importante entre la biblioteca de clases de la asignatura y la JCF que
podría afectar al diseño del ejercicio 2. La implementación de tablas de dispersión que
ofrece la JCF permite hacer crecer dinámicamente la tabla según el número de elementos
que se guardan en ella.
Esto nos permitiría usar una tabla de dispersión en lugar de un AVL si lo deseamos y no
hay ninguna otra restricción impuesta que nos lo impida (por ejemplo, si fuese necesario
hacer un recorrido ordenado de los elementos del diccionario).
• Después de analizar las condiciones restrictivas del problema, concluimos que como el
recorrido que debemos hacer en principioDeMes no es necesario que sea ordenado por
clave, si utilizásemos la JCF podríamos elegir libremente entre hacer servir la implemen-
tación con árboles equilibrados (la clase java.util.TreeMap de la JCF, que utiliza arboles
rojo y negro en lugar de AVL), o bien la implementación con tabla de dispersión (la cla-
se java.util.HashMap).
© FUOC • P06/75001/00583 • Módulo 9 49 Diseño de estructuras de datos
4. Las operaciones del TAD que se piden podrían ser las siguientes:
Por lo que respecta al diseño, necesitamos un diccionario para los alumnos. Como su nú-
mero se prevé grande (unos 10.000), la operación de consulta debe ser lo más eficiente po-
sible. Podríamos elegir entre una implementación con AVL y una tabla de dispersión.
Como tenemos el número aproximado y no hacen falta recorridos ordenados, elegimos la
tabla de dispersión.
Para cada alumno habrá que guardar un diccionario con las asignaturas cursadas, y para
cada asignatura, una lista con las notas (curso académico más calificación). El diccionario
de asignaturas por alumno puede ser una lista (consulta en tiempo lineal), ya que no ten-
dremos un número de asignaturas grande por alumno (por lo tanto, no nos importará un
tiempo lineal si la magnitud es pequeña).
En la figura 9, tenéis un diagrama con el diseño de bloques correspondiente.
Figura 9
Glosario
hilo de ejecución m Secuencia de instrucciones que se puede ejecutar en paralelo con otros
hilos de ejecución.
en thread
precisión arbitraria f Precisión aplicable a tipos de datos numéricos. La precisión de los valo-
res de este tipo puede ser tan grande como sea necesario (a diferencia de los tipos numéricos es-
tandar, que tienen una precisión fija, condicionada por el tamaño de su representación en bits).
© FUOC • P06/75001/00583 • Módulo 9 50 Diseño de estructuras de datos
requisito no funcional m Requisito impuesto por las características del problema que hay
que resolver y no afecta a la funcionalidad que se ha de proporcionar.
sin. restricción impuesta por el problema
subtipaje m Redefinición de una subclase a partir de una clase ya existente, con el objetivo
de modificar parte del comportamiento de la clase existente reaprovechando el resto de im-
plementación.
Bibliografía
Bibliografía básica
Larman, C. (2003). UML y patrones. Introducción al análisis y diseño orientado a objetos (2.a ed.).
Prentice Hall.
Goodrich, M.; Tamassia, R. (2001). Data structures and algorithms in Java (2.a ed.). John
Wiley and Sons.
Weiss, M. A. (2003). Data structures & problem solving using Java (2.a ed.). Upper Saddle River:
Addison Wesley. Disponible en línea en: <http://www.cs.fiu.edu/~weiss>.
Anexo