Está en la página 1de 52

Diseño

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

1. Diseño de nuevos TAD usando una biblioteca


de colecciones ..................................................................................... 9
1.1. Identificación de la signatura y funcionalidad ............................... 12
1.2. Identificación de las restricciones impuestas por el problema ...... 14
1.3. Diseño de la estructura de datos...................................................... 16
1.3.1. Aproximación global al planteamiento de una solución ... 17
1.3.2. Elección de un TAD y de una implementación
para un bloque .................................................................... 18
1.4. Descripción de las operaciones ....................................................... 27
1.5. Implementación ............................................................................. 29

2. Diseño de bibliotecas de colecciones ............................................ 31


2.1. Propiedades deseables de una biblioteca de colecciones ............... 31
2.1.1. Desvinculación entre interfaz e implementación ............... 31
2.1.2. Versatilidad y potencia ........................................................ 31
2.1.3. Extensibilidad ...................................................................... 32
2.1.4. Eficiencia ............................................................................. 32
2.1.5. Fiabilidad ............................................................................. 32
2.1.6. Usabilidad ............................................................................ 33
2.1.7. Simplicidad y facilidad de aprendizaje ............................... 33
2.1.8. Homogeneidad .................................................................... 33
2.1.9. Complejidad de la jerarquía de colecciones
y de TAD adicionales ........................................................... 33
2.1.10. Mantenibilidad de la implementación ................................ 34
2.2. Otros aspectos de la implementación ............................................ 34
2.2.1. Persistencia .......................................................................... 34
2.2.2. Concurrencia ....................................................................... 35

3. Bibliotecas de colecciones existentes ............................................ 37


3.1. Java Collections Framework (JCF) .................................................. 37
3.2. Java Data Structures Library (JDSL) ................................................ 38
3.3. Library of Efficient Data Structures (LEDA) .................................... 39
3.4. Standard Template Library (STL) .................................................... 41
3.5. Booch components ......................................................................... 42

Resumen .................................................................................................... 44
© FUOC • P06/75001/00583 • Módulo 9 Diseño de estructuras de datos

Ejercicios de autoevaluación ............................................................... 45

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.

Una de las funciones habituales a la hora de diseñar aplicaciones consiste en


diseñar abstracciones que nos permitan construir estas aplicaciones de una
manera modular. Estas abstracciones pueden consistir en colecciones especia-
lizadas para el dominio de la aplicación que estamos desarrollando o, simple-
mente, ser nuevos TAD que encapsulen una parte del dominio.

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.

En la primera parte de este módulo, pondremos el énfasis en los elementos y las


condiciones que nos han de permitir elegir adecuadamente tanto los TAD como
sus implementaciones en el diseño de nuevos TAD. Todos estos elementos y con-
diciones ya han ido surgiendo a lo largo de los módulos. Sin embargo, el enfoque
de los otros módulos presentaba principalmente las colecciones y sus imple-
mentaciones, y veía cada una de las colecciones por separado. En este módulo las
veremos como un todo, y nos centraremos tanto en la selección de las funciona-
lidades adecuadas (TAD), como en la implementación más oportuna para cada
situación concreta.

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

desarrolladores experimentados, y debe presentar las diferentes abstracciones de


un modo homogéneo.

En la segunda parte del módulo, razonaremos sobre este conjunto de factores


que hay que tener en cuenta en el diseño de bibliotecas de colecciones.

Para terminar, daremos un paseo por algunas de las bibliotecas de colecciones


más usadas, y comentaremos y compararemos su diseño. En este apartado
también se incluyen bibliotecas para otros lenguajes de programación, aparte
del Java.
© FUOC • P06/75001/00583 • Módulo 9 7 Diseño de estructuras de datos

Objetivos

Los materiales didácticos de este módulo proporcionan los conocimientos


fundamentales para que alcancéis los siguientes objetivos:

1. Profundizar en la creación de aplicaciones y TAD nuevos a partir de módu-


los y clases ya existentes (proporcionados por bibliotecas); comprender la
necesidad de maximizar la reusabilidad frente a la creación de soluciones
ad hoc.

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

3. Conocer el conjunto de factores que pueden participar en el diseño de una


biblioteca de colecciones.

4. Entender cómo contribuye cada uno de estos factores a maximizar el ren-


dimiento que los usuarios obtienen de una biblioteca de colecciones.

5. Dada una biblioteca de colecciones, saber evaluar las ventajas y los incon-
venientes a partir del conjunto de factores estudiados.

6. Conocer algunas de las bibliotecas de colecciones más populares en el


mundo del desarrollo orientado a objetos, e identificar tanto las caracterís-
ticas comunes, como las singularidades que las identifican.
© FUOC • P06/75001/00583 • Módulo 9 9 Diseño de estructuras de datos

1. Diseño de nuevos TAD usando una biblioteca


de colecciones

En este apartado presentaremos una metodología que nos ayudará al diseño


de TAD nuevos que requieran el uso de colecciones. Una vez tengamos claras
las funcionalidades del nuevo TAD que queremos diseñar, la metodología
nos ayudará a elegir tanto las colecciones, como las implementaciones más
adecuadas con el fin de tenerlo todo a punto para realizar su implementa-
ción. A continuación enumeramos las fases: a
1) Es muy importante, antes de pensar en temas relacionados con colecciones,
representación o implementación de un TAD, tener del todo claro cuáles son las
funcionalidades que este nuevo TAD debe ofrecer. Por este motivo, la primera de
las fases consiste en identificar la signatura y funcionalidad de este nuevo TAD.
El contexto en el que identificamos la signatura y la funcionalidad puede ser va-
riado. Por ejemplo, os lo podéis encontrar en el enunciado de un examen o una
práctica de esta asignatura. En este caso, deberíais identificar la signatura y fun-
cionalidad del nuevo TAD a partir del enunciado. O bien, si estamos trabajando
como profesionales en el área del desarrollo del software, deberemos determinar
nosotros mismos la funcionalidad y la interfaz del nuevo TAD, o bien discutirlo
con el resto del equipo. Definir la funcionalidad de la cual es responsable una abs-
tracción o TAD no es cosa fácil y escapa de los objetivos de esta asignatura.

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

que a cada una de estas colecciones, y diagrama de bloques a la visión global.


En segundo lugar, haremos un mapeado de cada uno de los bloques en una
colección y una implementación concretas, teniendo siempre en cuenta las
restricciones que el problema impone.

4) Descripción de las operaciones. Para cada operación es necesario descri-


bir el algoritmo que la implementa, utilizando las diferentes colecciones ele-
gidas en la fase anterior (y las implementaciones elegidas también, si fuese
necesario). Esta descripción se hará en lenguaje natural y servirá para verificar
que el diseño es correcto antes de pasar a la implementación; es decir, para ve-
rificar que se implementan las funcionalidades especificadas en la primera fase
y que se respetan las restricciones identificadas en la segunda. La descripción
debe ser bastante detallada para calcular fácilmente el coste de la implemen-
tación de cada una de las operaciones. Las descripciones obtenidas en esta fase
también nos servirán en la última fase como guía para la implementación.

5) Implementación. No es necesario decir aquí gran cosa sobre esta fase. La


implementación consiste en definir la signatura del TAD y programar la im-
plementación –utilizando el lenguaje de programación elegido (o impuesto)–
a partir de las descripciones obtenidas en el paso anterior.

A continuación, entraremos en detalle en cada una de estas fases ayudándo-


nos de un ejemplo. En el texto intercalaremos explicaciones generales sobre
la metodología propuesta con la concreción de estas explicaciones para el
caso del ejemplo.

Enunciado del ejemplo

Se trata de diseñar la estructura de datos necesaria para hacer una parte de


Ejemplo
un servicio DNS (domain name server). Un DNS es un servicio que, entre
otras funciones, nos traduce una dirección de Internet, también denomi-
nada dominio (por ej.: <http://www.uoc.edu>) a su dirección IP equivalente
(así, por ejemplo, la dirección IP correspondiente a <http://www.uoc.edu>
es 213.73.40.217).

Para un DNS pequeño, los requisitos de eficiencia no son muy trascenden-


tes, pero para un DNS grande sí, y sobre todo si pensamos que tienen un
elevadísimo número de peticiones. La función básica del DNS es la de con-
sultar una dirección, pero también debe incorporar otras: dar de alta una
dirección nueva, modificar la dirección IP de un dominio, dar de baja un
dominio... Todas estas operaciones se han de realizar sin detener el servi-
cio, y aquí radica la importancia de un buen diseño. Es necesario buscar
una correcta estructura de datos que nos permita efectuar todas las opera-
ciones de la manera más eficiente.

Nos limitaremos a las direcciones de Internet formadas por tres campos


que, en orden de escritura, son: el servidor (host), la entidad y el dominio
de primer nivel (top level domain). Por ejemplo, la dirección <http://
© FUOC • P06/75001/00583 • Módulo 9 11 Diseño de estructuras de datos

www.uoc.edu> corresponde al servidor “www” de la entidad “UOC”, den-


tro del dominio de primer nivel “edu”.

Cada una de las tres partes de una dirección tiene unas propiedades bastan-
te diferentes:

• Servidor: lo más habitual es que las entidades sólo tengan un servidor


(el www); y aunque pueden tener tantos como quieran, el número de
servidores siempre será pequeño para todas las entidades. Denotaremos
este número por H.

• Entidad: el número de entidades no se puede acotar y cambia constan-


temente. Denotaremos este número por E.

• Dominio de primer nivel: aunque varíen de tanto en tanto, considera-


remos que nunca superarán los 2.000 (com, edu, net, es, fr, etc.). Deno-
taremos este número por T.

Debemos decidir qué estructura utilizaremos para acceder, en un primer


momento, a un dominio de primer nivel, después a una entidad y, final-
mente, a un servidor.

Objetivos

Os pedimos definir e implementar un TAD que ofrezca las operaciones que


se detallan en el siguiente epígrafe.

También necesitáis construir algunos tipos abstractos de datos auxiliares


para guardar información sobre las direcciones y los dominios, concreta-
mente las clases Dominio, DirecciónIP, topLevelDomain, Entidad y Host. Gra-
cias a su simplicidad, especificidad y fácil comprensión, estas clases no
necesitan interfaz (aunque las podéis definir si lo preferís). Los atributos de
la clase Dominio son: el dominio de primer nivel, la entidad y la máquina
o servidor. Las clases topLevelDomain, Entidad y Host, por ejemplo, se pue-
den definir con un solo atributo de tipo String.

Funcionalidades

El TAD DNS debe ofrecer las siguientes operaciones:

• Creación de la estructura vacía. Esta operación no es necesario que aparez-


ca en la interfaz, ya que puede corresponder al constructor de la implemen-
tación del TAD DNS que hacéis (y las interfaces no tienen constructores).

• altaDominio(Dominio d, DireccionIP ip). Da de alta un dominio; si ya exis-


tía, modifica la dirección IP.

• bajaDominio(Dominio d). Da de baja un dominio; si no existía, no hace


nada.
© FUOC • P06/75001/00583 • Módulo 9 12 Diseño de estructuras de datos

• consultar(Dominio d): DireccionIP. Dado un dominio d devuelve su direc-


ción IP equivalente. Si no existe, devuelve “null”. Cada vez que hace-
mos una consulta es necesario modificar la estructura de datos con el
fin de que, en sucesivas consultas, se minimice el coste para acceder a
los servidores consultados más recientemente. El coste de esta modifi-
cación no debe superar O(1).

• masVisitados(natural n): Dominio, numeroVisitas. Devuelve el n-ésimo


dominio más visitado, así como el número de visitas (número de veces
que se ha llamado consultar con este dominio). El valor de n debe estar
entre 1 y 10.

• entidades(topLevelDomain t). Devuelve un iterador sobre las diferentes


entidades del dominio de primer nivel t. El iterador debe estar imple-
mentado de tal manera que podamos tener tantos iteradores indepen-
dientes como queramos en paralelo para los dominios de primer nivel
dados de alta.

Eficiencia

Es necesario optimizar el coste de las operaciones consultar, altaDominio y


bajaDominio. Las operaciones de recorrido del iterador y masVisitados no
son críticas, pero en ningún caso pueden llegar a lineales.

1.1. Identificación de la signatura y funcionalidad

En primer lugar, es muy importante comprender bien cuál es el pro-


blema que debemos solucionar. Para ello, necesitaremos hacer una lec-
tura detallada. Es fundamental leer el enunciado y releerlo (sin ningún
tipo de prisa) tantas veces como sea necesario.

Como resultado de esta lectura detallada deberemos ser capaces de lo siguiente:

a) RES1A. Hacer abstracción del problema, y definirlo de una manera


clara y concisa.

Por lo que respecta a PRAC-T02, se trata de proporcionar de manera eficiente Ejemplo


(continuación)
las funcionalidades de búsqueda correspondientes a un DNS. En definitiva,
debemos implementar una especie de diccionario que traduzca direcciones
de Internet/dominios en direcciones IP. Fijémonos en que, con algún peque-
ño añadido o alguna modificación, lo que nos pide el enunciado corresponde
a una especie de diccionario en el que mapeamos dominios (o direcciones de
Internet) a direcciones IP.

b) RES1B. Para aclarar totalmente el dominio con el que trabajaremos, haremos


una enumeración de todos los tipos de elementos/objetos con los que debere-
© FUOC • P06/75001/00583 • Módulo 9 13 Diseño de estructuras de datos

mos trabajar. Haremos esto desde un punto de vista conceptual y no desde un


lenguaje de programación. Es decir, hablaremos de personas o números, en lugar
de cadenas de caracteres, por ejemplo. Para encontrarlos, únicamente será nece-
sario señalar aquellos objetos que intuimos que son importantes para la solución
del problema. Es posible (aunque poco probable) que a estas alturas nos equivo-
quemos con alguna entidad (bien por defecto o bien por exceso). Este punto
RES1B no es primordial ahora, únicamente nos sirve para hacernos una idea más
clara del dominio y de los elementos con los que deberemos trabajar.

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)

• Crear la estructura vacía. Es una operación presente en la mayoría de


los TAD: primero se crea el TAD y posteriormente vamos añadiendo ele-
mentos con otras operaciones del TAD.

• Añadir, eliminar y consultar relaciones entre un dominio y una di-


rección IP. Estas operaciones se corresponden con altaDominio, bajaDo-
minio y consultar, respectivamente. Fijaos en que en las tres operaciones
el enunciado nos dice cómo se ha de comportar la operación si el domi-
nio existe al darlo de alta, o bien no existe al darlo de baja o consultarlo.
Podría darse el caso de que el enunciado no nos dijera nada. Entonces
tendríamos libertad para actuar de una manera o de otra; por ejemplo,
podríamos lanzar una excepción propia cada vez que nos encontrára-
mos en una situación como ésta. O bien podríamos decidir hacer lo que
nos dice en este caso el enunciado, no hacer nada. En cualquier caso, es
importante documentar bien todas las decisiones de esta clase que to-
memos y para las cuales el enunciado nos deje libertad.

• Consultar el número de visitas de los dominios más visitados. El


enunciado nos especifica que únicamente consultaremos este dato en
los diez dominios más visitados.
© FUOC • P06/75001/00583 • Módulo 9 14 Diseño de estructuras de datos

• Recorrer las entidades de un dominio de primer nivel determinado


mediante un iterador.

Identifiquemos los siguientes puntos clave:

• PC1: son necesarias operaciones para añadir, eliminar y consultar rela-


ciones entre un dominio y una dirección IP.

• PC2: debemos poder consultar el número de visitas para los 10 domi-


nios más visitados.

• PC3: se deben poder recorrer las entidades correspondientes a un domi-


nio de primer nivel (se hará el recorrido mediante un iterador).

1.2. Identificación de las restricciones impuestas por el problema

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:

1) RES2A. Respecto a las características de los datos debemos tener en cuen-


ta los siguientes puntos:

a) Número de elementos. Número de elementos que deberemos guardar ¿Número de elementos


para los diferentes tipos de elementos que deberemos guardar: ¿se trata de un grande, mediano
o pequeño?
número muy grande?, ¿mediano? o ¿pequeño? Dependiendo de la respuesta,
Se podría dar el caso de que no
será más adecuado elegir un TAD u otro. Así, por ejemplo, si se trata de un nú- tuviésemos una respuesta a
mero realmente pequeño, un coste de búsqueda lineal podría ser aceptable (si esta pregunta de tipo
muy grande / medio / pequeño.
del enunciado o del contexto no se deduce lo contrario). En cambio, si se trata En este caso, deberíamos
“curarnos en salud”, como se
de un número muy grande, un coste lineal podría ser inaceptable y debería- dice coloquialmente, y supo-
ner el peor de los casos.
mos elegir una implementación que nos asegurara un tiempo de búsqueda o
bien logarítmico o bien constante.

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)

decisión de diseño. Si no hay ningún inconveniente en el resto del enunciado,


podríamos elegir un TAD que hiciese la búsqueda en tiempo lineal. En cam-
bio, respecto al número de entidades se dice que “el número de entidades no
se puede acotar...”. No nos dice ni que sea pequeño ni que sea grande. Por lo
tanto, no podemos suponer que será un número pequeño, como en el caso de
los servidores, y dar una solución ad hoc para este caso (con un coste de bús-
queda lineal). En el caso de darla, ¿qué sucedería si el número de entidades fue-
se muy grande? Probablemente, en este caso, las operaciones serían altamente
ineficientes y el DNS se colapsaría. La solución es, por lo tanto, hacer un dise-
ño que sirva tanto para pocas entidades, como para muchas.

b) Variabilidad en el número de elementos. Puede ser que a lo largo del tiempo


el número de elementos a guardar presente grandes variaciones, o bien que se
© FUOC • P06/75001/00583 • Módulo 9 15 Diseño de estructuras de datos

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.

En el ejemplo el enunciado dice que “el número de entidades [...] cambia


Ejemplo
constantemente”. Esto, evidentemente, afectará a las decisiones de diseño (continuación)

relacionadas, ya que elegir una representación con vector para las entida-
des implicará malgastar espacio.

Por otro lado, como ejemplo de poca variabilidad, tenemos el número de


dominios de primer nivel. El enunciado dice: “dominio de primer nivel:
aunque varíen de tanto en tanto, [...]”, del cual deducimos que tienen poca
variabilidad. Así, si elegimos una representación con vector, no malgasta-
remos espacio de modo significativo. Observad, sin embargo, que eso no
descarta tampoco una implementación que haga uso de la memoria diná-
mica si nos conviniese por otros motivos.

c) Cota superior del número de elementos a guardar. A veces conocemos


el mayor número de elementos que podemos tener que guardar en la estruc-
tura, pero a veces no lo conocemos.

En el enunciado del ejemplo se especifica: “dominio de primer nivel: [...]


Ejemplo
consideraremos que nunca superarán los 2.000”. Siempre que tengamos (continuación)

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.

emplear vectores extensibles.

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.

• PC5: el número de entidades se puede acotar y cambia constantemente.

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

operaciones dependerá, evidentemente, de las colecciones elegidas para repre-


sentar internamente los datos, así como de las implementaciones correspon-
dientes de cada una.

En el enunciado del ejemplo aparecen varias restricciones sobre la eficien- Ejemplo


(continuación)
cia. Algunas son muy generales, como “es necesario buscar una correcta es-
tructura de datos que nos permita hacer de la manera más eficiente posible
todas las operaciones”. Otras son más específicas, como “las operaciones de
recorrido y masVisitados, no son críticas, pero en ningún caso pueden lle-
gar a lineales”. Fijaos en que esta última restricción nos especifica incluso
un umbral de coste inadmisible.

Identifiquemos los siguientes puntos clave referentes a la eficiencia:

• 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).

• PC9: es necesario optimizar el coste de las operaciones consultar, al-


taDominio y bajaDominio.

• PC10: las operaciones de recorrido del iterador y masVisitados no son


críticas, pero no pueden llegar a un coste lineal.

Observad que algunas de las restricciones se pueden encabalgar (y, de he-


cho, lo hacen). En concreto, PC7 es bastante redundante, teniendo en
cuenta PC9 y PC10 (y también PC8). En cualquier caso, no debe suceder
nunca que los puntos clave sean incoherentes entre sí. De lo contrario,
querría decir que en algún momento nos hemos equivocado o que el pro-
blema está mal planteado.

1.3. Diseño de la estructura de datos

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

1.3.1. Aproximación global al planteamiento de una solución

Hay dos tareas básicas que deberemos hacer en esta tercera fase:

1) Decidir qué estructura tendrá nuestra solución como bloques, en los


que cada bloque se corresponderá con un TAD de la biblioteca de clases (o
bien creado por nosotros). Este punto también incluye concretar cómo se
relacionan los bloques entre sí.

En el caso de nuestro ejemplo, podríamos proponer una estructura de un


Ejemplo
único bloque correspondiente al TAD Diccionario que permitiera obtener (continuación)

para un Dominio (dirección Internet) su correspondiente DireccionIP (en el si-


guiente paso ya veríamos cómo concretamos/implementamos este bloque).

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.

Podemos comprobar la diferencia entre ambas aproximaciones en la figura 1.

Figura 1

La figura 1a corresponde a la primera solución. En un único bloque


guardaríamos todos los dominios, y con la dirección IP correspondien-
te. La figura 1b representa la segunda posibilidad. Hay tres bloques im-
bricados, uno en los TLD, otro bloque por cada TLD con las entidades
correspondientes en cada TLD y, para acabar, tenemos un último blo-
que para cada entidad con las máquinas/servidores correspondientes.

2) Dado un conjunto de elementos que necesitamos guardar, elegir el con-


tenedor TAD/implementación más adecuado para guardar estos elementos
© FUOC • P06/75001/00583 • Módulo 9 18 Diseño de estructuras de datos

teniendo en cuenta todos los puntos clave identificados en la fase anterior.


Es decir, dado un bloque, concretar su implementación.

No disponemos de ningún método infalible para averiguar la estructura de


bloques; y, como para otros procesos del desarrollo del software faltos de “mé-
todo infalible”, la experiencia es la mejor guía para acertar una buena solu-
ción. Por todo ello, y dado que no existe ninguna garantía de que acertemos
la estructura de bloques a la primera, os proponemos que sigáis un proceso ite-
rativo que integre los dos puntos anteriores, de la siguiente manera:

1) A partir de los puntos clave de la fase anterior, intuid una estructura de


bloques.

2) A partir de la información de los puntos clave, concretad cada uno de los


bloques de la estructura (tomad decisiones de diseño), teniendo cuidado de no
invalidar ningún punto clave o decisión de diseño que ya hayamos tratado.

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.

Entendemos por decisión de diseño cualquier decisión que afecte a la


estructura de datos resultante, en todos los niveles: definición de los
bloques que forman la estructura, relación entre ellos, concreción de un
bloque en una implementación determinada, definición parcial o total
de los datos que constituyen un tipo, y modificación de los datos de la
estructura para alcanzar una propiedad determinada (por ejemplo,
mantener una estructura ordenada).

1.3.2. Elección de un TAD y de una implementación


para un bloque

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

a tomar decisiones de diseño. Clasificaremos estas ideas por el tipo de punto


clave que tratan, según hemos establecido en las dos primeras fases: funciona-
lidad, datos y eficiencia. Muchas veces, sin embargo, las ideas comentadas es-
tarán relacionadas con más de un punto.

Funcionalidad

Evidentemente las funcionalidades nos marcarán la estructura de bloques, y


deben ser la base a partir de la cual nuestra intuición deberá visualizar la es-
tructura de bloques inicial. Aparte de definir la estructura de bloques, las fun-
cionalidades también influirán sobre las decisiones de diseño. Presentamos, a
continuación, algunas formas concretas de hacerlo.

1) Solicitud de cálculos complejos

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.

Encontrar el elemento máximo de un conjunto es un ejemplo típico de esto.


Calcularlo cada vez puede llegar a tener un coste lineal (según la implementa-
ción del conjunto). En este caso, mantenerlo precalculado es mucho más efi-
ciente. Ahora bien, deberemos volver a hacer el cálculo de este elemento
máximo cada vez que añadimos un elemento nuevo al conjunto. a
2) Recorridos

En muchas ocasiones, también nos pedirán realizar el recorrido de un gru-


po de objetos. Querremos hacer el recorrido del modo más eficiente posi-
ble. Por ello, deberemos asegurar que podemos acceder rápidamente de un
objeto al siguiente objeto.

Muchas veces, el grupo de objetos corresponderá directamente al recorrido de


un bloque. Si es así, simplemente debemos asegurarnos de que el TAD con el
que representamos el bloque sea un contenedor recorrible.

Si el grupo de objetos que se quiere recorrer es un subconjunto de los objetos al-


macenados en un bloque, podríamos modificar este bloque para acceder de un
elemento al siguiente de una manera eficiente (por ejemplo, encandenando los
objetos). En el caso de que esta modificación fuese complicada, podríamos consi-
derar rehacer la estructura de bloques para ver si podemos hacer fácilmente que
el grupo de objetos por recorrer corresponda directamente a un bloque.
© FUOC • P06/75001/00583 • Módulo 9 20 Diseño de estructuras de datos

Fijaos en que este caso es un caso particular de cálculo complejo, y la solución


propuesta también es tener los datos (en este caso, el recorrido) precalculados
o implícitos en la estructura de algún modo.

3) Recorridos ordenados

Si la funcionalidad pedida es un recorrido ordenado, normalmente no es fac-


tible (por la eficiencia) ordenar los datos cada vez que nos piden hacer el reco-
rrido. Por lo tanto, la solución vuelve a ser tener precalculado este recorrido
ordenado. El modo habitual de hacerlo es con un TAD que sea un contenedor
recorrible ordenado.

Datos

1) Cota del número de elementos

Si disponemos de alguna cota para el número de elementos, podemos elegir una


Vector extensible
representación encadenada o con vector, según nos convenga para otros puntos
Las representaciones con vector
clave. En cambio, si no lo conocemos, deberemos elegir una implementación que proporcionadas en la biblioteca
de TAD de la asignatura no im-
emplee una representación encadenada o bien un vector extensible.
plementan un vector extensi-
ble, pero sería fácil adaptarlas
(podéis ver el ejemplo del apar-
2) Variabilidad en el número de datos tado 6 del módulo “Contene-
dores secuenciales” de esta
asignatura).
Si la variabilidad en el número de objetos a lo largo del tiempo en el que la
aplicación se ejecutará es alta, preferiremos una representación encadenada.
De este modo, utilizaremos en cada momento el espacio que ocupamos, y de-
jaremos que otras aplicaciones utilicen lo que no necesitamos. En cambio, si
la variabilidad es baja, podremos elegir sin problemas entre una implementa-
ción con memoria estática y una con memoria dinámica.

Eficiencia

La eficiencia es uno de los puntos más importantes a la hora de realizar un di-


seño. Muchas veces, la información sobre la eficiencia nos llega de manera ex-
plícita. Otras veces, no es así. En cualquier caso, siempre deberemos intentar
proporcionar el diseño más eficiente posible que satisfaga los puntos clave ob-
tenidos en las dos primeras fases.

En ocasiones, al intentar que unas operaciones sean más eficientes, penali-


zaremos otras que pasarán a ser más ineficientes. Un ejemplo claro de esto
es el ya expuesto de la información precalculada: si la calculamos en el mo-
mento en el que se pide, estaremos penalizando las operaciones que de-
vuelvan esta información; mientras que si tenemos esta información
precalculada, penalizaremos las operaciones que modifiquen la estructura
de datos (calculamos cuando modificamos alguna cosa).

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

Una de las operaciones más comunes es consultar datos asociados a un objeto


a partir de un identificador. Normalmente, querremos que este tipo de opera-
ciones sean lo más eficientes posible (a menos que el enunciado nos diga al-
guna cosa en contra). Una consulta de este tipo se traduce normalmente en
realizar una búsqueda del elemento por su clave.

Si el número de elementos entre los cuales queremos hacer la búsqueda es bas-


tante pequeño, en principio (a menos que en el enunciado se diga lo contra-
rio) no nos importará que el coste de la búsqueda sea lineal. En este caso
podríamos elegir una estructura con una organización secuencial.

En cambio, si el número de elementos no es pequeño, en general será necesa-


rio que el tiempo de búsqueda sea siempre mejor que lineal; es decir, logarít-
mico o constante. En este caso preferiremos un TAD implementado mediante
árboles de búsqueda (AVL), que garanticen un acceso logarítmico, o bien ta-
blas de dispersión, que garanticen un acceso constante.

2) Solicitud sobre una operación concreta

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.

Veamos cómo aplicamos todo esto al ejemplo. Relacionaremos cada deci-


Ejemplo
(continuación)
sión de diseño con los puntos clave que la motivan.

Primera aproximación

En primer lugar, vayamos al paso 1 del procedimiento. Es necesario intuir


una estructura en bloques con la que pensemos que podríamos dar solu-
Podéis ver el paso 1 del
procedimiento en el subapartado
ción al problema. Teniendo en cuenta que el TAD que diseñamos es, en 1.3.1 de este módulo didáctico.

realidad, un diccionario de dominios y direcciones IP, podríamos plantear


una estructura de un bloque único correspondiente al TAD Diccionario (po-
déis ver la figura 1a):

• DD1-ESTRUCTURA: un bloque único correspondiente a un dicciona-


rio de dominios y direcciones IP.

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

de dominios, es decir, elegiremos o bien un AVL o bien una tabla de dis-


persión. Conocemos el número de dominios de primer nivel aproximado
(PC6), pero no el de entidades por dominio de primer nivel (PC5). Por lo
tanto, no está acotado el número total de dominios. Esto nos lleva a des-
cartar la tabla de dispersión (la biblioteca de la asignatura no proporciona
una implementación con vector extensible). Así, pues:

• DD2: elegimos como implementación del único bloque de nuestra es-


tructura un AVL (PC1, PC5, PC6).

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:

• DD3: guardamos asociado a cada dominio el número de visitas que ha


recibido (PC2).

En la figura 2, podemos apreciar una representación gráfica de la estructura


de datos correspondiente a DD1, DD2 y DD3.

Figura 2. Representación de un AVL

Los nodos guardan información correspondiente a: (1) el dominio, (2) la dirección IP


y (3) el número de visitas que ha recibido el dominio. Los nodos del AVL estarían orde-
nados lexicográficamente según el dominio.

Ahora bien, para hacerlo de manera eficiente, deberíamos realizar un reco-


rrido por todos los dominios y quedarnos con los 10 dominios más visita-
dos. La solución pasaría por precalcular cuáles son los 10 dominios más
visitados. Deberíamos añadir otro bloque (fijaos en que estamos haciendo
lo que nos dice el paso 4 del proceso). En el nuevo bloque guardaríamos 10
dominios, que podrían ir variando a lo largo del tiempo: cuando un domi-
nio se consultara, sería susceptible de formar parte de los 10 dominios más
visitados.

Añadimos un bloque donde guardaremos los 10 dominios más visitados


(PC2). Por el hecho de que son muy pocos, podemos elegir una estruc-
tura secuencial. Y dado que siempre guardamos 10, podemos concretar
© FUOC • P06/75001/00583 • Módulo 9 23 Diseño de estructuras de datos

estructura en un vector. Podemos mantener el vector ordenado de


modo que cuando visitemos un dominio, sabremos en tiempo O(1) si
debe entrar a formar parte de los más vistitados (o bien ya pertenece) o
no. Por lo tanto:

• DD4: guardamos los 10 dominios más visitados en un vector ordenado


(PC2).

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.

PC3 nos dice que las entidades correspondientes a un dominio de primer


nivel se deben poder recorrer. Evidentemente, lo querremos hacer de ma-
nera eficiente. El recorrido se efectúa mediante un iterador, que se debe im-
plementar como un objeto aparte, con un estado propio. Según PC10, las
operaciones del iterador, a pesar de no ser críticas, no pueden llegar a un
coste lineal.

Si quisiéramos hacer el recorrido sin añadir nada más a la estructura, debe-


ríamos recorrer todos los dominios y seleccionar los que fuesen del domi-
nio de primer nivel correspondiente (y descartar los otros). Pero esto no
cumple PC10: pasar de una entidad a la siguiente del mismo dominio de
primer nivel requiere un coste lineal respecto al número total de dominios.
Por lo tanto, debemos buscar otra solución: tener el recorrido precalculado,
o bien implícito en la misma estructura.

Si queremos mantener la misma estructura de bloques, debemos encadenar


los dominios correspondientes al mismo dominio de primer nivel. Por lo
tanto, proponemos tener listas en el interior de nuestro AVL. Dado que po-
demos dar de baja dominios, debemos poder eliminar elementos de estas
listas. Por lo tanto, los encadenamientos deben ser dobles (al siguiente y al
anterior) tal y como se ve en la figura 4.
© FUOC • P06/75001/00583 • Módulo 9 24 Diseño de estructuras de datos

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

Aparte, necesitaríamos otro bloque en el que guardaríamos el primer domi-


nio correspondiente a cada dominio de primer nivel (que se correspondería
al principio de cada una de las listas). Este otro bloque podría ser una tabla
de dispersión, ya que guardaremos tantos elementos como dominios de pri-
mer nivel tengamos, y que sabemos por PC6 que nunca pasarán de 2.000. En
defintiva:

• DD5: añadimos un nuevo bloque para los primeros dominios de cada


dominio de primer nivel. Este bloque estará implementado como una
tabla de dispersión y servirá para acceder a la lista de dominios de un
dominio de primer nivel (PC3).

• DD6: los dominios estarán encadenados con el siguiente y el anterior


dominio del mismo dominio de primer nivel (PC3).

Figura 4

Figura 4

Para cubrir DD5 yDD6, debe-


mos introducir una lista de do-
minios para cada TLD. Para
acceder rápidamente al primer
elemento de una lista hemos
introducido un nuevo bloque:
una tabla de dispersión para
los TLD. Ahora, los nodos del
AVL forman parte también de
estas listas “independientes”
del AVL. Cada nodo tendrá las
referencias a los objetos (o
apuntadores) propios para in-
tegrarlo al AVL y, además, las
referencias/apuntadores pro-
pios de la lista de dominios que
tendrá asociada cada TLD.

Cabe señalar que el enunciado no especifica nunca el orden en el que debe-


mos recorrer las entidades. Por lo tanto, podemos elegir nosotros mismos el
orden que queramos. La decisión más eficaz en este caso es elegir aquél con
el que tanto los algoritmos, como las estructuras se simplifiquen al máximo.

En definitiva, resulta bastante complejo, lo que nos hará ir con precaución


y buscar otra estructura de bloques de diseño más sencillo antes de conti-
nuar por esta vía.

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.

Independientemente de esta estructura con tres bloques, mantendremos el


bloque correspondiente a los 10 dominios más visitados, que nos seguirá
dando servicio.

Comencemos de nuevo con las decisiones de diseño:

• DD1-ESTRUCTURA: tenemos 4 bloques, uno para los dominios de pri-


mer nivel, otro para las entidades de primer nivel, otro para los servido-
res de cada entidad y otro para guardar los 10 dominios más visitados
(PC1, PC2, PC3, PC10).

En la figura 5 vemos una representación de bloques de la estructura corres-


pondiente a DD1. Se corresponde con la estructura de la figura 1b con el
añadido de un cuarto bloque para guardar los 10 dominios más visitados.

Figura 5

Mantenemos las dos decisiones de diseño correspondientes a los dominios


más visitados:

• DD2: guardamos asociado a cada dominio el número de visitas que ha


recibido (PC2).

• DD3: guardamos los 10 dominios más visitados en un vector ordenado


(PC2).

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

elementos. Evidentemente, por PC7 y PC9, necesitamos un acceso/alta/baja


rápidos a los elementos de la estructura. Así pues:

• DD4: guardamos los dominios de primer nivel en una tabla de disper-


sión (PC6, PC7, PC9).

El bloque correspondiente a las entidades de cada dominio de primer nivel


se concreta con un árbol de búsqueda AVL, ya que se supone que puede ha-
ber muchas entidades por dominio de primer nivel (PC5), su número cam-
bia constantemente (PC5) y querremos acceder a ellas de manera eficiente.
Por lo tanto:

• DD5: guardamos las entidades de cada dominio de primer nivel en un


AVL (PC5, PC7, PC9).

El bloque correspondiente a los servidores se puede concretar como una lista


en memoria dinámica, ya que sabemos que el número de servidores por enti-
dad es pequeño, habitualmente 1 (PC4); y no nos importará que el tiempo de
búsqueda sea lineal (respecto al número de servidores por entidad). Por otro
lado, no tenemos ninguna cota concreta de servidores por entidad, lo que nos
hace descartar el uso de representación con vector. Por lo tanto:

• DD6: guardamos los servidores de cada entidad en una lista simplemen-


te enlazada implementada usando representación encadenada (PC4).

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

En la figura 6 observamos los cuatro bloques concretados. Utilizamos una


tabla de dispersión para guardar los TLD, guardamos en un AVL las entida-
des de un TLD y representamos los servidores de cada entidad mediante
una lista. Por otra parte, necesitamos un vector ordenado para almacenar
los 10 nodos más visitados.
© FUOC • P06/75001/00583 • Módulo 9 27 Diseño de estructuras de datos

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.

Podemos colocar los servidores a los que hemos accedido últimamente al


principio de la lista para priorizarlos. Podemos hacer esta operación sin
problemas en O(1). Esto da lugar a la última decisión de diseño:

DD7: cada vez que accedamos a un servidor, lo colocaremos al principio de


la lista de servidores de la entidad (PC8).

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.

De ahora en adelante, siempre haremos referencia a las decisiones de dise-


ño (DD) de la segunda aproximación.

1.4. Descripción de las operaciones

Una vez ya tenemos afianzado el diseño de la estructura de datos, es muy im-


portante detallar las operaciones del TAD. Explicaremos con lenguaje natural
(catalán, castellano, etc.) cómo hacer cada una de las operaciones que nos pide
el enunciado, mediante una enumeración de frases sencillas y claras.

Esto nos servirá para dos cosas muy importantes:

1) Realizar la implementación.

2) Calcular con precisión el coste de les operaciones y comprobar rigurosa-


mente que se consiguen todas las exigencias sobre este coste.

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:

a) T hará referencia al número de TLD.


b) E hará referencia al número de entidades de un TLD.
c) H hará referencia al número de servidores de una entidad.
© FUOC • P06/75001/00583 • Módulo 9 28 Diseño de estructuras de datos

• crea/operación constructora. Inicializa la estructura de datos. Básica-


mente:
– inicializa una tabla de dispersión de 2.001 elementos: O(T)
– inicializa el vector de los topTen: O(1)
Coste total: O(T) + O(1) = O(T)

• altaDominio(Dominio d, IP ip). Da de alta el dominio d y lo relaciona


con la dirección IP ip:
– Descomponemos el dominio en cada una de sus partes, el TLD, la enti-
dad y el servidor: O(1).
– Localización de TLD: O(1).
– Si existe, obtenemos un árbol AVL en el que tenemos todas las entida-
des: O(1).
– Si no existe, es necesario introducirlo en la tabla de dispersión: (O(1));
y crear un nuevo árbol AVL: O(1).
– Localización de entidad: O(log E).
– Si la entidad no existe, es necesario inicializarla creando un ContHosts
nuevo: O(1).
– Localización de servidor: O(H).
– Si ya existe, modificamos la dirección IP asociada: O(1).
– Si no existe, añadimos el servidor y la dirección IP a la lista O(1).
Coste total: O(logE + H))

• bajaDominio(Dominio d). Da de baja el dominio d:


– Descomponemos el dominio en cada una de sus partes, el TLD, la enti-
dad (E) y el servidor u host (H): O(1).
– Localización de TLD: O(1).
– Localización de entidad: O(log E).
– Buscamos y eliminamos el servidor: O(H).
Coste total: O(logE + H))

• consultar(Dominio d): IP. Consulta la dirección IP del dominio d:


– Descomponemos el dominio en cada una de sus partes, el TLD, la enti-
dad (E) y el servidor o host (H): O(1).
– Localización de TLD: O(1).
– Localización de entidad: O(log E).
– Localizació del servidor: O(H).
– Ponemos el servidor como primer elemento de la lista: O(1).
– Incrementamos el número de visitas: O(1).
– Actualizamos, si es necesario, la lista de los topTen: O(1).
Coste total: O(logE + H)

• masVisitados(int n): Dominio. Consulta el n-ésimo dominio más visi-


tado:
– Accedemos a la posición n del vector de los topTen, y devolvemos el do-
minio: O(1).
– Si la posición de topTen todavía no está inicializada, devuelve “null”: O(1).
Coste total: O(1)
© FUOC • P06/75001/00583 • Módulo 9 29 Diseño de estructuras de datos

• entidades (TLD t). Devuelve un iterador de todas las entidades de un


TLD:
– Localización de TLD: O(1).
– Creación del iterador sobre el AVL de entidades del TLD: O(1).
Coste total: O(1)

Respecto a las operaciones del iterador de entidades, hacemos un recorrido


sobre el árbol AVL de entidades; las dos operaciones haySiguiente y siguiente
tienen coste logarítmico sobre el número de elementos del AVL (O(log E)).

1.5. Implementación

En esta fase debemos trasladar el resultado de las fases anteriores a un lenguaje


de programación (en nuestro caso, Java). Al hacerlo, es muy importante maxi-
mizar la mantenibilidad. Para ello, es necesario que el código resultante apli-
que de modo adecuado todo lo que habéis aprendido sobre programación
estructurada y programación orientada a objetos. Un buen conocimiento de
la biblioteca de TAD usada también puede ayudar mucho, maximizar la reuti-
lización y, por lo tanto, minimizar la cantidad de código nuevo.

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.

Para reutilizar al máximo las colecciones de la biblioteca, normalmente utiliza-


remos como herramienta principal la delegación. Así, por ejemplo, DNSImpl
delega buena parte del trabajo en diferentes implementaciones de Diccionario.

La delegación, sin embargo, no es la única forma de reutilización. Hay situa-


ciones en las que nos ahorraría mucho trabajo disponer de una colección de
la biblioteca ligeramente modificada para unos intereses particulares. En esta
© FUOC • P06/75001/00583 • Módulo 9 30 Diseño de estructuras de datos

situación, la definición de una subclase de una colección de la biblioteca pue-


de ser la herramienta adecuada. Al definir la subclase podemos redefinir única-
mente aquel comportamiento que difiera del comportamiento de la clase padre.

Fijaos, por ejemplo, en la clase DiccionarioHosts: se trata de una subclase de


DiccionarioListaImpl en la que se ha redefinido el método consultar, de modo
que, tal y como pide el enunciado, cada vez que se consulta un servidor se
pone al principio de la lista de servidores de la entidad correspondiente. Esto
se realiza con una única búsqueda.

Esta técnica del subtipaje requiere conocer a fondo la implementación que se


La visibilidad requerida
extiende y utilizar los miembros protegidos de la clase padre. Si revisáis el có- para el subtipaje
digo de DiccionarioHosts, veréis cómo se hace uso de RecorridoConAnterior y dic- A menudo los miembros prote-
cionario, dos miembros protegidos de DiccionarioListaImpl. gidos tienen la visibilidad de
protegidos con el objetivo de
permitir la definición de sub-
clases que redefinen el com-
Otra cosa remarcable es que en ocasiones debemos tener en cuenta la implemen- portamiento de la clase padre.
tación de un contenedor en la definición de la clase correspondiente al elemento
que almacenamos. En el ejemplo, el diccionario de entidades se implementa me-
diante un árbol AVL (DiccionarioAVLImpl). Esta implementación de Diccionario
requiere que los elementos sean ordenables (o bien mediante un comparador, o
bien porque la clase correspondiente al elemento implementa la interfaz Compa-
rable). Así pues, o bien deberemos definir un comparador de entidades, o bien de-
beremos hacer que Entidad implemente Comparable. En la codificación que
encontraréis como recurso electrónico, se ha elegido esta segunda vía. a
Para acabar, observad que no es necesario proporcionar una implementación de
Iterador especial para recorrer las entidades de un dominio de primer nivel. Dado
que para cada dominio de primer nivel disponemos de un diccionario de enti-
dades en el que la clave es la entidad y el valor, el diccionario de servidores co-
rrespondientes, el método claves del diccionario de entidades proporciona
directamente un iterador sobre las claves.
© FUOC • P06/75001/00583 • Módulo 9 31 Diseño de estructuras de datos

2. Diseño de bibliotecas de colecciones

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. Propiedades deseables de una biblioteca de colecciones

En el diseño de bibliotecas de colecciones (y de hecho en cualquier biblioteca


de clases), es necesario tener en cuenta dos aspectos diferenciados: por un la-
do, el diseño de la interfaz con la que el usuario de la biblioteca deberá tratar
y, por otro, el de la implementación.

2.1.1. Desvinculación entre interfaz e implementación

Idealmente, la interfaz deberá estar desvinculada de la implementación, de


modo que los usuarios de la biblioteca sólo hagan referencia a ésta en el mo-
mento de decidir la implementación concreta de un TAD. De esta manera, los
algoritmos que usan la biblioteca son independientes de las implementacio-
nes que se utilicen, y sería fácil cambiar unas por otras si fuese necesario. No
todos los lenguajes de programación posibilitan esta independencia. Java sí
que lo hace posible mediante el uso de interfaces.

Incluso, en el caso de que el lenguaje de programación posibilite esta indepen-


dencia, a menudo nos encontraremos con bibliotecas de un modelo híbrido.
Por un lado, existe un conjunto de interfaces que permiten la desvinculación
de la implementación. Pero, por otro, nos encontramos con ciertas funciona-
lidades que sólo son accesibles mediante alguna implementación concreta.
Por lo tanto, si necesitamos utilizar estas funcionalidades, necesitaremos hacer
referencia directa a esta implementación.

2.1.2. Versatilidad y potencia

Dos de las características más importantes en una biblioteca de colecciones


son su versatilidad y su potencia. Las colecciones definidas en la biblioteca se de-
ben poder utilizar en una gran variedad de situaciones.
© FUOC • P06/75001/00583 • Módulo 9 32 Diseño de estructuras de datos

2.1.3. Extensibilidad

Es probable (por no decir seguro) que algunos usuarios de una biblioteca de


colecciones se encuentren en alguna situación en la que no puedan aplicar
una colección tal y como está definida en la biblioteca, pero sí con una va-
riación o ampliación mínima en sus funcionalidades. Para casos como éstos,
es importante que el diseño, tanto de la interfaz de la biblioteca como de su
implementación, permita extender el comportamiento de sus operaciones.
Si esto no fuese posible o fuese demasiado complejo, el usuario de la biblio-
teca debería definir una colección totalmente nueva, aparte de la biblioteca,
y volver a implementar incluso aquella parte del comportamiento que po-
dría haber aprovechado.

2.1.4. Eficiencia

Las implementaciones que proporcione una biblioteca de colecciones deberán


ser eficientes y estar optimizadas. Una de las ventajas de utilizar una biblioteca
de colecciones es que pone al alcance unas implementaciones desarrolladas
por expertos en estructuras de datos que aplicarán sus conocimientos para ob-
tener unas implementaciones realmente eficientes. El esfuerzo necesario para
que un desarrollador de aplicaciones novel consiga una implementación de
una eficiencia similar es demasiado elevada en la mayoría de los casos.

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

Una biblioteca de colecciones debe ser cómoda de utilizar. No es conveniente


que el uso de una biblioteca complique demasiado los algoritmos porque tal
vez se estaría pagando un precio demasiado elevado, pero en ocasiones se hace
necesario añadirle un poco de complejidad adicional cuando, por ejemplo, es
preciso que el usuario pueda efectuar la gestión de excepciones lanzadas desde
la biblioteca.

2.1.7. Simplicidad y facilidad de aprendizaje

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

En relación con la simplicidad y, por supuesto, también con la usabilidad, es im-


portante que la interfaz que ofrezcan las diferentes colecciones sea homogénea.
Las diferentes colecciones definidas en la biblioteca dispondrán de algunas ope-
raciones comunes y de otras que no lo serán, pero en todo caso es deseable de-
finir un patrón de comunicación homogeneo para todas. Así, por ejemplo, sería
preferible que todas las operaciones de borrar elementos de una colección tuvie-
sen comportamientos parecidos en el tratamiento de casos excepcionales, como
cuando el elemento no se encuentra en la colección. En esta situación, se podría
o bien lanzar una excepción, o bien devolver una valor especial, etc. Pero sería
poco útil que para una colección se lanzase una excepción y para otra otra se
devolviese un valor especial. A los usuarios potenciales de la biblioteca les resul-
taría más difícil asimilar este comportamiento heterogéneo.

2.1.9. Complejidad de la jerarquía de colecciones


y de TAD adicionales

Otro elemento relacionado con la simplicidad de la biblioteca en un lenguaje


orientado a objetos es la complejidad de la jerarquía de colecciones y TAD adi-
cionales. Algunas bibliotecas presentan una jerarquía muy plana, con única-
mente dos niveles: una clase padre que representa un contenedor o colección
genérica, y todas las colecciones concretas en el segundo nivel. Otras bibliote-
cas presentan jerarquías profundas, en las que cualquier propiedad común a
varias colecciones “toma la forma” de una colección padre.
© FUOC • P06/75001/00583 • Módulo 9 34 Diseño de estructuras de datos

La primera aproximación resulta fácil de usar y entender (es evidente que no


complica las cosas por lo que respecta a este aspecto), pero no saca provecho
de las propiedades de la orientación a objetos. La segunda aproximación sí que
utiliza la orientación a objetos para representar mediante una jerarquía las di-
ferentes características de las colecciones de la biblioteca, pero, cada nivel de
la jerarquía, y cada uno de los nodos de ésta, añaden complejidad a la biblio-
teca. Ambos extremos presentan problemas. Otras opciones intermedias que
representen sólo aquellos elementos de la jerarquía realmente útiles para los
usuarios de la biblioteca pueden proporcionar más beneficios.

2.1.10. Mantenibilidad de la implementación

Como en cualquier desarrollo, es realmente importante para la mantenibili-


dad de la biblioteca que la implementación se haya desarrollado siguiendo el
paradigma de la orientación a objetos, maximizando también internamente
la reutilización mediante la aplicación adecuada de técnicas como la abstrac-
ción, la herencia y el polimorfismo.

Como consecuencia de la aplicación de las técnicas provinientes de la orien- Jerarquía de


tación a objetos, la biblioteca tendrá una jerarquía de implementaciones. Esta implementaciones en la JCF

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

2.2. Otros aspectos de la implementación

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

A veces resulta interesante almacenar el estado de una aplicación, de modo


que podamos parar la ejecución y recuperar el estado cuando la volvamos
a poner en marcha. Otras veces, no será necesario guardar el estado entero
de una aplicación, pero sí el conjunto de datos con los que trabaja (de hecho,
un subconjunto del estado de la aplicación). Es posible, incluso probable,
que una parte del estado de la aplicación esté constituida por instancias de
colecciones de elementos.
© FUOC • P06/75001/00583 • Módulo 9 35 Diseño de estructuras de datos

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

Otro aspecto interesante que es necesario comentar es el uso de una biblioteca


de colecciones en entornos concurrentes. Es habitual que una aplicación ten-
ga varios hilos de ejecución que se ejecutan en paralelo y que se pueden inter-
comunicar. Estos hilos acceden a la misma área de memoria y pueden haber
sido programados de modo que accedan a los mismo objetos.

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.

Si existe la posibilidad de que varios hilos de ejecución realicen accesos concu-


rrentes no permitidos en una colección, el usuario de la biblioteca es responsable
de que sólo un hilo realice la acción y el resto se espere mediante las herramientas
disponibles en el lenguaje de programación. Estas herramientas son muy depen-
dientes del lenguaje de programación usado y pueden ir desde los clásicos semá- Los semáforos se estudian en la
asignatura Sistemas operativos.
foros hasta métodos o regiones sincronizadas como en el caso de Java.

En el diseño de la biblioteca, se puede decidir que los usuarios no se deban pre-


ocupar por la sincronización y definir estos mecanismos de manera interna;
pero esto supone un sobrecoste importante en la eficiencia. Así, por ejemplo,
en el caso del lenguaje Java, cada región sincronizada implica la definición au-
tomática de semáforos sobre los que se realizan operaciones cada vez que se
entra o se sale de una región sincronizada. Si la biblioteca implementa este me-
canismo de manera interna para la biblioteca, pagaremos el sobrecoste tam-
bién para los casos en los que no sea necesaria la sincronización, por ejemplo,
© FUOC • P06/75001/00583 • Módulo 9 36 Diseño de estructuras de datos

cuando la aplicación tiene un único hilo de ejecución. Por lo tanto, la mayoría


de las bibliotecas no implementan este tipo de mecanismo y dejan libertad a
los usuarios para decidir cuándo quieren pagar el sobrecoste y cuándo no.

La biblioteca de colecciones Java Collections Framework (JCF), por ejemplo,


ofrece implementaciones de colecciones sin sincronizar. En el caso de que el
usuario necesite el uso de mecanismos de sincronización, la biblioteca ofrece un
conjunto de métodos en la clase Collections (synchronizedList, synchronizedMap,
etc.) que permiten crear un envoltorio sincronizado para instancias de coleccio-
nes no sincronizadas. El usuario tiene así plena libertad para decidir cuándo
paga el sobrecoste de la sincronización, y no tiene la necesidad de implementar
él mismo los mecanismos de sincronización.
© FUOC • P06/75001/00583 • Módulo 9 37 Diseño de estructuras de datos

3. Bibliotecas de colecciones existentes

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.

3.1. Java Collections Framework (JCF)

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

La JCF ha sido diseñada explotando la dualidad interfaz-implementación pro-


porcionada por el Java, de modo que se dispone de una jerarquía de interfaces
y de una jerarquía de implementaciones. Adicionalmente, se proporcionan al-
gunos algoritmos básicos polimórficos que trabajan sobre las colecciones de la
biblioteca, como por ejemplo, algoritmos de ordenación.

La jerarquía de interfaces sirve para dotar de homogenidad a la signatura de las


colecciones proporcionadas, lo que permite una buena integración con un uso
que maximice el diseño orientado a objetos. La jerarquía de implementaciones
maximiza la reutilización en la codificación misma de las implementaciones,
algo que puede resultar útil en el caso de desear extender la biblioteca con colec-
ciones definidas por el usuario mismo. Con este objetivo, la biblioteca contiene
algunas clases abstractas que proporcionan la base para la implementación de co-
lecciones (por ejemplo, java.util.AbstractMap) y que pueden ser extendidas y
completadas para proporcionar colecciones con un comportamiento específico Los árboles binarios
equilibrados de la JCF
adaptado a alguna situación concreta.
Los árboles binarios equilibra-
dos usados en la JCF no son los
El conjunto de colecciones proporcionado es en realidad pequeño, aunque su- árboles AVL explicados en este
texto, sino árboles rojinegros,
ficiente para la mayoría de los usos y necesidades. Las colecciones proporciona- que mejoran ligeramente la efi-
das se limitan a pilas, diferentes variantes de listas, dobles colas, diccionarios ciencia de sus operaciones en
un factor constante
(mappings) y conjuntos. De estos últimos, se proporcionan implementaciones (Goodrich y Tamassia, 2001;
Sahni, 2000).
basadas en tablas de dispersión y árboles binarios equilibrados.
© FUOC • P06/75001/00583 • Módulo 9 38 Diseño de estructuras de datos

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.

Respecto a la clase Comparator, se puede utilizar en las colecciones ordenadas


–como ahora SortedSet o SortedMap– como alternativa al orden natural de los ele-
mentos, proporcionado al implementar éstos la interfaz java.lang.Comparable.

Con anterioridad a su versión 1.2, el JDK proporcionaba algunas implementa-


ciones de colecciones, como java.util.Vector, con un conjunto de operaciones
diseñado de modo individual para cada una. La JCF, incorporada al JDK a partir
de la mencionada versión, proporciona un conjunto más amplio de coleccio-
nes. Adicionalmente, el diseño de la interfaz se ha realizado conjuntamente, en
un esfuerzo por maximizar su homogeneidad.

Con este objetivo de maximizar la homogeneidad, algunas interfaces correspon-


dientes a colecciones muy generales ofrecen operaciones opcionales, que algunas
de sus extensiones podrán proporcionar y otras no. Las implementaciones que
no soportan alguna operación definida en el TAD que implementan (la inter-
faz correspondiente), lanzan la excepción UnsupportedOperationException.

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.

3.2. Java Data Structures Library (JDSL)

Goodrich y Tamassia (2001) han participado en el diseño de la Java Data Struc-


tures Library (JDSL), también desarrollada, como su nombre indica, en Java. Esta
biblioteca proporciona un conjunto de colecciones más amplio que las propor-
cionadas por la JCF, entre las cuales encontramos: árboles de propósito general,
colas con prioridad y grafos.

Como elemento de diseño distintivo, la JDSL proporciona dos abstracciones


que permiten diferenciar desde fuera la situación de un elemento en una co- El concepto de posición de la
biblioteca de colecciones de la
lección. Estas abstracciones, denominadas posición y localizador, incrementan asignatura está directamente
inspirado en el de la JDSL.
mucho la versatilidad de la biblioteca; y permiten diseñar fácilmente nuevas
© FUOC • P06/75001/00583 • Módulo 9 39 Diseño de estructuras de datos

estructuras de datos a partir de las existentes en situaciones en las que, en otra


biblioteca sin estas abstracciones, sería necesario partir de cero.

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.

situaciones concretas sin necesidad de redefinir todo el algoritmo.

Uno de los principales objetivos perseguidos en el diseño de la JDSL ha sido la


fiabilidad. Se ha diseñado cuidadosamente un conjunto de situaciones excep-
cionales que son notificadas al usuario mediante el lanzamiento de excepcio-
nes muy informativas sobre la situción producida. Las situaciones anormales
reportadas incluyen también malos usos de posiciones y localizadores.

La jerarquía de interfaces de la JDSL clasifica las colecciones en posicionales y


en basadas en clave. En las primeras, cada uno de los elementos tiene una po-
sición dentro de la colección, y ofrecen un conjunto de métodos que permiten
trabajar a partir de la abstracción posición. Las colecciones basadas en clave
permiten acceder a los elementos de una colección a partir de una clave aso-
ciada y utilizan localizadores como accesorios.

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.

Con esta técnica, la JDSL proporciona a sus usuarios la posibilidad de decidir en


tiempo de compilación qué métodos, componentes o, en general, partes de un
sistema de software tienen derecho a modificar una colección y cuáles sólo tie-
nen derecho a consultarla. Esta técnica puede proporcionar ventajas similares a
las proporcionadas por la posibilidad en algunos lenguajes de programación de
definir parámetros de entrada frente a entrada/salida.

Respecto a la eficiencia, como en la mayoría de las bibliotecas, los costes asin-


tóticos son los mejores posibles. Estos costes se ven penalizados por un peque-
ño factor constante en cuanto al espacio y tiempo a causa de la versatilidad
(espacio necesario adicional para el uso de accesorios) y la fiabilidad (tiempo
necesario adicional para la realización de ciertas comprobaciones). En la im-
plementación de la biblioteca, se han empleado técnicas de creación sobre la
marcha (on-demand) y caching para mejorar la eficiencia.

3.3. Library of Efficient Data Structures (LEDA)

La Library of Efficient Data Structures (LEDA) es una biblioteca desarrollada en


C++ disponible sobre la mayoría de plataformas y diseñada poniendo especial
© FUOC • P06/75001/00583 • Módulo 9 40 Diseño de estructuras de datos

é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++.

Al estar desarrollada en C++, no ha sido diseñada utilizando la dualidad inter-


faz-implementación. En cambio, hace un uso bastante amplio de tipos para-
métricos, introducidos en C++ hace mucho tiempo. Este uso le sirve para
proporcionar implementaciones genéricas y decidir en tiempo de compilación
el tipo de elementos que almacenar, del mismo modo que en la biblioteca de
colecciones de la asignatura.

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.

El mecanismo es sencillo, pero al mismo tiempo eficaz y modular: LEDA defi-


ne el TAD y aquella parte del comportamiento más general, independiente-
mente de la implementación concreta. El comportamiento que depende de la
implementación concreta se delega en una estructura de datos que figura
como parámetro del TAD. De este modo, cambiando el valor de este paráme-
tro, podemos proporcionar de un modo fácil y modular diferentes implemen-
taciones del TAD. Para todos los TAD en los que se usa este mecanismo, LEDA
proporciona implementaciones útiles para el caso general; pero, mediante la
parametrización de tipos, ofrece la posibilidad a los usuarios de definir su pro-
pia implementación.

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.

El segundo mecanismo de iteración proporcionado es el de los iteradores, amplia-


mente utilizado en estos apuntes. Se proporcionan diferentes tipos de iteradores,
enfocados principalmente a ser usados en algoritmos de tratamiento de grafos.
Adicionalmente, LEDA también proporciona la posibilidad de definir envoltorios
© FUOC • P06/75001/00583 • Módulo 9 41 Diseño de estructuras de datos

(wrappers) para hacer compatibles sus iteradores con los iteradores de la STL, una
biblioteca que se comenta a continuación.

3.4. Standard Template Library (STL)

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.

Se trata de una biblioteca muy abstracta y reutilizable en una amplia variedad


de situaciones. Sin embargo, se ha puesto especial énfasis en que este hecho
no penalice la eficiencia de operaciones y algoritmos. Los elementos en torno
a los que se construye la biblioteca son los contenedores, los iteradores, los al-
goritmos y los functores.

Los contenedores proporcionados incluyen los típicos contenedores secuen-


ciales, conjuntos, y diccionarios (maps). Respecto a los dos últimos, se propor-
cionan versiones en las que se permiten un único elemento por clave, y
versiones en las que se permiten múltiples elementos por la misma clave.

La biblioteca dispone de una larga colección de algoritmos para manipular


los datos almacenados en contenedores. Muchos algoritmos están desaco-
plados totalmente de los contenedores y pueden trabajar sobre diferentes ti-
pos. Esto se consigue explotando al máximo el concepto de iterador.

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.

mite iterar sobre una tabla de elementos almacenados en un área de memoria).

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

La STL clasifica el conjunto de iteradores en varios modelos, según las necesi-


dades que tienen los algoritmos. Así, por ejemplo, encontramos los input-ite-
rators, que permiten iterar sobre una secuencia de elementos, pero únicamente
una vez. Otros modelos son el forward-iterator que especializan los input-itera-
tor y permiten repetir el recorrido, o los bidirectional-iterator, que permiten
combinar movimientos hacia delante y hacia atrás.

Para acabar, la STL utiliza la posibilidad de redefinir operadores ofrecida por el


C++ para parametrizar de un modo muy cómodo fragmentos de comporta-
miento para la biblioteca. El mecanismo consiste en delegar estas partes de
comportamiento en el operador función de los parámetros de entrada del al-
goritmo. De este modo, los usuarios de los algoritmos únicamente necesitan
definir el operador función con el comportamiento deseado, y pasan el objeto
correspondiente como parámetro. Se trata, otra vez, de un mecanismo intere-
sante, pero que emplea particularidades de un lenguaje concreto (el C++) y
que sería también reemplazable por otros mecanismos proporcionados por la
orientación a objetos de modo general.

3.5. Booch components

Esta biblioteca se concibió inicialmene para Ada83 en 1987, se rediseñó pos-


teriormente para C++ y, finalmente, se volvió a adaptar a Ada95.

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.

La biblioteca proporciona una serie de TAD: sacos, colecciones, colas, grafos,


listas, diccionarios y árboles, entre otros. Se trata de TAD genéricos que se
pueden instanciar para tipos concretos de elementos. Todos ellos heredan de
una misma clase, que proporciona el comportamiento básico para cualquier
contenedor.
© FUOC • P06/75001/00583 • Módulo 9 43 Diseño de estructuras de datos

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.

Para cada una de las estrategias de gestión de memoria disponibles para un


TAD (no todas están disponibles para cada TAD), los Booch Components de-
finen una nueva clase que hereda del TAD. De este modo, se consigue desvin-
cular completamente la definición del comportamiento propio del TAD de
aquel comportamiento que corresponde a la gestión de memoria.

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.

Para finalizar, los Booch Components también proporcionan un conjunto de


abstracciones algorítmicas que ofrecen la posibilidad de realizar operaciones
como búsquedas y ordenaciones sobre las colecciones (entre otras).
© FUOC • P06/75001/00583 • Módulo 9 44 Diseño de estructuras de datos

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.

Los apartados 2 y 3 del módulo hablan de bibliotecas de colecciones. En el dos


se describen las características deseables. En la tercera parte, se han presentado
algunas de las bibliotecas de colecciones más populares en el mundo de la
orientación a objetos y se han descrito sus características principales.
© FUOC • P06/75001/00583 • Módulo 9 45 Diseño de estructuras de datos

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?

4. Definid, diseñad (usando la biblioteca de clases de la asignatura) y describid las operacio-


nes de un TAD que permitan manejar el histórico de notas de los estudiantes que han sido
matriculados en un colegio. Únicamente se guardará el histórico de los alumnos matricu-
lados durante los últimos 10 años, que será de unos 10.000. Concretamente, el TAD debe
permitir realizar dos operaciones: dar de alta la nota de un alumno para una asignatura y
un curso concretos, y consultar todas las notas de un alumno para una asignatura (en el
caso de que la haya repetido).

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?

10. Continuando con la implementación ConjuntoVectorImpl, desarrollad un programa de


prueba que emplee una misma instancia de Conjunto<int> desde dos hilos de ejecución
diferentes, de modo que en un hilo se vayan introduciendo elementos aleatorios entre 1
y 10, en el otro se vayan borrando también aleatoriamente elementos entre 1 y 10 (para
hacerlo, revisad la documentación sobre la clase Thread en el Javadoc del JDK).
a) Imaginad y describid una situación concreta en la que la ejecución de los dos hilos
pueda causar un estado inconsistente en la representación de la colección (una ejecución
del programa de prueba desarrollado en el que los dos hilos trabajen sobre la colección
un buen rato debería generar esta situación).
b) Modificad la implementación para hacerla robusta con vistas a posibles usos concu-
rrentes (pista: revisad la documentación sobre la palabra reservada synchronized). ¿Qué
inconveniente tiene esta solución?
c) Partiendo de la implementación original de la colección (sin el uso del synchronized in-
troducido en el apartado anterior), proporcionad una solución que cubra accesos concu-
rrentes parecida a la proporcionada en la Java Collections Framework para la clase
Collections (métodos synchronizedCollection y otros).

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.

13. Revisad la interfaz de las siguientes clases (o interfaces):


• uoc.ei.tads.Lista (de la biblioteca de la asignatura)
• java.util.List (de la JCF)
• jdsl.core.ref.NodeSequence (de la JDSL)
Contestad los siguientes apartados:
a) Explicad, basándonos en cada una de las colecciones mencionadas más arriba, el uso
que hace cada una de las bibliotecas de la dualidad interfaz-implementación.
b) Comparad la versatilidad y la potencia de las tres colecciones.
c) Haced lo mismo respecto a la usabilidad, simplicidad y facilidad de aprendizaje.
d) Comparad la homogeneidad de cada una de las tres colecciones en el ámbito de la bi-
blioteca en el que están definidas.
© FUOC • P06/75001/00583 • Módulo 9 47 Diseño de estructuras de datos

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.

Podemos aplicar el patrón observador a nuestro caso:

• El objeto observado será claramente la instancia de ColaConPrioridadActualizable.


• Definimos una nueva interfaz que denominaremos ObservadorContenedorPosicional con mé-
todos para escuchar acontecimientos referentes a la creación o a la eliminación de nuevas
posiciones.
• Los clientes de la ColaConPrioridadActualizable deberán implementar esta interfaz y re-
gistrarse como observadores.
Figura 7
Podemos ver, en la figura 7, el diagrama UML con
las clases involucradas y sus operaciones.

En los recursos electrónicos de la asignatura encon-


traréis la implementación acompañada de un ejem-
plo de aplicación.

2. Definimos un TAD con las siguientes operaciones:

• 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

Descripción de las operaciones:

Asumimos, primero, que numeroClientes es el número de clientes almacenados en el dic-


cionario de clientes; y, segundo, que ncDescuento es el número de clientes a los cuales apli-
camos un descuento especial por el consumo del mes anterior. El enunciado fija este
número en 1.000. A continuación desarrollaremos la descripción de las operaciones:

• 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:

• void ponerNota(String alumno,String asignatura)


• Iterador<Nota> consultarNota(String alumno,String asig,String cur-
soAcademico)

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

caching m Técnica que permite a una implementación, de manera transparente para el


usuario, recordar un conjunto de valores calculados con el objetivo de evitar volverlos a
calcular. Proporciona una mejora en la eficiencia temporal a costa de un empeoramiento
en la eficiencia espacial.

creación sobre la marcha f Técnica que permite retrasar el momento de la creación de


un objeto hasta que el usuario intenta hacer uso de él. Esto se hace de manera transparente
al usuario y permite evitar la creación de aquellos objetos que después no se utilizarán.
en on demand creation

decisión de diseño f Cualquier elección que condicione el diseño final en el proceso de


resolución de un problema.

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

restricción impuesta por el problema


sin. requisito no funcional

reusabilidad f Capacidad para usar un comportamiento ya definido en alguna clase o mé-


todo para solucionar total o parcialmente un problema nuevo.

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.

Franch, X. (2001). Estructura de la información (4a. ed.). Apuntes de la asignatura. Barcelona:


Fundació per la Universitat Oberta de Catalunya.

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

Meyer, B. (1999). Construcción de software orientado a objetos. Madrid: Prentice Hall.


© FUOC • P06/75001/00583 • Módulo 8 51 Diseño de estructuras de datos

Anexo

Para saber más


Tanto el campo de diseño de nuevos TAD, como el campo de Respecto a cada una de las bibliotecas concretas que se han pre-
diseño de bibliotecas están englobados dentro del campo sentado en el apartado 3 del módulo, existe mucha bibliografía,
mucho más amplio del diseño orientado a objetos. Sobre este tanto escrita como, sobre todo, en Internet. El mejor modo de
tema, existe mucha bibliografía para investigar. Como refe- encontrar el material más actualizado es realizar una búsqueda
rencias que se deben tener en cuenta en este ámbito, podéis en vuestro buscador preferido. Os puede resultar un ejercicio
consultar las obras de Meyer (1997) y de Larman (1999). muy gratificante profundizar en el conocimiento de algunas de
las bibliotecas presentadas, incluyendo la descarga y la realiza-
ción de algún ejercicio práctico.

También podría gustarte