Está en la página 1de 66

Contenedores

secuenciales
Jordi Álvarez Canal

P06/75001/00577
Módulo 3
© FUOC • P06/75001/00577 • Módulo 3 Contenedores secuenciales

Índice

Introducción ............................................................................................ 5

Objetivos ................................................................................................... 7

1. Pilas ...................................................................................................... 9
1.1. Operaciones .................................................................................... 9
1.2. Implementación basada en un vector ............................................ 12
1.2.1. Definición de la representación .......................................... 12
1.2.2. Implementación de las operaciones ................................... 13
1.2.3. Análisis de costes ................................................................. 14
1.2.4. Codificación en Java ........................................................... 15
1.2.5. Ejemplo de uso de la colección ........................................... 16

2. Colas ..................................................................................................... 19
2.1. Operaciones .................................................................................... 19
2.2. Implementación basada en un vector ............................................ 20
2.2.1. Definición de la representación .......................................... 21
2.2.2. Implementación de las operaciones ................................... 23

3. Representaciones encadenadas ...................................................... 25


3.1. Referencias y contenidos ................................................................ 25
3.2. Referencia nula ............................................................................... 28
3.3. Encadenamiento de datos .............................................................. 28
3.4. Ejemplo: implementacion encandenada de Cola............................ 30
3.4.1. Definición de la representación .......................................... 30
3.4.2. Implementación de las operaciones ................................... 31
3.5. Gestión de la memoria y recogida de basura ................................. 32
3.6. Representacion con vector y representación encadenada ............. 33

4. Listas ..................................................................................................... 35
4.1. Posiciones ....................................................................................... 36
4.2. Recorridos ....................................................................................... 37
4.3. Operaciones .................................................................................... 38
4.4. Implementación del TAD Lista ....................................................... 40
4.4.1. Definición de la representación .......................................... 41
4.4.2. Implementación de las operaciones ................................... 42
4.5. Recorrido de los elementos de un contenedor: TAD Iterador ......... 46
4.5.1. Implementación .................................................................. 48
4.6. Ejemplo de uso del TAD Lista ......................................................... 50

5. Representaciones con vector: redimensionamiento ................ 53

6. Los contenedores secuenciales en la Java Collections


Framework ......................................................................................... 57
© FUOC • P06/75001/00577 • Módulo 3 Contenedores secuenciales

Resumen .................................................................................................... 59

Ejercicios de autoevaluación ............................................................... 61

Solucionario ............................................................................................. 63

Glosario ..................................................................................................... 63

Bibliografía .............................................................................................. 64

Anexo.......................................................................................................... 65
© FUOC • P06/75001/00577 • Módulo 3 5 Contenedores secuenciales

Introducción

El concepto de secuencia es principal en la programación estructurada. Recordad En la asignatura Fundamentos


de programación, el concepto de
los esquemas de recorrido y búsqueda en una secuencia; y cómo muchos algorit- secuencia se trata como una de las
nociones básicas que permiten la
elaboración de algoritmos.
mos se pueden desarrollar como la aplicación de alguno de estos patrones, o su
combinación.

Si tenemos en cuenta la importancia del tratamiento de los datos, parece claro


que, en muchas ocasiones, nos interesará también almacenar conjuntos de da-
tos de manera secuencial y, posteriormente, acceder a ellos también de mane-
ra secuencial.

Así, por ejemplo, sea cual sea el tipo de colección que utilicemos para resolver un
problema, muy a menudo nos interesará hacer el tratamiento secuencial de los
elementos almacenados. Por otro lado, también nos encontraremos en situacio-
nes en las que guardar los elementos de manera secuencial puede ser la manera
natural de representarlos. Casos concretos en el mundo real pueden ser la lista de
la compra, las cartas de una baraja, los coches en una línea de montaje, etc. El con-
junto de tipos abstractos de datos (TAD) que nos permitirán representar los ele-
mentos de manera secuencial son los que se representan en este módulo.

Dicho esto, es necesario realizar una aclaración importante antes de continuar. En el módulo “Tipos abstractos de
datos” se presenta el marco general
Ya hemos presentado el marco general en el que definiremos y trabajaremos con de los TAD.

los contenedores o TAD. En este módulo se hace patente la diferencia entre es-
pecificación e implementación del contenedor. La especificación la proporcio-
na la interfaz Java, que es donde tienen acceso los usuarios del contenedor. La
implementación se ejecuta cuando los usuarios del contenedor utilizan sus
operaciones; y debe permanecer siempre escondida para estos usuarios.

Así pues, cuando hablamos de contenedores secuenciales nos podemos referir a


dos cosas diferentes:

• El contenedor presenta una interfaz adecuada para que los usuarios puedan
implementación
trabajar de manera secuencial. Por ejemplo, una aplicación que represen- secuencial en sí misma
tara una cadena de montaje de coches que cada uno de los robots de la ca- Un ejemplo de esto lo tenemos
en la implementación Conjunto-
dena podría utilizar de contenedor. VectorImpl del módulo “Tipos
abstractos de datos”. En esta
implementación, los elementos
• La implementación del contenedor es en sí misma secuencial. Eso significa están almacenados físicamente
uno al lado del otro, ya que se
que los datos están almacenados físicamente en la memoria del ordenador almacenan en un vector.
de modo secuencial.

En este módulo presentaremos contenedores que proporcionan una intefaz dise-


ñada para el trabajo secuencial (es decir, pertenecientes al primer grupo). Adicio-
© FUOC • P06/75001/00577 • Módulo 3 6 Contenedores secuenciales

nalmente, el modo más natural y sencillo de implementar estos contenedores


será mediante implementaciones secuenciales (es decir, todas las implementacio-
nes que se ven en el módulo son también implementaciones secuenciales). A par-
tir de ahora, siempre que hagamos referencia a contenedor secuencial, estaremos
haciendo referencia a un contenedor con una interfaz diseñada para el trabajo se-
cuencial. Cuando queramos hacer referencia a las características de la implemen-
tación hablaremos de implementación secuencial.

Si revisáis la bibliografía de la asignatura, encontraréis que existen bastantes


Implementaciones de un
contenedores secuenciales y diferentes versiones –con más o menos operacio- contenedor secuencial
nes– de cada uno; la especificación de cada una de las operaciones también Si bien normalmente las
implementaciones de un con-
puede variar ligeramente de una fuente a la otra.
tenedor secuencial serán im-
plementaciones secuenciales,
nos podemos encontrar con
Esto es así porque no existen contenedores en los que la especificación esté de- implementaciones secuencia-
les para contenedores que no
finida de manera universal, como sucede con las leyes de la física. Por lo tanto, lo sean. Pensad, por ejemplo,
en la colección Conjunto pre-
cada autor o diseñador de bibliotecas de contenedores define su conjunto de sentada en el módulo “Tipos
contenedores de la manera que cree más conveniente, con las operaciones per- abstractos de datos”: la imple-
mentación es secuencial, pero,
tinentes para los usos de la biblioteca. en cambio, la interfaz no está
diseñada para el trabajo se-
cuencial.

La biblioteca de contenedores de la asignatura ha sido diseñada teniendo como


objetivo principal la didáctica. Por este motivo, hemos decidido proporcionar una
Las colecciones del JDK
biblioteca con los elementos indispensables tanto por lo que respecta a la canti-
dad de contenedores, como a las operaciones que proporcionan. Esta política es completamente
diferente a la usada por Sun
en el diseño de las colecciones
que acompañan a Java; utiliza
En este módulo veremos los contenedores secuenciales más comunes: las pi- una jerarquía de colecciones
bastante más compleja y multi-
las, las colas y las listas. La introducción de estos contenedores nos servirá tam- tud de operaciones y variantes
bién para presentar algunas cuestiones que nos acompañarán en el resto de de éstas para cada colección.

módulos: las alternativas para almacenar datos en la memoria del ordenador,


las construcciones como el patrón iterador que nos permiten acceder fácilmen-
te a los elementos de un contenedor sin necesidad de conocer ni la implemen-
tación ni buena parte de la interfaz, y otras.

En este módulo, como en los siguientes, primero se presenta cada uno de los
tipos de contenedor de manera intuitiva, después se describe más formalmen-
te su comportamiento, se explica a fondo su implementación y, por último, se
ven ejemplos de usos de éste.
© FUOC • P06/75001/00577 • Módulo 3 7 Contenedores secuenciales

Objetivos

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


fundamentales para que el estudiante alcance los siguientes objetivos:

1. Conocer la interfaz y saber usar los TAD secuenciales básicos: Pila, Cola y
Lista.

2. Saber decidir, de entre los TAD secuenciales básicos, cuál es el más adecua-
do para resolver un problema.

3. Entender las implementaciones presentadas por los TAD Pila, Cola y Lista;
y las diferencias básicas entre una representación con vector y una repre-
sentación encadenada.

4. Entender el concepto de posición como noción auxiliar de los TAD posicio-


nales. Saber usar el TAD auxiliar Posición.

5. Conocer el concepto de iterador y saber usar el TAD correspondiente para


realizar los tratamientos secuenciales de los elementos de una colección.

6. Saber proporcionar, a partir de una colección, implementaciones de itera-


dor sobre la base de su representación.

7. Comprender la relación entre los diferentes elementos del lenguaje Java y


el uso de la memoria que se derivan de éste; y comprender también el sis-
tema de gestión de la memoria que proporciona este lenguaje.

8. Ser capaces de diseñar y codificar algoritmos (por ejemplo, nuevas opera-


ciones de los TAD vistos en el módulo) utilizando tanto representaciones
con vector, como encadenadas.
© FUOC • P06/75001/00577 • Módulo 3 9 Contenedores secuenciales

1. Pilas

Una pila es un contenedor en el que los elementos se introducen y se LIFO es la sigla de la expresión
inglesa last in, first out.
borran de acuerdo con el principio “el último que entra es el primero
que sale”, conocido por la sigla LIFO.

Figura 1
Imaginad, por ejemplo, una pila de platos: únicamente podemos elegir el úl-
timo plato que hemos añadido a la pila. También utilizamos pilas cuando ju-
gamos a las cartas; incluso hay muchos juegos de cartas en los que se utiliza
más de una pila, tal como se ve en la figura 1.

Después de haber comentado estos dos ejemplos, seguro que vosotros mis-
mos sois capaces de encontrar algunas situaciones más de la vida cotidiana
en las que se usan pilas. Ya debéis saber que la CPU (es decir, la unidad cen-
tral de procesamiento, de la expresión inglesa central process unit) dispone de La estructura interna de la CPU se
estudia en la asignatura Estructura
y tecnologia de computadores.
una pila en la que las instrucciones de código máquina que la misma CPU
ejecuta pueden poner, consultar y obtener datos. De un modo parecido, los
lenguajes de programación estructurados (incluyendo los orientados a obje-
tos y en concreto Java) utilizan una pila para implementar el mecanismo de
llamada a procedimientos. En la figura 2, se ve una pila de llamadas de un
programa.
Figura 2

Como podéis observar, en este momento de la ejecución, el programa de ejem-


plo llama a la operación añadir de la implementación ConjuntoVectorImpl del
Pila de llamadas del programa de ejemplo del
TAD Conjunto. Al mismo tiempo, esta operación está llamando a la operación apartado 3 del módulo “Tipos abstractos
de datos” en un momento de su ejecución
esta, que llama al método privado buscarPosicionElemento.

Sin necesidad de introducirnos tanto en los intestinos del ordenador, el botón Podéis ver el TAD Conjunto definido
en el módulo “Tipos abstractos de
“Atrás” que proporcionan la mayoría de los navegadores de Internet utiliza datos” de esta asignatura.

una pila de páginas visitadas. Siempre que visitamos una página nueva, aña-
dimos a la pila de páginas la que hasta ahora era la actual; de modo que cuan-
do tiramos hacia atrás únicamente es necesario elegir la página que está arriba
del todo de la pila (que es la última que habíamos visitado).

1.1. Operaciones

Podéis ver el tratamiento de las


En este subapartado presentaremos cuáles son las operaciones que proporcio- operaciones en el módulo “Tipos
abstractos de datos de esta
asignatura.
na la colección Pila que encontraréis en la biblioteca de la asignatura. Recor-
© FUOC • P06/75001/00577 • Módulo 3 10 Contenedores secuenciales

dad que las colecciones de la biblioteca de la asignatura han sido definidas


como tipos paramétricos: a
Observación
colección Pila<E> exten Contenedor<E> es
Fijaos en que, además de las
operaciones descritas, Pila pro-
constructor() porciona también las operacio-
nes dadas por el Contenedor
Crea una pila. (estaVacio, numeroElementos y
@pre Cierto. elementos).

@post Devuelve una pila vacía.

void empilar(<E> elem)


Añade un elemento en lo alto de la pila.
@pre Cierto.
@post La pila final es la pila inicial añadiéndole elem a lo alto.

<E> desempilar()
Elimina el elemento de lo alto de la pila (el último que se ha añadido a la
pila), y lo devuelve como resultado.
@pre La pila tiene como mínimo un elemento.
@post La pila es equivalente a la pila antes de añadirle el elemento elimina-
do. El valor devuelto ($return) es el elemento borrado.

<E> cima()
Devuelve el elemento de lo alto de la pila (el último que se ha añadido a la
pila).
@pre La pila tiene como mínimo un elemento.
@post La pila es la misma que antes de realizar la operación. El valor de-
vuelto ($return) es el elemento que está en lo alto de la pila (el último
elemento añadido).

Estas cuatro operaciones proporcionan el comportamiento básico para cons-


truir y trabajar con una pila (junto con la operación estaVacio, definida en
Contenedor). A continuación, veréis el funcionamiento de la operaciones de la
pila mediante ejemplos. Como podréis comprobar, las operaciones pueden ac-
tuar en dos direcciones diferentes: modificar el estado del contenedor y calcu-
lar un resultado a partir del estado del contenedor.

Si una operación modifica el estado del contenedor, diremos que se trata


de una operación constructora o modificadora; y si no lo modifica, dire-
mos que es una operación consultora. La diferencia entre una operación
constructora y una modificadora radica en el hecho de que la primera es
imprescindible para llegar a un estado determinado del contenedor y la
segunda, no.

Veamos con más detalle esta diferencia. El estado de una pila siempre será el
resultado de una secuencia de llamadas a los métodos de pila. Por ejemplo,
© FUOC • P06/75001/00577 • Módulo 3 11 Contenedores secuenciales

el estado de la pila p de la figura 3, cuando tiene los elementos 8, 2 y 7, se


puede haber obtenido de la siguiente secuencia:

p = <llamada al constructor>();
p.apilar(7);
p.cima();
p.apilar(4);
p.desapilar();
p.apilar(2);
p.apilar(8);

Normalmente, podemos llegar al mismo estado mediante varias secuencias


de llamadas diferentes. Por ejemplo, también podemos llegar a la pila men-
cionada si eliminamos la llamada a cima. Y también si eliminamos al mismo
tiempo las llamadas p.apilar(4) y p.desapilar().

Observad, pues, cómo las operaciones cima y desapilar son prescindibles a la


hora de obtener la pila de la figura 3. Por lo tanto, las únicas operaciones ne-
cesarias para construir esta pila son constructor y apilar.

Extendiendo esta idea de una pila concreta (un estado concreto) a cualquier
pila o estado, diremos que el conjunto de operaciones constructores será aquel
conjunto mínimo de operaciones que nos permitirá, a partir de una secuencia
de llamadas a operaciones de este conjunto, construir cualquier estado para un
contenedor.

Veamos la clasificación de las operaciones de la pila según este criterio:

• Operaciones constructoras. Son constructor y apilar. Una combinación


adecuada de llamadas a estas dos operaciones nos permite construir cual-
quier pila.

• Operaciones modificadoras. Es la operación desapilar. Esta operación mo-


difica la pila sacando el elemento de la cima. Dado que siempre seremos
capaces de construir cualquier pila llamando únicamente a constructor y a
apilar, esta operación es modificadora (y no constructora).

• Operaciones consultoras. Son estaVacio y cima, junto con las otras opera-
ciones definidas en Contenedor (numElems y elementos). Estas operaciones
nos permiten consultar el estado de la pila sin modificarlo. Excepción

La operación elementos defini-


Una de las características del contenedor Pila es que, si miramos el contenedor da en Contenedor es una ex-
cepción a este principio que
como una secuencia de elementos, las operaciones que nos permiten añadir, comentaremos más adelante
en este módulo.
borrar y consultar elementos de la secuencia trabajan siempre sobre el mismo
© FUOC • P06/75001/00577 • Módulo 3 12 Contenedores secuenciales

extremo. Esto proporciona el comportamiento LIFO (“el último que entra es


el primero que sale”) característico de las pilas. Al redefinir este comporta-
miento podemos obtener otros contenedores secuenciales.

Figura 3

1.2. Implementación basada en un vector

La implementación más sencilla de una pila es usar un vector para guardar los Podéis ver el TAD
ConjuntoVectorImpl en el módulo
elementos. Esto tiene una implicación importante, tal como ya vimos, con “Tipos abstractos de datos” de esta
asignatura.
ConjuntoVectorImpl: si usamos un vector para representar los elementos, nece-
sitamos definir el máximo de elementos que se guardarán en el contenedor.
Por lo tanto, estamos hablando de un contenedor acotado.

1.2.1. Definición de la representación

Para definir cómo se almacenan los elementos de la pila en el vector, podemos


colocar el vector en posición vertical al lado de una de las pilas de cartas que
hemos visto antes, tal y como se aprecia en la figura 4.

Figura 4

En el vector tendremos una parte que estará llena con los elementos de la pila y
una parte que estará libre y que utilizaremos si se van añadiendo nuevos elemen-
tos. Como es habitual en este tipo de representación con vector, necesitamos un
atributo entero que nos diga qué parte del vector está llena y cuál está libre (de
hecho, este atributo nos indicará el número de elementos guardados).

Tal y como se aprecia en la figura 4, guardaremos cada elemento de la pila en


una posición del vector, de manera que el elemento más “antiguo” de la pila
estará almacenado en la posición 0 del vector, y el elemento más “nuevo”, en
la posición más grande de la parte ocupada.
© FUOC • P06/75001/00577 • Módulo 3 13 Contenedores secuenciales

1.2.2. Implementación de las operaciones

A partir de la representación anterior, pasamos a ver cómo implementamos las


operaciones de la coleccion pila. Daremos una lista conceptual de acciones que
realiza cada operación. Estas opciones se pueden corresponder con una instruc-
ción de Java o ser más complejas, y nos ayudan a ver con mucho detalle cómo se
implementan las operaciones con la representación elegida. Posteriormente, se
puede completar la descripción de la operación con el análisis del código Java.

Adicionalmente, al lado de las operaciones y de cada una de las acciones, en- El concepto de coste asintótico se
explica en el módulo “Complejidad
contraréis el coste asintótico de la operación, que se especificia utilizando la algorítmica”.

notación O. Como ya sabéis, el coste asintótico mide la eficiencia de las ope-


raciones del TAD y nos permite realizar un análisis aproximado de ésta.

En este primer TAD se detallan los costes de las acciones internas de cada una
de las operaciones. Adicionalmente, se dedica un apartado al análisis y la ex-
plicación de los costes.

colección PilaVectorImpl<E> implementa Pila<E>, ContenedorAcotado<E>

• constructor() O(DEFAULT_MAX)
– Llama al constructor con DEFAULT_MAX como número de elementos
máximo (O(DEFAULT_MAX)).

• constructor(int max) O(max)


– Crea el vector de elementos (O(max)).
– Asigna el número de elementos actual a 0 (O(1)).

• booleano estaVacio() O(1)


– Comprueba si el número de elementos es 0 (O(1)).

• booleano estaLleno() O(1)


– Comprueba si el número de elementos es diferente de 0 (O(1)).

• int numElems() O(1)


– Consulta el número de elementos (O(1)).

• void apilar(E elem) O(1)


– Asigna elem a la primera posición libre del vector de elementos (O(1)).
– Incrementa el número de elementos actual (O(1)).

• E cima() O(1)
– Devuelve el elemento de la última posición completa del vector (O(1)). La operación elementos se describe
y se explica más adelante.

• E desempilar() O(1)
– Guarda en una variable auxiliar la cima de la pila (O(1)).
– Asigna ‘nulo’ a la última posición completa del vector (O(1)).
– Reduce el número de elementos actual (O(1)).
© FUOC • P06/75001/00577 • Módulo 3 14 Contenedores secuenciales

Magnitudes del coste:


DEFAULT_MAX = Número máximo de elementos por defecto.

La implementación PilaVectorImpl implementa dos comportamientos comple-


mentarios: por un lado, el comportamiento de Pila y, por otro, el comportamien-
to de ContenedorAcotado. Por este motivo, la clase implementa estas dos interfaces.

Por otro lado, se utiliza la sobrecarga para definir dos constructores: uno que El uso de la sobrecarga
no tiene ningún parámetro y otro que acepta un parámetro entero. Este segun-
El uso de la sobrecarga para
do permite especificar la medida exacta de la pila creada mediante el paráme- definir varios constructores,
algunos de los cuales propor-
tro. De este modo, el primer constructor únicamente proporciona un valor por cionan valores por defecto
defecto a este parámetro. para los parámetros, resulta
muy útil y habitual.

La implementación del constructor (con parámetro) y de apilar es equiva-


Podéis ver la operación insertar de
lente a la que habíamos visto para el constructor y la operación insertar de ConjuntoPilaImpl en el módulo
“Tipos abstractos de datos”
ConjuntoPilaImpl. Las operaciones cima y desapilar son también muy sencillas y se de esta asignatura.

relacionan con el acceso y la gestión de la última posición completa del vector.

1.2.3. Análisis de costes

Como podéis comprobar a partir de los costes de las operaciones, PilaVectorImpl


es una implementación realmente eficiente. Todas las operaciones tienen un cos-
te constante excepto las constructoras, que tienen un coste proporcional al máxi-
mo de elementos que puede guardar la pila.

Analicemos con detalle el coste de dos de las operaciones:

• constructor(int max). En primer lugar, crea un vector de medida max en


el que se guardarán los elementos de la pila. Esto supone pedirle a Java el
trozo de memoria correspondiente. La ejecución de esta petición se podría
hacer en realidad en tiempo constante (O(1)). El lenguaje Java, sin embargo,
cuando crea un vector, siempre inicializa todas las posiciones del vector a ‘nu-
lo’. Por ello, necesita hacer un recorrido de las posiciones del vector, lo que
provoca un coste O(max). Posteriormente, se asigna el atributo correspondien-
te al número de elementos actual a 0. Se trata de una única instrucción de asig-
nación que, por lo tanto, se realiza en tiempo constante (O(1)). El coste
asintótico de la operación será el máximo de la secuencia de acciones que
realiza. Por lo tanto, la operación tiene un coste de O(max).

• desapilar(). En primer lugar guardamos en una variable auxiliar la cima de


la pila. Dado que disponemos del número de elementos guardados, pode-
mos acceder directamente a la cima de la pila (en tiempo constante O(1)).
Así pues, esta acción corresponde a una instrucción de asignación, que por
lo tanto realizaremos en tiempo constante (O(1)). A continuación, es nece-
sario asignar ‘nulo’ a la última posición completa del vector. El acceso es,
de nuevo, en tiempo constante. Asignarle ‘nulo’ lo hacemos también en
tiempo constante. Para acabar, reducimos el número de elementos actual.
Esto se traduce en una instrucción de resta y asignación que realizamos en
© FUOC • P06/75001/00577 • Módulo 3 15 Contenedores secuenciales

tiempo constante. Igual que antes, el coste asintótico de la operación será


el máximo de la secuencia de acciones que realiza. Dado que todas tienen
un coste constante, el coste de la operación también lo será.

1.2.4. Codificación en Java

A continuación podéis ver cómo se traducen en lenguaje Java las dos opera-
ciones que acabamos de ver:

PilaVectorImpl.java

package uoc.ei.tads;

public class PilaVectorImpl<E> implements Pila<E>,


ContenedorAcotado<E> {
public static final int MAXIMO_ELEMENTOS_POR_DEFECTO = 256;
protected int n;
protected E[] elementos;

public PilaVectorImpl() {
this(MAXIMO_ELEMENTOS_POR_DEFECTO);
}

public PilaVectorImpl(int max) {


elementos = (E[])new Object[max];
n = 0;
}
public int numElems() { return n; }
public boolean estaVacio() { return ( n == 0 ); }
public boolean estaLleno() { return (n == elementos.length); }
public E cima() { return elementos[n-1]; }

public void apilar(E elem) {


elementos[n] = elem;
n++;
}

public E desapilar() {
E aux = elementos[n-1];
elementos[n-1] = null;
n--;
return aux;
}

...
}
© FUOC • P06/75001/00577 • Módulo 3 16 Contenedores secuenciales

En la implementación del método desapilar se asigna ‘null’ a la posición ocu-


pada por el elemento desapilado. Esta asignación puede parecer superflua.
De hecho, si no la hiciésemos, la implementación de desapilar seguiría garan-
tizando su contrato.

Esta asignación es importante porque el lenguaje Java (a diferencia de otros


Recogida de basura
lenguajes como el C++) se encarga de gestionar de manera automática la
El proceso de recoger la memo-
memoria, reciclando los espacios que se han dejado de utilizar. Java realiza ria ocupada por todos aquellos
este trabajo contando las referencias en cada uno de los trocitos de memo- objetos que se han dejado de
usar se conoce en inglés como
ria ocupada por objetos (como los objetos referenciados en el vector ele- garbage collection (literalmente,
‘recogida de basura’). En el sub-
mentos del tipo PilaVectorImpl). apartado 3.5 encontraréis una
explicación más detallada de
este mecanismo.
Al eliminar la referencia al elemento desapilado, estamos diciendo a Java que
la pila correspondiente ya no usa este elemento. Entonces, si no existe ningu-
na otra diferencia en el objeto desapilado en toda la Java Virtual Machine,
Java reciclará la memoria ocupada por este objeto. Por razones de eficiencia,
Java no garantiza que este proceso se realice inmediatamente después de que
el número de referencias al objeto sea 0.

1.2.5. Ejemplo de uso de la colección

Como ya hemos comentado anteriormente, el TAD Pila es un TAD en el mun-


do de la informática. Veamos un ejemplo de él.

Muchas impresoras, en el proceso de imprimir un documento van depositando


las hojas impresas en una bandeja. Una vez terminada la impresión, si la cara im-
presa de las hojas es la de abajo, podemos sacar el conjunto de hojas de la bandeja
y éste estará perfectamente ordenado. En cambio, hay impresoras (por ejemplo,
muchas impresoras de inyección) en las que la cara impresa es la de arriba. Eso
implica que, cuando tomemos el documento impreso de la bandeja, debamos re-
ordenar todas las hojas.

Para evitar esto, normalmente el sistema operativo dispone de una opción de


impresión que permite imprimir las páginas de un documento en orden inver-
so. En este ejemplo implementaremos esta funcionalidad: a partir de un docu-
mento que representaremos por un conjunto de páginas, queremos obtener
otro equivalente al primero, con sus páginas en orden inverso.

Se pide definir una clase Documento que permita representar un documento


como una secuencia de páginas. Para simplificar, representaremos una pági-
na como un String. Dentro de esta clase, es necesario definir un método que
muestre el documento para la salida estándar (System.out). Este método debe
tener la siguiente signatura:

public void imprimir(boolean ordenInverso)

Si el booleano es ‘falso’, el documento se mostrará comenzando por la primera


página. Si el booleano es ‘cierto’, se mostrará en orden inverso.
© FUOC • P06/75001/00577 • Módulo 3 17 Contenedores secuenciales

Solución

Proponemos definir una clase Pagina que contenga únicamente un String con el
texto de la página. Definimos una clase Documento, que guarda las páginas de éste
como un vector de Pagina.

En la clase Documento, definimos:

• El constructor, que creará un documento vacío.


• Una operación para añadir una página al final del documento.
• Redefinimos el método toString de la clase Object con el objetivo de poner
en un String todo el documento.

A partir de aquí añadimos el método imprimir mencionado en el enunciado,


que deberá ser capaz de mostrar el documento comenzando por la primera pá-
gina o bien en orden inverso, depediendo del valor del parámetro. En el caso
de que el booleano sea ‘cierto’, necesitamos invertir el Documento.

Con este objetivo, definimos un método auxiliar denominado invertir que crea
un nuevo Documento con las mismas páginas que el documento original pero
en orden inverso.

Para implementar el método invertir hacemos lo siguiente:

• Ponemos las páginas del documento en una pila (comenzando por la pri-
mera).
• Creamos un documento vacío, al que vamos añadiendo las páginas de la
pila hasta que está vacía.

Dado que la pila sigue la estrategia LIFO (“el último que entra es el primero
que sale”), este algoritmo nos permite obtener un documento en el que las pá-
ginas estén en orden inverso a como se encuentran en el documento original.
A continuación, tenéis un fragmento del código Java:

Documento.java
En los recursos electrónicos
encontraréis el código Java
package uoc.ei.ejemplos.modulo3; completo de la clase Documento,
junto con las clases Pagina
y PruebaDocumento.
import uoc.ei.tads.*;

public class Documento {


Pagina[] paginas;
int numPaginas;
...
protected Documento invertir() {
Documento documentoInvertido= new
Documento(paginas.length);
Pila<Pagina> pila = new
PilaVectorImpl<Pagina>(paginas.length);
© FUOC • P06/75001/00577 • Módulo 3 18 Contenedores secuenciales

for(int i = 0;i<numPaginas;i++)
pila.apilar(paginas[i]);
while(!pila.estaVacio())
documentoInvertido.insertar(pila.desapilar());
return documentoInvertido;
}

public void imprimir(boolean ordenInverso) {


Documento docOrdenAdecuado = ordenInverso ?
invertir() : this;
System.out.println(docOrdenAdecuado.toString());
}
...
}
© FUOC • P06/75001/00577 • Módulo 3 19 Contenedores secuenciales

2. Colas

En este apartado estudiaremos el contenedor Cola. Del mismo modo que la pila,
el orden de los elementos en un cola está completamente determinado por su or-
den de inserción. El principio que hemos empleado es, sin embargo, diferente.

FIFO es la sigla de la expresión


inglesa first in first out.
En una cola, “lo primero que entra es lo primero que sale”, principio co-
nocido por la sigla FIFO.

Como sucede con las pilas, encontramos colas en muchas situa- Figura 5

ciones del mundo real. Normalmente, cuando vais a una tienda a


comprar os situáis en una cola de personas que lleva más tiempo
esperando. En la imagen, se aprecia una cola de personas que es-
peran para comprar una entrada para un concierto.

El uso de colas es muy habitual en el mundo real para organizar


peticiones que deben ser atendidas por un recurso. En el terreno
de los ordenadores, esta situación también es frecuente. Pensad,
por ejemplo, en los diferentes procesos que un ordenador ejecuta
Personas haciendo cola para comprar
supuestamente a la vez. Si un ordenador dispone únicamente de una entrada para un concierto.
un procesador, en cada momento podrá ejecutar un solo proceso.
Los procesos se colocan en una cola, y el procesador va seleccionando los procesos
por ejecutar de esta cola. Una vez seleccionados durante un período de tiempo
En las asignaturas de sistemas
breve (seguramente de milisegundos), el proceso se vuelve a colocar en la cola y operativos se estudia esta técnica con
detalle. La situación descrita es una
se selecciona el siguiente. De una manera similar, el sistema operativo gestiona el simplificación en la que no se tienen
en cuenta prioridades. Normalmente,
los procesos pueden tener diferentes
envío de impresiones a una impresora mediante una cola. En la figura 6, se puede prioridades (dependiendo de su criticidad).
En el módulo “Colas con prioridad”de esta
observar un ejemplo de cola de impresora. asignatura, se estudian estas colas.

Figura 6
Sistemas peer to peer

Los sistemas peer to peer de


intercambios de ficheros tam-
bién utilizan colas para decidir,
en cada momento, a quién en-
vía información cada uno de
los nodos. Si bien añaden a la
simplicidad de las colas un
mecanismo muy complejo de
prioridades.

2.1. Operaciones

Las operaciones son análogas a las que definíamos en el apartado anterior


para las pilas, pero con otros nombres y con los comportamientos adaptados
al principio FIFO.
© FUOC • P06/75001/00577 • Módulo 3 20 Contenedores secuenciales

colección Cola<E> extiende Contenedor<E> es


Las operaciones
del contenedor
constructor()
Crea una cola. Del mismo modo que Pila, Cola
también proporciona las ope-
@pre Cierto. raciones a Contenedor
(estaVacio, numElems
@post Devuelve una cola vacía. y elementos).

void encolar(<E> elem)


Añade un elemento a la cola.
@pre Cierto.
@post La cola resultante es la cola inicial a la que se ha añadido elem.

<E> desencolar()
Elimina el elemento que lleva más tiempo en la cola, y lo devuelve
como resultado.
@pre La cola tiene como mínimo un elemento.
@post En la cola resultante, quedan todos los elementos que tenía ex-
cepto el más antiguo. Los elementos están en el mismo orden.

<E> primero()
Devuelve el elemento que lleva más tiempo en la cola.
@pre La cola tiene como mínimo un elemento.
@post La cola es la misma que antes de realizar la operación. El valor de-
vuelto ($return) es el elemento que lleva más tiempo en la cola.

Al igual que en el caso de la Pila, podemos clasificar las operaciones de la Cola


en constructoras, modificadoras y consultoras:

• Operaciones constructoras: constructor y encolar.


• Operaciones modificadoras: desencolar.
• Operaciones consultoras: estaVacio y cabeza, también junto a las otras
operaciones definidas en contenedor (numElems y elementos).

Si miramos una cola como una secuencia ordenada de elementos con dos extre-
mos, la operación que nos permite añadir elementos trabaja sobre un extremo de
la secuencia, y las operaciones que nos permiten borrar y consultar elementos tra-
bajan sobre el otro extremo. Esto proporciona el comportamiento FIFO caracte-
rístico de las colas.

Figura 7

2.2. Implementación basada en un vector

Al igual que con una pila, la implementación más sencilla de una cola es con un
vector para guardar los elementos. En este apartado estudiaremos esta implemen-
tación. Hablamos, por lo tanto, de una representación acotada de una cola.
© FUOC • P06/75001/00577 • Módulo 3 21 Contenedores secuenciales

2.2.1. Definición de la representación

Podemos intentar definir una representación como las que hemos utilizado hasta Podéis ver PilaVectorImpl en el
apartado 1 de este módulo y
ahora para una PilaVectorImpl y ConjuntoVectorImpl. Es decir, a partir del vector en ConjuntoVectorImpl en el módulo “Tipos
abstractos de datos” de esta asignatura.
el que guardamos los elementos, y con la ayuda de un atributo entero que nos in-
dica el número de elementos almacenados, dividiremos el vector en dos partes:

1) La parte llena del vector.


2) La parte con posiciones del vector libres.

La parte llena comenzaría en la posición 0 (donde guardaríamos el elemento más


antiguo), y el atributo entero nos serviría para distinguir dónde acabaría la parte
llena y dónde comenzaría la libre, tal y como se ve en la figura 8.

Figura 8

Esta representación tiene un problema de eficiencia. Observad cómo ambos


extremos de la cola se ven modificados por alguna operación: la operación en-
colar añade un elemento a uno de los extremos, mientras que la operación des-
encolar elige un elemento del otro extremo (a diferencia de lo que sucedía con
la pila, donde todas las operaciones que modificaban el estado actuaban sobre
el mismo extremo).

En esta situación, la operación de desencolar supondría mover todos los ele-


mentos una posición, tal y como se puede apreciar en la figura 9. Este movi-
miento supone claramente un recorrido para todos los elementos de la cola y,
por lo tanto, un coste lineal respecto al número de elementos de la cola.

Figura 9

Un coste lineal no siempre será malo. La linealidad puede ser aceptable


según la situación y la magnitud en la que se manifieste. Ahora bien,
siempre que haya una representación equivalente con la que podamos
obtener costes asintóticos mejores, descartaremos la representación del
coste lineal (o, en general, del coste asintótico peor).
© FUOC • P06/75001/00577 • Módulo 3 22 Contenedores secuenciales

Podríamos intentar invertir el orden en el que se guardan los elementos en el


vector, dejando el último elemento añadido en la posición 0, y el más antiguo
en la posición más cerca a la zona libre. Eso permitiría tener una implementa-
ción para desencolar en tiempo constante. Ahora bien, no solucionaría el pro-
blema, ya que entonces sería encolar la que tendría un coste lineal. Dejemos
el razonamiento como ejercicio para el estudiante.

El problema es hacer corresponder de manera fija uno de los extremos de la


secuencia con la posición 0 del vector de elementos. Siempre que ejecutemos
la operación que modifica este extremo, será necesario un desplazamiento de
los elementos, con el correspondiente coste lineal.

Podemos solucionar este problema haciendo que ambos extremos de la parte


entera del vector se puedan desplazar. Lo haremos introduciendo un nuevo
atributo denominado primero, que nos indica la primera posición del vector
ocupada, tal y como se muestra en la figura 10.

Figura 10

Cada vez que se desencola un elemento de la cola, se incrementa el atributo pri-


mero. Esta solución proporciona implementaciones en tiempo constante para
encolar y para desencolar. Sin embargo, todavía hay un problema: ¿qué sucede si
encolamos y desencolamos muchas veces (como probablemente ocurra en mu-
chos de los usos de una cola)? Llegará un momento en el que la última posición
ocupada del vector pasará a ser la última posición real del vector. En esta situa-
ción nos gustaría aprovechar las posiciones de la parte inicial del vector que no
están ocupadas (antes de la posición primero).

Podemos aprovechar estas posiciones si consideramos el vector como una es-


tructura en la que la posición 0 sería la posición inmediatamente posterior a
la última posición, tal y como se ve en la figura 11, en la que está representada
una cola equivalente a la de la figura 10.

Figura 11
© FUOC • P06/75001/00577 • Módulo 3 23 Contenedores secuenciales

Implementar la circularidad en un vector es muy fácil y no complica demasia-


do el código resultante. Únicamente es necesario aplicar el módulo según la
medida del vector de elementos. Así pues, a partir de una posición p en un vec-
tor v, la siguiente posición del vector será siempre:

p + 1 % elementos.length

Utilizando esta estrategia de la representacion circular, conseguimos imple-


mentaciones en tiempos constantes para todas las operaciones de consulta y
modificiación de la cola y, además, siempre podemos aprovechar la totalidad
del vector en el que guardamos los elementos.

2.2.2. Implementación de las operaciones

colección ColaVectorImpl<E> implementa Cola<E>, ContenedorAcotado<E>

• constructor() O(DEFAULT_MAX)
– Llama al constructor con DEFAULT_MAX como número de elementos máxi-
mo (O(DEFAULT_MAX)).

• constructor(int max) O(max)


– Crea el vector de elementos (O(max)).
– Asigna el número de elementos actual y primero en 0 (O(1)).

• booleano estaVacio() O(1)


– Comprueba si el número de elementos es 0 (O(1)).

• booleano estaLleno() O(1)


– Comprueba si el número de elementos es igual al máximo (O(1)).

• int numElems() O(1)


– Consulta el número de elementos (O(1)).

• void encolar(E elem) O(1)


– Asigna elem a la primera posición libre del vector de elementos después de
la parte completa del vector (O(1)).
– Incrementa el número de elementos actual (O(1)).

• E primero() O(1)
– Devuelve la primera posición de la parte completa del vector (O(1)).

• E desencolar() O(1)
– Guarda en una variable auxiliar la cabeza de la cola(O(1)).
– Asigna ‘nulo’ a la primera posición completa del vector (O(1)).
– Pasa primero a la siguiente posición (O(1)).
– Decrementa el número de elementos actual (O(1)).
© FUOC • P06/75001/00577 • Módulo 3 24 Contenedores secuenciales

Magnitudes del coste:


DEFAULT_MAX = Número máximo de elementos por defecto.

ColaVectorImpl.java

package uoc.ei.tads;

public class ColaVectorImpl<E> implements Cola<E>, ContenedorAcotado<E> {


public static final int MAXIMO_ELEMENTOS_POR_DEFECTO = 256;
protected E[] elementos;
protected int n;
private int primero;

public ColaVectorImpl() {
this(MAXIMO_ELEMENTOS_POR_DEFECTO);
}

public ColaVectorImpl(int max) {


elementos = (E[])new Object[max];
n = 0;
primero = 0;
}

private int posicion(int posicion) {


return posicion % elementos.length;
}

private int siguiente(int posicion) {


return (posicion+1)==elementos.length ? 0 : posicion + 1;
}

public void encolar(E elem) {


int ultimo = posicion(primero + n);
elementos[ultimo] = elem;
n++;
}

public E desencolar() {
E elem = elementos[primero];
elementos[primero] = null;
primero = siguiente(primero);
n--;
return elem;
}
...
}

La implementación es un poco más compleja que la de la colección Pila a cau-


sa de la representación circular. Con el objetivo de mantener el código lo más
claro posible, se han utilizado dos métodos privados, posicion y siguiente, que
se ocupan de la circularidad.
© FUOC • P06/75001/00577 • Módulo 3 25 Contenedores secuenciales

3. Representaciones encadenadas

El lenguaje Java distingue dos grupos de tipos diferentes: los tipos básicos,
proporcionados directamente por el mismo lenguaje, y equivalentes a los ti-
pos que podemos encontrar en lenguajes estructurados no orientados a obje-
tos como el C o el Pascal (int, char y boolean), y las clases.

Java da un tratamiento diferente a estos dos grupos de tipos. Cuando en un


programa Java se define una variable de un tipo básico, el compilador Java
reserva espacio para un objeto de este tipo básico y asocia la variable de me-
moria que se ha reservado. Siempre que se modifica o se consulta esta varia-
ble, se está accediendo al trozo de memoria asociado a la variable.

En cambio, cuando se define una variable cuyo tipo corresponde a una clase
A, el compilador de Java reserva espacio para una dirección de memoria, y aso-
cia la variable con el espacio reservado.

Observad que, en este caso, el espacio reservado para la variable no es el ob-


jeto mismo, sino únicamente una dirección de memoria. Allí se almacena la
dirección de una instancia de A. Esta dirección se denomina habitualmente
referencia.

3.1. Referencias y contenidos

El concepto de referencia es parecido al concepto de apuntador de otros lengua-


jes como el C++, C, Pascal y otros. Al igual que se hace habitualmente en estos
lenguajes, diremos que una referencia “apunta” a un objeto cuando la direc-
ción contenida en el espacio de memoria correspondiente a la referencia es la
de este objeto.

A diferencia de lo que sucede en la mayoría de lenguajes que permiten trabajar


con apuntadores, en Java las referencias quedan ocultas, y a los ojos del pro-
gramador siempre se está trabajando directamente con el objeto apuntado.

A continuación, en el bloque de código siguiente, presentamos un programa


de ejemplo con el que se puede observar este comportamiento diferente para
las variables de tipos básicos y las de tipos definidos. El programa imprime
los números primos entre el 2 y un número máximo que se pide al usuario
interactivamente. Las líneas de código del método main se han numerado
para facilitar la explicación posterior que realizaremos.
© FUOC • P06/75001/00577 • Módulo 3 26 Contenedores secuenciales

Primeros.java

package uoc.ei.ejemplos.modulo3.referencia;
...
public class Primeros {
...
1 public static void main(String[] args) {
2 String str;
3 long maximo;
4 try {
5 str = Utilidades.leerString("",System.in);
6 try {
7 maximo = Long.parseLong(str);
8 for(long i=2; i<maxim; i++)
9 if (esPrimo(i))
10 System.out.print(i+" ");
11 ...
12 }
13 }
14 }

Fijaos en las variables maximo y str. La primera es instancia de un tipo básico


(long), y la segunda, de una clase (o tipo definido). En la figura 12 podemos
observar el comportamiento diferenciado para los dos casos, con una repre-
sentación esquemática de la memoria del ordenador después de haberse eje-
cutado las líneas 3, 5 y 7.

Figura 12
© FUOC • P06/75001/00577 • Módulo 3 27 Contenedores secuenciales

Variable maximo:

• En la línea 3 se declara la variable maximo. Esto asocia la mencionada va-


riable a un espacio de memoria en el que se almacena un valor de tipo long
(el valor de la variable).

• En la línea 7 se da valor a maximo. Declaración de variables

Muchas veces, en Java, la de-


– En primer lugar, se evalúa la expresión Long.parseLong(str). Esto devolverá claración de una variable se
hace en el mismo momento en
un valor de tipo long. el que se le da valor. En los
ejemplos precedentes lo he-
mos hecho en líneas diferentes
– A continuación, se realiza la asignación a maximo (maximo = ...). Esta asig- para que quedase más claro
qué corresponde a la declara-
nación resulta de copiar este valor en el espacio de memoria reservado para ción y qué a la creación de ob-
jetos (y asignación de valor
maxim. a una variable).

En cambio, para la variable str la historia es diferente:

• En la línea 2 se declara. Esto asocia la variable a un espacio de memoria


donde se almacenará una dirección de memoria.

• En la línia 5 se da valor a str.

– En primer lugar, se evalúa la expresión siguiente, que devolverá un objeto de


tipo String. Este objeto estará almacenado en la memoria, posicionado en al-
guna dirección (que para facilitar la explicación llamaremos @XXX).

Utilidades.leerString("",System.in).

– A continuación se realiza la asignación a str (str = ...). Esta asignación se traduce


en poner, en el espacio de memoria reservado para str, la dirección @XXX.

Queda claro que en el espacio de memoria correspondiente a variables de tipos


básicos se guarda directamente el contenido (o valor) de la variable. En cam-
bio, para variables que tienen por tipo una clase, en el espacio de memoria se
guarda una referencia al contenido (o valor). Es muy importante tener clara
esta distinción, ya que a partir de ahora estudiaremos bastantes estructuras de
datos en las que se hace uso intensivo de referencias.

Todos los lenguajes que permiten trabajar con apuntadores o referen-


cias disponen de alguna construcción para pedir memoria al sistema
operativo. En el lenguaje Java, esta petición la realiza la construcción
new, que, en primer lugar, pide la memoria necesaria al sistema opera-
tivo y, en segundo lugar, se encarga de ejecutar el método constructor
correspondiente.
© FUOC • P06/75001/00577 • Módulo 3 28 Contenedores secuenciales

3.2. Referencia nula

Todos los lenguajes que trabajan con apuntadores, aunque sea de manera en-
mascarada como Java, ofrecen al programador la posibilidad de decir que una
variable no está asociada, en un momento determinado, a ningún objeto.

En Java, esto se realiza mediante el valor especial ‘null’. Cuando el valor de una
variable es ‘null’, el espacio de memoria asociado a la variable no contiene nin-
guna dirección de memoria válida (que apunte a un contenido), sino que con-
tiene una marca que indica que la variable no tiene ningún objeto asociado.

3.3. Encadenamiento de datos

Hasta ahora habíamos construido estructuras de datos utilizando una técnica La técnica de la agregación se
explica en la asignatura
como la agregación y aprovechando la facilidad de construir vectores de objetos. Programación orientada a objetos.

La posibilidad de trabajar con direcciones de memoria ofrece nuevos recursos


y mucha flexibilidad para la definición de nuevas estructuras de datos. A con-
tinuación presentamos un ejemplo sencillo: la definición de una clase Persona,
donde guardamos una referencia a la madre y el padre de aquella persona.

Persona.java

package uoc.ei.exemples.modul3.referencia;
public class Persona {
String nombre;
Persona padre;
Persona madre;

public Persona(String nom) {


this.nombre = nombre;
madre = null;
padre = null;
}

public Persona(String nombre,Persona madre,Persona padre){


this.nombre = nombre;
this.madre = madre;
this.padre = padre;
}
...
}

Observad que estamos haciendo referencia a la clase Persona dentro de la misma


definición de clase Persona. Esta técnica se denomina, en general, recursividad;
y su uso es muy habitual en la definición de estructuras de datos. A aquellas es- El tema de la recursividad se explica
más ampliamente en el módulo.
“Árboles” de esta asignatura.
tructuras de datos que contienen referencias recursivas las denominaremos estruc-
turas de datos recursivas.
© FUOC • P06/75001/00577 • Módulo 3 29 Contenedores secuenciales

Las estructuras de datos recursivas nos permiten definir estructuras de datos


Unas viejas conocidas
que estén compuestas de elementos encadenados entre sí. Continuando con desconocidas
el ejemplo anterior, podemos crear una “familia” de Persona tal y como se Probablemente, vosotros mis-
muestra en el siguiente código: mos, en algún momento, de-
béis haber definido estructuras
de datos recursivas sin saber
que tenían este nombre.
Persona.java

public class Persona {


...
public static void main(String[] args) {
Persona abueloPedro = new Persona("Pedro");
Persona abuelaPaquita = new Persona("Paquita");
Persona madre = new Persona("Carmen",abueloPedro, abuelaPaquita);
Persona tioPedro = new Persona("Pedro",abueloPedro, abuelaPaquita);
Persona padre = new Persona("Jorge");
Persona Juan = new Persona("Juan",madre,padre);
...
}
}

En la figura 13 tenéis una representación gráfica de los objetos Persona crea-


dos. En la parte izquierda podéis ver una representación conceptual y en la
parte derecha tenéis un esquema parcial más cercano a la memoria del orde-
nador. A partir de una Persona, por ejemplo Juan, podemos acceder a su ma-
dre y a su padre, y desde éstos a sus abuelos. Y así sucesivamente hasta que
encontremos una Persona que no tiene definido padre ni madre (es decir, que
el valor de estos atributos sea ‘null’).

Figura 13

A todo este grupo de objetos lo denominamos estructura de datos encadenada.


Y a cada uno de los objetos que la componen los llamamos nodo.
© FUOC • P06/75001/00577 • Módulo 3 30 Contenedores secuenciales

3.4. Ejemplo: implementación encadenada de Cola

El uso de encadenamientos será muy útil a la hora de definir nuevas estructu-


ras de datos. Adicionalmente, todas aquellas implementaciones de coleccio-
nes que se basan en una representación con un vector se pueden trasladar a
una representación que, en lugar de utilizar vectores, emplee nodos y encade-
namientos.

Para abreviar, denominaremos representaciones encadenadas a este


tipo de representaciones con nodos y encadenamientos.

Veamos a continuación, a modo de ejemplo, la representación encadenada Podéis ver el TAD Cola definido en
el apartado 2 de este módulo
del TAD Cola. didáctico.

3.4.1. Definición de la representación

Toda representación encadenada de una colección está definida en dos niveles


diferentes:

• La representación del nodo, que nos permitirá almacenar un elemento de


la colección y proporcionar los encadenamientos necesarios para acceder a
otros nodos.

• La representación de la colección misma, que normalmente consistirá en


Encapsulación
uno o más encadenamientos, que nos permitirán acceder a los nodos don-
A menudo, los nodos quedan
de están almacenados los elementos; y cierta información adicional como, totalmente ocultos al usuario
por ejemplo, el número de elementos de la colección. de la colección, que únicamen-
te accederá al objeto Colección
y a sus operaciones.
El primero paso, y también el más importante, para proporcionar la representa-
ción es definir los encadenamientos necesarios (tanto para los nodos como para
la colección). Por ello, será necesario estudiar las operaciones que es necesario
implementar y proporcionar la configuración de encadenamientos que permite
una implementación más eficiente de las operaciones de la colección.

En el caso de la representación encadenada del TAD Cola, tenemos que:

1) Habrá una secuencia de nodos, con un primero y un último.

2) En un momento determinado, únicamente necesitaremos acceso al primer


y al último nodo.

3) En el momento de encolar, necesitaremos acceso al último nodo.

4) En el momento de desencolar, necesitaremos acceso al primer nodo. Debe-


mos tener en cuenta también que, después de desencolar, el segundo nodo
pasa a ser el nuevo primero.
© FUOC • P06/75001/00577 • Módulo 3 31 Contenedores secuenciales

De estos cuatro puntos podemos concluir que los nodos estarán encadenados
secuencialmente (punto 1), de manera que desde el primero se pueda acceder al
segundo, del segundo al tercero, y así hasta el último. Es decir, que queremos
tener encadenamientos hacia el siguiente nodo (punto 4). Además, en la repre-
sentación de la colección, necesitaremos tener encadenamientos para acceder
Podéis ver los encadenamientos
hacia los siguientes nodos en el
en tiempo constante al primero y al último. A partir de esto, una buena repre- apartado 4 de este módulo didáctico.

sentación es la que nos muestra en la figura 14.

Figura 14

Este tipo de estructuras en las que cada nodo tiene un encadenamiento que lo
encadena con el siguiente se denomina habitualmente lista simplemente enla-
zada, y es muy útil para implementar un gran número de colecciones con es-
tructura secuencial mediante representaciones encadenadas.

3.4.2. Implementación de las operaciones

Un vez definida la representación, veamos la descripción de la implementa-


ción de las operaciones más relevantes. Observad que, mientras que las imple-
mentaciones con vector implementaban la interfaz ContenedorAcotado, no
tiene sentido que las implementaciones encadenadas lo hagan, ya que no exis-
te ningún límite máximo para el número de elementos.

Tened en cuenta, sin embargo, que existe un límite físico en cuanto al número
La falta de memoria en los
de elementos de una implementación encadenada determinado por la memoria diferentes lenguajes

disponible (determinada por el sistema operativo). En Java, si en algún momen- La no-disponibilidad de me-
moria se trata de manera dife-
to se llega a este límite, la Java Virtual Machine lanza una RuntimeException, y rente según los lenguajes de
provoca normalmente la finalización de la ejecución de la aplicación. programación. Así, por ejem-
plo, en C o en C++, las funcio-
nes que obtienen memoria del
colección ColaEncadenadaImpl<E> implementa Cola<E> sistema operativo devuelven el
equivalente a ‘null’ y es res-
ponsabilidad del programador
• constructor() tratar esta situación.
– Inicializa primer y último en ‘null’.

• void encolar(E elem) O(1)


– Creamos un nuevo nodo con ‘elem’ y ‘null’ como siguiente .
– Si el primero es ‘null’ entonces primero = ‘nuevo nodo’.
– si no, ultimo.siguiente = ‘nuevo nodo’.
– Asignar ultimo al ‘nuevo nodo’.
– Incrementar el número de elementos de la cola.
© FUOC • P06/75001/00577 • Módulo 3 32 Contenedores secuenciales

• E desencolar() O(1)
– Guardamos el elemento del primer nodo en una variable auxiliar.
– Hacemos que el primero de la cola pase a ser el siguiente del primero actual. Podéis encontrar la
implementacion encadenada
de Cola en lenguaje Java como
– Si el nuevo primero es ‘null’, entonces hacemos que el último sea ‘null’. recurso electrónico en los ejemplos
de este módulo.
– Disminuimos el número de elementos actual.
– Devolvemos el elemento que hemos guardado al principio.

3.5. Gestión de la memoria y recogida de basura

El primer paso de la descripción de la operación encolar del apartado anterior


hace referencia a la creación de un nodo. Esta creación corresponde a la cons-
trucción new del lenguaje Java, que como ya hemos comentado anteriormen-
te, en primer lugar, demandará la memoria necesaria al sistema operativo y,
en segundo lugar, ejecutará el constructor correspondiente.

En la operación encolar se pide memoria al sistema operativo para nuevos


nodos que se encolarán al final de la cola. Por otro lado, en la operacion des-
encolar se dejan de utilizar nodos que se habían creado mediante encolar. Es-
tos nodos ya no estarán accesibles a partir de ningún encadenamiento ni del
objeto Cola ni de los nodos pertenecientes a la cola; y, de hecho, ya no los
necesitaremos más. En la figura 15 se puede observar cómo, partiendo de la
cola que nos ha servido de ejemplo en la figura que muestra la representa-
ción encadenada, después de una operación de desencolar tenemos un nodo
que ya no forma parte de la cola.

Figura 15

Este nodo ya no es necesario. Además, ni siquiera es accesible para la misma


Objetos basura
representación de cola. Por lo tanto, es deseable que la memoria ocupada por
Estos nodos, o más en general
todos aquellos nodos que ya no utilizaremos vuelva al sistema operativo. Si objetos, que en algún momen-
to se han creado pero que ya
no fuese así, después de muchas operaciones de encolar y desencolar, podría- no usaremos más, los denomi-
mos agotar la memoria del ordenador únicamente con estos nodos que ya naremos objetos basura (en
inglés, garbage).
no forman parte de la cola.
© FUOC • P06/75001/00577 • Módulo 3 33 Contenedores secuenciales

Cada lenguaje de programación define su propio mecanismo para reutilizar la


memoria ocupada por objetos basura. Pero todos se pueden clasificar según
dos grandes líneas:

1) La gestión de la memoria es responsabilidad del programador. En este caso,


el lenguaje debe ofrecer alguna construcción para que el programador devuel-
va la memoria ocupada al sistema operativo cuando el objeto ya no se haya de
usar más en la ejecución del programa. Lenguajes como el Pascal, C y C++ ofre-
cen las construcciones correspondientes, y asumen que el programador se en-
carga de llamarlas en los momentos adecuados. Así, C++ ofrece los operadores
new para crear objetos (tomar memoria) y delete, para destruirlos (devolver la
memoria al sistema operativo).

2) La gestión de la memoria es responsabilidad del lenguaje de programación.


En este caso, el lenguaje de programación debe ser capaz de detectar cuándo
un objeto no es accesible; y si no lo es, devolver la memoria ocupada al sistema
operativo. Este tipo de lenguajes necesitan, pues, un mecanismo de “recogida
de basuras” (en inglés, garbage collection). El programador no necesita ejecutar
el mecanismo de recogida de basuras en ningún momento, sino que se ejecuta
como un proceso transparente y paralelo a la ejecución de nuestro progama.
Java y los lenguajes funcionales, como por ejemplo, Lisp, incorporan mecanis-
mos de recogida de basura. Así pues, Java únicamente dispone del operador
new, sin ninguna contrapartida que devuelva la memoria al sistema operativo.

Los lenguajes en los que la gestión depende del programador tienen la desventaja
de que pueden sufrir más fácilmente problemas de pérdida de memoria porque el
programador se ha olvidado de liberar “objetos basura”. En cambio, tienen la ven-
taja de que la gestión del espacio no supone ningún sobrecoste de eficiencia.

3.6. Representación con vector y representación encadenada

Como hemos podido comprobar, existe más de una implementación para


un mismo TAD. En concreto, muchas veces os encontraréis con el dilema
de haber de elegir entre una implementación con vector y una implemen-
tación encadenada. La decisión en ocasiones no es fácil ni clara. Los si-
guientes puntos ponen de manifiesto ventajas y desventajas de las dos
representaciones:

• En una representación con vector, necesitamos conocer el número de ele- Redimensionamento


mentos máximo que es necesario guardar en el momento de crear el vector. de vectores

En cambio, en una representación encadenada podemos ir seleccionando La representación con vector


se puede hacer más “inteligen-
el espacio necesario sobre la marcha. te”, de manera que no sea ne-
cesario conocer el número de
elementos máximo para guar-
• Las representaciones con vector nos permiten acceder directamente a cual- dar desde el principio, como
quier elemento por el índice de la posición en la que están almacenadas. veremos en el apartado 5 este
módulo.
En cambio, en una representación encadenada accedemos a un nodo a par-
tir de un encadenamiento que tenemos en otro nodo.
© FUOC • P06/75001/00577 • Módulo 3 34 Contenedores secuenciales

• En una representación con vector, malgastamos espacio ocupando memo-


ria para las posiciones del vector que están libres (no siempre estaremos
ocupando el tamaño máximo). En cambio, en una representación encade-
nada, no hay posiciones libres.

• En una representación encadenada, necesitamos espacio para los encade-


namientos; mientras que en un vector no tenemos estos encadenamientos.

• En una representació con vector, nosotros mismos gestionamos y controla-


mos el espacio libre (esto añade complejidad, pero a veces puede interesar),
mientras que en una representación encadenada la gestión corresponde al len-
guaje de programación, que la delega normalmente al sistema operativo.

A partir de estos puntos, y según las condiciones y restricciones del problema


que intentamos resolver, tendremos elementos para decidir cuál de las dos re-
presentaciones es más adecuada en cada momento.
© FUOC • P06/75001/00577 • Módulo 3 35 Contenedores secuenciales

4. Listas

Ya habéis estudiado las colecciones Pila y Cola. Ambas son colecciones secuencia-
les en las que únicamente se pueden consultar y modificar los extremos. Muchas
veces nos interesará trabajar con estructuras secuenciales a las que podamos acce-
der y modificar la secuencia de elementos en cualquier parte. A este tipo de colec-
ciones las denominamos listas, y, como las pilas y las colas, a grandes rasgos, se
corresponden con lo que entendemos en el mundo real por el término lista.

Por ejemplo, cuando vamos a comprar al mercado, normalmente hacemos


una lista con las cosas que necesitamos (sobre todo si somos desmemoriados
o debemos comprar muchas cosas). Una vez en el mercado, actuaremos según
el siguiente algoritmo:

1) Revisaremos la lista.
2) Decidiremos la siguiente parada en la que compraremos.
3) Compraremos los elementos de la lista que podamos en esta parada.
4) Eliminaremos estos elementos de la lista. Repetiremos este procedimiento
mientras queden elementos en la lista.

También podríamos usar una pila o una cola para almacenar los elementos
Organizaciones alternativas
que es necesario comprar; pero el algoritmo sería bastante más ineficiente, ya de los elementos

que tendríamos acceso sólo al elemento de uno de los dos extremos, de mane- Una alternativa para utilizar
una cola o una pila sería agru-
ra que no sabríamos si hay más elementos para comprar en esta parada, y qui- par los elementos por paradas.
zá necesitaríamos volver después. Pero esto quita flexibilidad a la
hora de construir la lista. ¿Qué
sucede si la persona que hace
la lista no es la misma que la
Un editor de texto que va a comprar y no conoce
las paradas?
Un ejemplo más relacionado con la informática del uso de listas lo podemos encontrar
en un editor de texto. Cuando editamos un documento, estamos editando una lista de
líneas de texto. Cada una de las líneas de texto está constituida por una lista de caracteres.
Los editores de texto suelen tener un cursor, que es el lugar donde podemos modificar
tanto la lista de caracteres (añadiendo o borrando caracteres), como la lista de líneas de
texto (añadiendo, por ejemplo, una nueva línea).

Así pues, muchas veces nos será útil poder disponer de una colección que co-
rresponda a una secuencia sin restricciones de acceso y que, como ya hemos
dicho, denominaremos lista.

Los elementos de la lista tendrán un orden determinado según cómo se hayan


añadido en la lista: habrá un primero y un último, y cada elemento excepto el
último tendrá un siguiente. Adicionalmente, nos interesará realizar las si-
guientes operaciones:

• Añadir nuevos elementos en cualquier punto de la lista.


• Borrar cualquiera de los elementos de la lista.
• Recorrer la secuencia de elementos almacenados.
© FUOC • P06/75001/00577 • Módulo 3 36 Contenedores secuenciales

Para conseguir esto de una manera limpia, la colección Lista se ayudará de dos
tipos adicionales: el tipo Posicion y el tipo Recorrido.

4.1. Posiciones

El primero de estos tipos nos permitirá guardar o hacer referencia al contexto


posicional de un elemento de la lista. El concepto de posicion es muy parecido
al de nodo que hemos visto en el apartado anterior: una posición tendrá un ele-
mento siguiente y otro anterior (que podrán ser ‘null’ si se trata del último o
del primer elemento).

En cambio, ambos conceptos tienen una diferencia esencial: mientras que un Lectura recomendada
nodo es una representación física totalmente ligada a cuestiones de imple-
El TAD Posicion y el enfoque
mentación, una posición es un TAD desvinculado de toda representación y de posicional de algunas de las
colecciones de la biblioteca
toda implementación. está inspirado en la
aproximación de Goodrich y
Tamassia (2001).
Como veremos más adelante, podremos implementar una posición con un En esta obra podréis
encontrar mucho material
nodo. Pero no necesitamos saber cómo está definido ni qué encadenamientos complementario sobre el
tiene para trabajar con posiciones. Ésta es la principal ventaja de trabajar con tema. El enfoque, sin
embargo, es ligeramente
posiciones: desvincular los algoritmos que usen listas (u otras colecciones po- diferente al nuestro.

sicionales que veremos más adelante) sin necesidad de conocer la implementa-


ción que hay detrás.

El TAD Lista ofrecerá un conjunto de operaciones que permetirán modificar o


aceder a los elementos de la lista en función de información posicional. Así
podremos, por ejemplo, añadir un elemento después de una posición determi-
nada, o borrar una posición determinada.

En la figura 16 podéis ver una representación conceptual de una lista de enteros


con 6 elementos. Observad que, para realizar estas operaciones posicionales sobre
la lista, no tenemos suficiente con la información de lista: “añade después del cin-
co”, o “borra el 3”. En el primer caso, la lista misma debería buscar el 5; y en el
segundo caso no sabría a qué 3 elementos nos referimos. Los valores guardados
dentro de las posicioines de la lista son simples enteros y no contienen informa-
ción posicional en absoluto. En cambio, sí es factible realizar la siguiente opera-
ción: “añade después de la posición X” (por ejemplo, la tercera posición).

Figura 16
© FUOC • P06/75001/00577 • Módulo 3 37 Contenedores secuenciales

El TAD Posicion tiene una única operación que permite recuperar el elemento
almacenado. La biblioteca de TAD de la asignatura ha sido diseñada de manera
que el resto de operaciones posicionales son responsabilidad de la colección o
del TAD Recorrido.

tipo Posicion<E> es Como el resto de TAD, el TAD Posicion


está definido en la biblioteca de TAD
de la asignatura como una
interfaz Java.

E getElem()
Devuelve el elemento guardado en la posición.
@pre Cierto.
@post El elemento devuelto es el elemento guardado en la posición.

4.2. Recorridos

El TAD Recorrido nos permite recorrer las posiciones de una colección posicional
(como, por ejemplo, el TAD Lista) en un orden. En el caso de las listas, el recorrido
más habitual comienza por la primera posición, continúa con la segunda, y sigue
avanzando posiciones hasta llegar a la última.

En el ejemplo inicial de la lista de la compra, crearíamos un recorrido sobre la


lista de cosas por comprar cada vez que llegásemos a una parada. El recorrido
nos permitiría recorrer de manera ordenada las posiciones de la lista de la com-
pra. Entonces, para cada posición:

1) accederíamos al valor de la posición y determinaríamos si lo podemos com-


prar en aquella parada,

2) en caso afirmativo, los compraríamos y usaríamos la operación posicional


correspondiente del TAD Lista para borrar la posición de la lista,

3) avanzaríamos a la posición siguiente.

Un recorrido es un objeto completamente aparte de la colección que reco-


rre y tiene un estado propio que consiste en: a) una referencia a la colección
que se está recorriendo, y b) el elemento actual. De este modo, si nos inte-
resara, podríamos mantener múltiples recorridos independientes para la
misma colección.

La colección no tiene ningún conocimiento de los recorridos que hay


en cada momento, ni tampoco de su estado. Por este motivo, se ha de
ir con cuidado cuando se borren elementos de una colección: ¡si se bor-
ra el elemento actual de algún recorrido activo, este recorrido quedará
en un estado inconsistente!
© FUOC • P06/75001/00577 • Módulo 3 38 Contenedores secuenciales

tipo Recorrido<E> es

booleano haySiguente()
Comprueba si quedan posiciones por recorrer.
@pre Cierto
@post Devuelve ‘cierto’ si quedan posiciones por recorrer y ‘falso’, en
caso contrario.

Posicion<E> siguiente()
Devuelve la siguiente posición.
@pre haySiguiente()
@post La siguiente posición del recorrido.

4.3. Operaciones

Los TAD auxiliares Posicion y Recorrido permiten acceder secuencialmente a las


posiciones de una lista. Partiendo de esto, el TAD Lista proporciona un con-
junto de operaciones que permiten crear un recorrido para una lista y realizar
diferentes operaciones de actualización sobre la lista, a partir de una posición.

colección Lista<E> es

constructor()
Crea una lista vacía.
@pre Cierto.
@post Devuelve una lista sin ningún elemento.

Posicion<E> insertarAlPrincipio(E elem)


Añade un elemento al principio de la lista y devuelve la nueva posición.
@pre Cierto.
@post La enumeración secuencial de los elementos de la lista es elem +
la enumeración de los elementos de old(this).

Posicion<E> insertarAlFinal(E elem)


Añade un elemento al final de la lista y devuelve la nueva posición creada.
@pre Cierto.
@post La enumeración secuencial de los elementos de la lista es la enu-
meración de los elementos de old(this) + elem.

Posicion<E> insertarAntesDe(Posicion<E> pos,E elem)


Añade un elemento a la lista justo antes de la posición pos y devuelve la
nueva posición creada.
@pre pos Es una posición válida de la lista.
@post La enumeración secuencial de los elementos de la lista es la enu-
meración de los elementos de old(this) hasta pos (excluida) + elem + la
enumeración de old(this) a partir de pos (incluida).
© FUOC • P06/75001/00577 • Módulo 3 39 Contenedores secuenciales

Posicion<E> insertarDespuésDe(Posicion<E> pos,E elem)


Redundancia
Añade un elemento a la lista justo después de la posición pos y devuelve en las operaciones del TAD
la nueva posición creada. Observad que existe una cierta
@pre pos Es una posición válida de la lista. redundancia en las operacio-
nes de este TAD: las cuatro
@post La enumeración secuencial de los elementos de la lista es la enu- operaciones de insertar no son
todas imprescindibles para
meración de los elementos de old(this) hasta pos (incluida) + elem + la crear una lista. En el diseño de
enumeración de old(this) a partir de la posición siguiente a pos. este TAD se ha dado prioridad,
sobre todo, a la claridad en la
interpretación de lo que hacen
las operaciones y a su facilidad
E borrarPrimero() de uso.
Borra el primer elemento de la lista y devuelve su valor.
@pre La lista no está vacía.
@post La enumeración secuencial de los elementos de la lista es la enu-
meración de los elementos de old(this) menos el primer elemento.

E borrar(Posicion<E> pos)
Borra la posición pos, devolviendo su valor.
@pre pos Es una posición válida de la lista.
@post La enumeración secuencial de los elementos de la lista es la enu-
meración de los elementos de old(this) excluyendo pos.

E borrarSiguiente(Posicion<E> pos)
Borra la posición siguiente a pos y devuelve el valor almacenado.
@pre pos Es una posición válida de la lista que no es la última posición.
@post La enumeración secuencial de los elementos de la lista es la enume-
ración de los elementos de old(this) excluyendo la posición siguiente a pos.

E reemplazar(Posicion<E> pos,E elem)


Reemplaza el elemento almacenado en pos por elem y devuelve el antiguo.
@pre pos Es una posición válida de la lista.
@post La enumeración secuencial de los elementos de la lista es la enu-
meración de los elementos de old(this) reemplazando el elemento alma-
cenado en pos por elem.

void intercambiar(Posicio<E> pos1,Posicion<E> pos2)


Intercambia los elementos almacenados en pos1 y pos2.
@pre pos1 y pos2 Son posiciones válidas de la lista.
@post La enumeración secuencial de los elementos de la lista es la enu-
meración de los elementos de old(this) intercambiando los elementos
almanceandos en pos1 y pos2.

Recorrido<E> posiciones()
Devuelve un recorrido de las posiciones de la lista. El nodo actual inicial
es el primer nodo de la lista (en el caso de que la lista no esté vacía).
@pre Cierto.
@post El recorrido devuelto es tal que la enumeración secuencial de los
elementos devueltos por llamadas sucesivas es igual a la enumeración
secuencial de los elementos de la lista.
© FUOC • P06/75001/00577 • Módulo 3 40 Contenedores secuenciales

En varias de las operaciones presentadas en la especificación anterior, la pre-


condición contiene la condición de que los parámetros que hacen referencia El sistema de DBC de la asignatura
también se encarga de comprobar
las posiciones válidas.
a posiciones, hagan referencia a posiciones válidas de la lista. ¿Qué significa
esto? Simplemente, que se trata de alguna o algunas de las posiciones que en
aquel momento contiene la lista y que resultan descartadas, tanto las posi-
ciones que no son de la lista como aquellas que en algún momento lo han
sido, pero que ya no forman parte de ella.

4.4. Implementación del TAD Lista

La representación más natural para implementar el TAD Lista se obtiene utili- Podéis ver el TAD Cola definido en
el subapartado 3.4 de este módulo
zando una representación encadenada. En un primer momento, podríamos didáctico.

pensar en emplear la misma representación que hemos utilizado para la im-


plementación del TAD Cola. Recordemos que en esta representación los nodos
Versión acotada
tienen un único encadenamiento hacia el siguiente nodo. del TAD Lista

Es posible implementar una


variante acotada del TAD Lista
Siempre que tenemos una representación candidata, la debemos validar imagi- utilizando una representación
con vector, de manera que los
nándonos cómo implementaremos las operaciones del TAD con esta represen- elementos del vector simulen
tación. Revisando las operaciones del TAD Lista, comprobamos los siguientes nodos encadenados. Para más
información, podéis consultar
aspectos: la bibliografía recomendada.
Podéis revisar, por ejemplo,
el capítulo 7 de la obra de
Sahni (2000).
• La operación insertarAntesDe(Posicion<E>,E) necesita acceder a la posición
anterior desde una posición concreta.

• La operación borrar(Posicion<E>) necesita encadenar el elemento anterior


de una posición concreta con la siguiente (para poder, incluso sacando la
posición de la lista, mantener la continuidad de la lista).

Es decir, el conjunto de operaciones que hemos definido para el TAD Lista


requiere que, a partir de una posición, podamos acceder tanto a la posición
anterior como a la siguiente. Una estructura simplemente enlazada como la
del subapartado 3.4 permite acceder en tiempo constante a la siguiente po-
sición. No disponemos, en cambio, de ningún encadenamiento al nodo an-
terior; de manera que, para acceder a éste, sería necesario, en primer lugar, ir
al principio de la lista y recorrerla toda (usando los encadenamientos hacia
el siguiente de los nodos) hasta encontrar el nodo que tenga nuestra posi-
ción como siguiente. Eso tiene un coste lineal respecto al tamaño de la lista.

Como alternativa podemos utilizar una representación encadenada más


completa que disponga también para cada nodo de un encadenamiento al
nodo anterior, aparte del encadenamiento al siguiente. Esta estructura se de-
nomina comúnmente lista doblemente enlazada. En la figura 17 se muestra
cómo se accede al nodo anterior con listas simplemente enlazadas y con lis-
tas doblemente enlazadas.
© FUOC • P06/75001/00577 • Módulo 3 41 Contenedores secuenciales

Figura 17

4.4.1. Definición de la representación

Como toda representación encadenada, habrá dos partes bien diferenciadas,


que son la representación del nodo y la representación de la colección:

• La representación del nodo consistirá en el elemento guardado en el nodo


o una referencia al elemento guardado en el nodo, y dos encadenamientos
–uno hacia el nodo siguiente y otro hacia el anterior.

• La representación de la colección consistirá en dos atributos, que son el nú-


mero de elementos guardados en la lista, y un encadenamiento al último
elemento de ésta.

Recordemos que en la representación encadenada de Cola, la representación Recordad que en el subapartado 3.2.1
ya hemos utilizado una representación
circular para implementar el TAD Cola
de la colección tenía dos encadenamientos: uno al primer elemento y otro al utilizando un vector.

último. Aquí también necesitamos acceso rápido al primer y al último elemen-


to. Por lo tanto, ¿cómo podemos garantizar accesos en tiempo constante al pri-
mero y al último con un único encadenamiento al último? Lo podemos hacer
usando una representación circular. En la representación circular, reaprovecha-
mos el encadenamiento hacia el siguiente del último nodo (que normalmente
debería ser ‘null’) para apuntar al primer nodo. Así pues, tal como se ve en la
figura 18, para acceder al primer nodo únicamente es necesario acceder al si-
guiente del último.
© FUOC • P06/75001/00577 • Módulo 3 42 Contenedores secuenciales

Figura 18

4.4.2. Implementación de las operaciones

La algoritmia necesaria para implementar las operaciones de Lista con la re-


presentación que acabamos de ver es parecida a la que ya hemos visto para
la representación encadenada de Cola, pero añade la necesidad de gestionar
los encadenamientos cuando añadimos o borramos un nodo de una posi-
ción intermedia de la lista. Veamos la descripción de la implementación de
las operaciones más representativas:

colección ListaDoblementeEncadenada<E> implementa Lista<E>

constructor() O(1)
Pone último a ‘null’.
Pone el número de elementos a 0.

Posicion<E> añadirDespuésDe(Posicion<E> pos,E elem) O(1)


Convertimos pos en un NodoDoblementeEncadenado(casting)
Si el nodo es ‘null’
Creamos nodo auxiliar con elem y ‘null’ como siguiente y anterior.
Ponemos como siguiente y anterior el mismo nodo auxiliar (repre-
sentación circular).
Asignamos a último el nodo creado.
Si no
Creamos nodo auxiliar con elem, anterior = nodo (pos) y
siguiente = siguiente de nodo (pos).
Hacemos que el siguiente de nodo (pos) sea el nodo auxiliar creado.
Hacemos que el anterior del siguiente de auxiliar sea el nodo auxiliar
creado.
fsi
si nodo (pos) es el último
Asignamos a último el nodo auxiliar creado.
Incrementamos el número de elementos de la lista.
Devolvemos el nodo creado.
La operación posiciones se comenta
en el apartado siguiente. El resto de
operaciones son parecidas a las descritas,
E borrar(Posicion<E> pos) O(1) y podéis encontrar su implementación
en el código fuente Java disponible como
recurso electrónico.
Convertimos pos en NodoDoblementeEncadenado(casting)
© FUOC • P06/75001/00577 • Módulo 3 43 Contenedores secuenciales

Si el número de elementos es 1
Asignamos último a ‘null’.
Si no
Sea ant el nodo anterior al nodo correspondiente a pos, y seg el siguiente.
Asignamos seg al encadenamiento siguiente de ant.
Asignamos ant al encadenamiento anterior de seg.
Asignamos ‘null’ a los encadenamientos anterior y siguiente del
nodo (pos).
Si el nodo borrado es el último
Asignamos ant al último.
fsi
fsi
Incrementamos el número de elementos de la lista.
Devolvemos el elemento guardado en el nodo borrado.

E reemplazar(Posicion<E> pos,E elem) O(1)


Convertimos pos en NodoDoblementeEncadenado (casting).
Ponemos elem como elemento guardado en el nodo.

En la figura 19 tenéis una representación gráfica del proceso realizado para


añadir un nuevo elemento en una posición determinada de la lista. En primer
lugar, se crea el nodo que guardará el elemento y se inicializa; en segundo lu-
gar, se integra en la lista.

Figura 19
© FUOC • P06/75001/00577 • Módulo 3 44 Contenedores secuenciales

En la figura 20, podéis ver el proceso contrario correspondiente al borrado de


una posición determinada de la lista.

Figura 20

La implementación con listas doblemente enlazadas permite que el coste


Recordad
temporal de todas las operaciones del TAD Lista sea constante (O(1)). Ahora
En el caso de borrado, una vez
bien, está claro que esto tiene el sobrecoste espacial de haber de mantener que el nodo ya no se referencia
desde ningún otro nodo ni va-
dos encadenamientos por nodo. En el caso de que las listas sean largas o riable, puede ser recogido por
el recolector de basura.
que tengamos un número de listas realmente grande, este sobrecoste espa-
cial puede ser relevante.

Por otro lado, no siempre necesitaremos las operaciones insertarAntesDe y bo-


rrar, que son las que necesitan el encadenamiento al anterior para mantener
su coste constante.

En el caso de que estas dos operaciones no sean necesarias y el sobrecoste espa- La misma biblioteca utiliza la
LlstaEncadenada en varias ocasiones
cial sea relevante, podemos utilizar una implementación encadenada basada en para implementar otros TAD.

una lista simplemente enlazada. No detallaremos aquí esta implementación, ya


que es una simplificación de la ya comentada, pero sí que la encontraréis en la
clase ListaEncadenada de la biblioteca de TAD por si necesitáis utilizarla. Las ope-
raciones insertarAntesDe y borrar se proporcionan igualmente, pero en esta im-
plementación tienen un coste lineal.
© FUOC • P06/75001/00577 • Módulo 3 45 Contenedores secuenciales

Tened en cuenta, al revisar las diferentes implementaciones en el código fuen-


te (¡cosa muy recomendable!), que la descripción algorítmica que se ha efec-
tuado con anterioridad de la implementación de las operaciones puede estar
repartida en el código fuente en varios métodos, con el objetivo de minimizar
el código redundante e incrementar su reusabilidad. Adicionalmente, no es
necesario decir que se ha hecho uso de la orientación a objetos, con lo cual la
clase ListaDoblementeEncadenada se ha definido como subclase de ListaEncade-
nada, y reutiliza buena parte del comportamiento definido en esta clase.

Los siguientes bloques de código muestran todo el código correspondiente al


algoritmo de la operación insertarDespuesDe de ListaDoblementeEncadenada.
Hay dos bloques de código diferentes: uno para el comportamiento definido
en ListaEncadenada y otro para la especialización proporcionada en ListaDoble-
menteEncadenada.

ListaEncadenada.java

package uoc.ei.tads;

public class ListaEncadenada<E> implements Lista<E> {


...
public Posicion<E> insertarAlFinal(E elem) {
ultimo = nuevaPosicion(ultimo, elem);
return ultimo;
}

public Posicion<E> insertarDespuesDe(Posicion<E> nodo,E elem) {


Posicion<E> nuevoNodo;
if (ultimo==nodo)
nuevoNodo = insertarAlFinal(elem);
else
nuevoNodo = nuevaPosicion((NodoEncadenado<E>)nodo,elem);
return nuevoNodo;
}

protected NodoEncadenado<E> nuevaPosicion(NodoEncadenado<E> nodo, E elem) {


...
}
...
}

ListaDoblementeEncadenada.java

package uoc.ei.tads;

public class ListaDoblementeEncadenada<E> extends ListaEncadenada<E> {


© FUOC • P06/75001/00577 • Módulo 3 46 Contenedores secuenciales

...
protected NodoEncadenado<E> nuevaPosicion(NodoEncadenado<E> nodo, E elem) {
NodoDoblementeEncadenado<E> nuevoNodo = null;
if (nodo == null) {
nuevoNodo = new NodoDoblementeEncadenado<E>(elem);
nuevoNodo.setSiguiente(nuevoNodo);
nuevoNodo.setAnterior(nuevoNodo);
ultimo = nuevoNodo;
}
else {
NodoDoblementeEncadenado<E> actual = (NodoDoblementeEncadenado<E>)nodo;
NodoDoblementeEncadenado<E> siguiente =
(NodeDoblementeEncadenado<E>)actual.getSiguiente();
nuevoNodo = new
NodoDoblementeEncadenado<E>(siguiente,elem,
(NodoDoblementeEncadenado<E>)nodo);
nodo.setSiguiente(nuevoNodo);
siguiente.setAnterior(nuevoNodo);
}
n++;
return nuevoNodo;
}
}

La operación insertarDespuesDe se define en la clase ListaEncadenada; pero tanto la


creación del nodo como la gestión de los encadenamientos se realiza en el méto-
do auxiliar nuevaPosicion, que está redefinido en ListaDoblementeEncadenada.

4.5. Recorrido de los elementos de un contenedor: TAD Iterador

Una de las construcciones más usadas en la programación estructurada es el re- En la asignatura Fundamentos de
programación se estudian los
esquemas de recorrido y de búsqueda.
corrido de secuencias de elementos. De otras asignaturas ya conocemos dos al-
goritmos básicos para el tratamiento secuencial: el esquema de recorrido y el de
búsqueda.

En esta asignatura estudiamos principalmente dos cosas: por un lado, diferen-


tes modos de presentar colecciones de elementos en forma de TAD y, por otro,
cómo usar los TAD resultantes sin necesidad de preocuparse por el modo
como han sido implementados.

Una de las operaciones que querremos hacer más habitualmente sobre una co- TAD posicional
lección será recorrer sus elementos. El TAD Recorrido nos permite recorrer las Un TAD posicional es aquel
que ofrece la posibilidad de
posiciones de una colección posicional como es Lista (y a partir de las posicio- realizar operaciones a partir
nes podemos acceder a los elementos que están almacenados en ella). Pero no de un parámetro que indica
la posición.
todas las colecciones son posicionales (por ejemplo, Pila y Cola no lo son); y
© FUOC • P06/75001/00577 • Módulo 3 47 Contenedores secuenciales

muchas veces nos convendrá también recurrir a los elementos de este tipo de
colecciones. Por otro lado, muchas veces estaremos interesados en acceder
únicamente a los elementos (en lugar de acceder a la posición).

Por todas estas cuestiones, resulta muy útil que las bibliotecas de colecciones
como la de la asignatura proporcionen alguna forma de recorrer los elemen-
tos de una colección. Una manera muy habitual y flexible de hacerlo es me-
diante una abstracción independiente de la colección equivalente al TAD
Recorrido, pero que únicamente ofrecezca acceso a los elementos y “oculte”
cualquier información posicional.

El TAD Iterador nos permitirá recorrer cualquier colección de una manera total-
mente homogénea; sin necesidad de saber qué tipo de colección es. Evidente-
mente, el conocimiento sobre el tipo de colección estará contenido en cada
implementación de Iterador; y dispondremos de una por cada tipo de colección.

tipo Iterador<E> es

booleano haySiguiente()
@pre Cierto
@post Devuelve ‘cierto’ si hay siguiente elemento, y ‘falso’, en el caso
contrario.

E siguiente()
Devuelve el siguiente elemento del recorrido. El primer elemento del re-
corrido nos lo devuelve la primera llamada a esta operación.
@pre haySiguiente()
@post Devuelve el siguiente elemento del recorrido.

Podemos crear un iterador para recorrer una colección a partir del método elemen- Recordad que la clase Contenedor es la
raíz de la jerarquía de colecciones de
tos() definido en el TAD abstracto Contenedor e implementado por cada una de las la biblioteca de colecciones de la
asignatura.
diferentes implementaciones de la colección. Esta operación, a pesar de que no
habíamos hecho casi referencia a ella hasta ahora, está disponible para todas las
colecciones de la biblioteca de TAD de la asignatura.

Se debe tener en cuenta que puede ser muy peligroso modificar una colección
mientras se está recorriendo con un iterador. Concretamente, borrar la posi-
ción actual de un recorrido provocará muy probablemente una situación in-
coherente entre el iterador con el que se realiza el recorrido y la colección.

Una manera de evitar estos problemas es hacer que los iteradores recorran los ele-
mentos de una colección en un momento dado; es decir, hacer una “foto” de la
colección. Esta “foto” se toma normalmente en el momento de creación del ite-
rador, de modo que quede desvinculado de la representación de la colección y,
así, que ésta se pueda modificar sin problemas. Hacer esta “foto”, evidentemente,
consume recursos tanto temporales como espaciales.

Otra posibilidad para evitar el problema es sincronizar una colección con sus
iteradores, de manera que la colección “notifique” las modificaciones que se
© FUOC • P06/75001/00577 • Módulo 3 48 Contenedores secuenciales

van produciendo. Esta solución añade complejidad al diseño de la biblioteca


y, además, puede suponer un coste temporal importante si el número de ite-
radores de una colección es grande.

Las dos soluciones anteriores suponen un coste añadido que no necesitaremos en


la mayoría de los casos. Por ello, gran parte de las bibliotecas de colecciones (y en-
tre éstas, la biblioteca de la asignatura) dejan la responsabilidad de realizar modi-
ficaciones en una colección cuando ésta se está iterando a los usuarios de la
biblioteca.

En el caso de que sea necesario hacer modificaciones en la colección mientras En el módulo “Diseño de estructura
de datos” se profundiza en el diseño
se está recorriendo, la orientación a objetos ofrece al usuario de la biblioteca he- de las bibliotecas de colecciones y se trata
la extensibilidad, entre otros aspectos.
rramientas para implementar fácilmente alguno de los mecanismos ya comen-
tados. Esto será posible siempre y cuando la biblioteca se haya diseñado de
manera que se pueda extender fácilmente.

4.5.1. Implementación

Veamos a continuación un ejemplo de implementación del TAD Iterador para


una colección, una ListaEncadenada. La implementación está hecha sobre una
clase denominada ListaEncadenadaConIterador que es subclase de ListaEncade-
nada y que encontraréis como recurso electrónico en los ejemplos del módulo.
Si lo hacemos así es porque no queremos modificar la clase ListaEncadenada de
la biblioteca de TAD de la asignatura, que usa una implementación de Iterador
muy genérica basada en el TAD Recorrido (y que es reutilizada también por
otras colecciones). Como es el primer contacto con el concepto de iterador, se
ha preferido partir de una versión totalmente didáctica.

A continuación, comentamos algunos aspectos de esta implementación:

• Al tratarse de una implementación de un TAD auxiliar, y como la idea es Clase interna en Java
obtener una instancia siempre a partir de un objeto de tipo colección (mé-
Una clase interna (o inner class)
todo elementos()), hemos “ocultado” la clase que implementa Iterador. He- es una clase que está
definida dentro de otra clase y
mos hecho esto definiendo IteradorLista como una clase interna (inner de la que es miembro, igual
que los métodos o atributos
class) protegida dentro mismo de ListaEncadenadaConIterador. que la última tenga definidos.
Las reglas de visibilidad de la
clase interna son las mismas
• Cuando se define el método elementos() de la colección, se crea una ins-
que para los otros miembros.
tancia de IteradorLista. Esta instancia se ha creado utilizando el construc- De esta manera, si en una clase
A definimos una clase interna
tor de éste (visible dentro de la implementación de la colección). protegida B, ésta sólo estará
disponible dentro de A y de
sus subclases.
• El método toString de la colección se ha redefinido como ejemplo de uso
del TAD Iterador.

• La implementación del iterador tiene un estado definido por el atributo si-


guiente, que nos indica el elemento siguiente que es necesario visitar. Dado
que ListaEncadenada utiliza una representación circular, el iterador necesita
conocer también el último elemento, de manera que cuando se “visite” el
último elemento sabemos que no debemos “visitar” ninguno más (cuando
© FUOC • P06/75001/00577 • Módulo 3 49 Contenedores secuenciales

lo hemos visitado ponemos siguiente en ‘null’ de manera que haySiguiente


sepa que ya se han visitado todos los elementos).

ListaEncadenadaConIterador.java

package uoc.ei.ejemplos.modulo3.lista;
import ...
public class ListaEncadenadaConIterador<E>
extends ListaEncadenada<E> {

public Iterador<E> elementos() {


return new IteradorLista<E>(this);
}

public String toString() {


StringBuffer sb = new StringBuffer();
Iterador<E> iter = elementos();
while (iter.haySiguiente()) {
sb.append(iter.siguiente());
if (iter.haySiguiente())
sb.append(", ");
}
return sb.toString();
}

protected static class IteradorLista<EI>


implements Iterador<EI> {
private NodoEncadenado<EI> ultimo;
private NodoEncadenado<EI> siguiente;

IteradorLista(ListaEncadenadaConIterador<EI> ll) {
this.ultimo = ll.ultimo;
if (ultimo! = null)
siguiente = ultimo.getSiguiente();
}

public boolean haySiguiente() {


return siguiente! = null;
}

public EI siguiente() throws ExcepcionPosicionInvalida {


NodoEncadenado<EI> aux = siguiente;
siguiente = siguiente==ultimo ?
null : siguiente.getSiguiente();
return aux.getElem();
}
}
}
© FUOC • P06/75001/00577 • Módulo 3 50 Contenedores secuenciales

4.6. Ejemplo de uso del TAD Lista

Los contenedores secuenciales son muy versátiles y se pueden utilizar en mul-


titud de situaciones diferentes en la programación de aplicaciones. Principal-
mente –sobre todo en el caso de la lista–, se utiliza para ayudar a implementar
una colección de nivel de abstracción más alto. A continuación, veremos un
ejemplo y comprobaremos cómo la reutilización puede hacer que la imple-
mentación dé lugar, en algunos casos, a una cantidad muy reducida de código.

Ya hemos utilizado el TAD Conjunto como ejemplo para introducir el concepto de Podéis ver el TAD Conjunto definido
en el módulo “Tipos abstractos de
datos” de esta asignatura.
TAD, y el formato que usaríamos a lo largo de este texto para definirlos. Antes he-
mos dado una implementación basada en una representación con vector. Por lo
tanto, podemos dar una implementación basada en este tipo de representaciones.

Pero, de hecho, podemos ir un poco más allá: no es necesario volver a definir


un nuevo tipo de nodo, ni volver a programar el código que nos gestione los
encadenamientos cuando debamos añadir o borrar nodos. Podemos reutilizar
una de las implementaciones del TAD Lista.

Así pues, veamos cómo podemos implementar el mismo TAD Conjunto con las
operaciones insertar, esta y borrar, utilizando el TAD Lista.

El TAD Lista nos servirá para almacenar los elementos del conjunto, de mane-
ra que únicamente deberemos implementar las operaciones de Conjunto en
función de las operaciones de Lista. En ningún caso, es necesario detallar
cómo se representa la lista; ésta es una tarea que ya se ha hecho en las diferen-
tes implementaciones del TAD Lista de las que ya disponemos. a

Una cosa importante que debemos decidir es qué implementación del


TAD Lista usaremos. Para ello, es necesario estudiar si necesitamos usar
las operaciones insertarAntesDe o borrar de Lista o bien podemos imple-
mentar las operaciones de Conjunto sin usarlas.

Si es necesario utilizar alguna de estas dos operaciones de Lista, será mejor uti-
lizar ListaDoblementeEncadenada, ya que ofrece tiempo constante para estas
operaciones. Si no es necesario emplearlas, será mejor ListaEncadenada, ya que
ofrece tiempo constante en el resto de operaciones y sólo tiene un encadena-
miento por nodo.

Veamos cómo podemos traducir las operaciones de Conjunto en operaciones


de Lista:

• insertar(E elem). En el caso de que elem no aparezca en ésta (llama a esTa),


añadimos elem al final de la lista.

• esta(E elem). Creamos un iterador de los elementos de la lista (llama a la


operación elemento de Lista). Hacemos una lista de elem. Si lo hemos en-
contrado devolvemos ‘cierto’ y, si no, ‘falso’.
© FUOC • P06/75001/00577 • Módulo 3 51 Contenedores secuenciales

• borrar(E elem). Creamos un recorrido (Lista.posiciones) de las posiciones de


la lista. Buscamos una posición que almacene el elemento. Si la encontra-
mos, la borramos mediante Lista.borra.

La implementación que acabamos de describir emplea la operación borra de Lis-


ta. Pero con una implementación un poco más inteligente de la operación borra
de Conjunto lo podemos evitar: únicamente es necesario tener la precaución,
mientras se busca la posición, de ir guardando la posición anterior. Si la búsque-
da acaba con éxito, usaremos Lista.borrarSiguiente sobre la posición anterior en
lugar de Lista.borrar sobre la posición encontrada.

En el siguiente bloque de código, podéis comprobar cómo la implementación


queda mucho más reducida.

ConjuntoListaImpl.java

package uoc.ei.ejemplos.modulo3.lista;
import ...
public class ConjuntoListaImpl<E> implements Conjunto<E> {
private Lista<E> listaDeElementos;

public ConjuntoListaImpl() {
listaDeElementos = new ListaEncadenada<E>();
}

public void insertar(E elem) {


if (!esta(elem))
listaDeElementos.insertarAlFinal(elem);
}

public boolean esta(E elem) {


boolean encontrado = falso;
Iterador<E> iter = listaDeElementos.elementos();
while (!encontrado && iter.haySiguiente())
encontrado = elem.equals(iter.siguiente());
return encontrado;
}

public E borrar(E elem) {


E elementoBorrado = null;
boolean encontrado = false;
Recorrido<E> rec = listaDeElementos.posiciones();
Posicion<E> anterior = null,actual = null;
while (!encontrado && rec.haySiguiente()) {
anterior = actual;
actual = rec.siguiente();
encontrado = actual!= null && elem.equals(actual.getElem());
}
© FUOC • P06/75001/00577 • Módulo 3 52 Contenedores secuenciales

if (encontrado)
elementoBorrado = listaDeElementos.borrarSiguiente(anterior);
return elementoBorrado;
}
}

Como podéis comprobar, los algoritmos de esta implementación son sencillos Podéis ver los esquemas algorítmicos
de recorrido y búsqueda en la
y claros. Se basan principalmente en el uso de las operaciones proporcionadas asignatura Fundamentos de programación.

por el TAD Lista y los TAD auxiliares Posicion, Recorrido e Iterador, combinadas
con los esquemas algorítmicos de recorrido y búsqueda que ya conocéis.

El uso de bibliotecas de colecciones como la de la asignatura (y de hecho, de


bibliotecas en general) potencia la capacidad de reutilización e incrementa la
productividad y la fiabilidad, al evitar que los desarrolladores deban reinven-
tar cada vez representaciones parecidas y hayan de codificar la gestión de to-
dos los elementos que intervienen en éstas.
© FUOC • P06/75001/00577 • Módulo 3 53 Contenedores secuenciales

5. Representaciones con vector: redimensionamiento

Un inconveniente importante de las representaciones con vector es que es ne-


cesario dar un tamaño del vector nada más empezar. Esto tiene dos consecuen-
cias poco deseables: por un lado, necesitamos conocer el máximo número de
elementos que se guardarán en la colección; y, por otro, si el número de elemen-
tos que existe en la colección está alejado de este máximo, estaremos desapro-
vechando mucho espacio.

Existe una estrategia que, a pesar de que trabaja con representaciones con vec-
tor, nos permite ir adaptando el número de elementos guardados en la colec- No todos los lenguajes de
programación permiten esta
ción. Consiste en crear un vector más grande cuando el vector esté lleno (y se solución. Java la permite.
quiera introducir un nuevo elemento), y traspasarle todos los elementos, para
finalmente descartar el vector antiguo y continuar trabajando con el nuevo. El
redimensionamiento se realiza de manera totalmente transparente para el usua-
rio de la colección. Las representaciones que usan esta técnica se denominan
vectores extensibles (extendable array). En la figura 21 podemos ver de manera es-
quemática los tres pasos necesarios para el redimensionamiento del vector.

Figura 21

Ahora bien, el redimensionamiento del vector tiene un coste lineal sobre el


número de elementos de la colección, cosa que puede tener un efecto sobre
el coste de las operaciones del TAD. Por ejemplo, podemos aplicar la técnica
en el TAD Cola, proporcionando una implementación alternativa en Cola-
Podéis ver la implementación de
VectorImpl que utilice un vector extensible. Podemos partir de la implementa- ColaVectorImpl en el apartado 3 de
este módulo didáctico.
ción de ColaVectorImpl y modificarla ligeramente.

En primer lugar, es necesario decidir dónde hacer el redimensionamiento. En


principio, sólo habrá que hacer más grande el vector cuando añadimos ele-
mentos a la colección, cosa que en este caso se efectúa en la operación encolar.
© FUOC • P06/75001/00577 • Módulo 3 54 Contenedores secuenciales

En la implementación de encolar deberemos añadir una sentencia condicional


que redimensione el vector si está lleno.

Pero el coste de la operación de encolar era constante; y ahora, con el redimensio-


namiento, pasa a ser lineal. Eso sí, la operación de redimensionamiento no se eje-
cutará cada vez que encolemos un elemento. El objetivo es que cuando el vector
se redimensione, se haga de tal modo que se pueda encolar un número razonable
de elementos sin necesidad de volverlo a redimensionar.

Así pues, la mayoría de las ocasiones en las que se ejecute la operación de en-
colar se hará con un coste constante; mientras que, de vez en cuando, se hará
con coste lineal. Mentimos si decimos que encolar tiene coste constante (su
coste ha pasado a ser lineal). Pero si decimos simplemente que su coste es li-
neal, aun diciendo la verdad, daremos una imagen equivocada de nuestra im-
plementación a los usuarios potenciales.

Por ello existe la noción de coste amortizado/amortización de coste, que


nos puede ser útil para expresar mejor el coste de una operación siempre
que nos encontremos con esta situación en la que muchas ejecuciones
tienen un coste más bajo, y algunas aisladas tienen un coste superior.

La amortización del coste consiste en repartir la suma de los costes de un La amortización del coste
conjunto de ejecuciones entre todas estas ejecuciones. De esta manera, en la
El concepto de amortización
operación de encolar conseguiremos repartir el coste lineal del redimensiona- del coste es similar al de amorti-
zación de capital, en el que re-
miento entre las otras ejecuciones (de coste constante). Si después de realizar partimos el capital gastado en
este reparto, el coste resultante de las ejecuciones de encolar se mantiene cons- una compra entre un conjunto
de años durante los cuales usa-
tante, podremos afirmar que encolar tiene un coste amortizado constante mos el objeto comprado.

(O(1)). Pero antes de realizar tal afirmación, será necesario asegurarse matemá-
ticamente de que el coste se mantiene constante. Veamos de qué manera, para
el caso concreto de encolar.

El coste de una operación de redimensionamiento es lineal; es decir, O(n).


Y eso significa que, en una operación de redimensionamiento, se hacen k
× n operaciones simples (para alguna constante k). El coste de una opera-
ción de encolar sin redimensionamiento es O(1), por lo tanto, se trata de k’
operaciones simples (para alguna constante k’).

Para estar completamente seguros de que podemos repartir las k × n operaciones


Equivalencia
del redimensionamiento entre los elementos encolados con coste constante y
Decir que la operación de en-
mantener este coste constante, es necesario que tengamos como mínimo n ope- colar tiene un coste amortizado
raciones de encolar sin redimensionamiento para cada una con redimensiona- O(1) es equivalente a decir que
ejecutar n veces la operación
miento. De este modo, tal como aparecía en la figura 22, podemos acumular en tiene un coste O(n).

cada operación de encolar con coste constante (k’ operaciones básicas) otra cons-
tante (k operaciones básicas), con lo cual su coste será k + k’. Ambas son constantes
independientes del tamaño de los datos. Por lo tanto, seguimos teniendo coste
constante. Es decir, hemos asimilado un redimensionamiento con coste lineal
con n operaciones de encolar de coste constante. a
© FUOC • P06/75001/00577 • Módulo 3 55 Contenedores secuenciales

Figura 22

Para conseguir que haya n operaciones de encolar sin redimensionamiento para


cada una con redimensionamiento, es necesario que el redimensionamiento du-
plique como mínimo la medida del vector. Con ello, quedarán n posiciones del
nuevo vector libres, que garantizarán un mínimo de n elementos encolados adi-
cionales sin redimensionamiento.

Por lo tanto, podremos decir que el coste amortizado de encolar es constante


en una implementación con vector extensible siempre que cada redimensio-
namento duplique la medida del vector. a
Actividad

Razonad por qué, en realidad, es suficiente con garantizar que el número de posiciones
libres después del redimensionamiento sea proporcional a n.

A continuación tenéis un fragmento de la implementación de Cola que usa un


vector extensible en el que se muestra el método encolar modificado y el mé-
todo de redimensionar:

ColaRedimensionableImpl.java

package uoc.ei.ejemplos.modulo3.redimensionamiento;
import ...
public class ColaRedimensionableImpl<E> implements Cola<E>{
...
public void encolar(E elem) {
if (estaLleno())
redimensionar();
int ultimo = posicion(primero + n);
elementos[ultimo] = elem;
n++;
}
© FUOC • P06/75001/00577 • Módulo 3 56 Contenedores secuenciales

private void redimensionar() {


// creación del nuevo vector
// (con el doble de capacidad)
E[] auxElementos =
(E[])new Object[elementos.length*2];
// copia de los elementos de uno al otro
Iterador<E> it = elementos();
int i = 0;
while (it.haySiguiente()) {
auxElementos[i] = it.siguiente();
i++;
}
// sustitución del vector antiguo lleno
// por el nuevo con más capacidad
primero = 0;
elementos = auxElementos;
}
}
© FUOC • P06/75001/00577 • Módulo 3 57 Contenedores secuenciales

6. Los contenedores secuenciales en la Java Collections


Framework

Aunque nuestra asignatura dispone de su propia biblioteca de TAD, hay mu-


chas más bibliotecas, cada una con sus peculiaridades. Una de las más usadas
es la Java Collections Framework (JCF), que forma parte del JDK desde la ver-
sión 1.2 (éste es el motivo de que sea tan popular). A pesar de no tratarse, prin-
cipalmente por motivos didácticos, de la biblioteca que utilizaremos en la
asignatura, es interesante comentarla desde la perspectiva de su uso. Por un lado,
Comentario sobre la JCF
será un recurso importante para todos aquellos que continuéis desarrollando
Los comentarios de este apar-
aplicaciones en lenguaje Java. Y, por otro, tenéis la oportunidad de ver otra bi- tado tenderán a ser breves y a
blioteca de colecciones, con otro diseño. dar una idea general de las cla-
ses de la JCF. En ningún caso
intentarán ser una guía de ayu-
Por este motivo, al final de cada uno de los módulos en los que introducimos da a la programación con la
JCF. Para ello, podéis recurrir a
nuevos tipos de colección, haremos un comentario sobre las clases e interfaces la documentación Javadoc del
JDK.
equivalentes en la JCF. En este módulo, comenzaremos con los contenedores
secuenciales.

La JCF utiliza la dualidad interfaz/clase ya comentada. Así que hablaremos, en La dualidad interfaz/clase de la JCF
se comenta en el módulo “Tipos
abstractos de datos” de esta asignatura.
primer lugar, de las interfaces relacionadas con las colecciones secuenciales y,
después, de las implementaciones ofertadas.

Todas las interfaces que representan colecciones extienden una interfaz raíz
denominada Collection, que proporciona un conjunto de métodos moderada-
mente extensos que permiten:

• Añadir un elemento aislado o un conjunto de elementos en la colección.


• Borrar un elemento concreto, un conjunto de ellos, o todos los de la colección.
• Obtener un iterador para recorrer todos los elementos de la colección.
• Consultar si hay un elemento o un conjunto de elementos.
• Consultar si la colección está vacía.
• Saber el número de elementos de la colección.
• Copiar todos los elementos de la colección en un vector (Object[]).

Algunas de estas operaciones son opcionales (podéis consultar el Javadoc). Esto


significa que una implementación concreta para un tipo de colección puede im-
plementar la operación o no. En el caso de que la operación no esté implemen-
tada, se suele lanzar una excepción de tipo OperationNotSupportedException.

La interfaz Collection de la JCF ofrece muchas más operaciones que la interfaz Con-
tenedor de nuestra biblioteca. Esto será una constante para todas las interfaces co-
mentadas en este apartado a lo largo de los diferentes módulos. Una de las
decisiones de diseño más apreciables en la JCF ha sido la de ofrecer muchos mé-
todos si se consideraban útiles (ante la compacidad y simplicidad de la biblioteca
de TAD de la asignatura, en la que hemos intentado compaginar la usabilidad con
la didáctica).
© FUOC • P06/75001/00577 • Módulo 3 58 Contenedores secuenciales

Una interfaz básica en la JCF es Iterator, equivalente a nuestro Iterador. Esta in-
terfaz, aparte de los dos métodos necesarios para realizar el recorrido de elemen-
tos (que aquí se llaman hasNext y next), proporciona una operación opcional (no
todas las colecciones la soportan) que permite borrar de la colección el elemento
actual.

La interfaz básica para trabajar con contenedores secuenciales es List. Aparte


Interfaces en la JCF
de las operaciones ya comentadas para Collection, proporciona operaciones
No mencionamos interfaces
que permiten mapear elementos en posiciones indexadas para un entero (co- para los TAD Pila y Cola, ya que
la JCF no tiene ninguno. Ofrece
mo si se tratase de un vector). Podemos obtener el elemento almacenado en únicamente la interfaz List.
una posición determinada, o la primera o la última posición en la que se al-
macena un elemento. También podemos obtener una sublista a partir de un
intervalo de dos posiciones. Vale la pena remarcar que, en todas estas opera-
ciones, las posiciones se especifican con enteros, y no a partir de un TAD auxi-
liar Posicion como en la biblioteca de la asignatura.

Aparte de obtener un Iterator para recorrer los elementos de la lista, List ofrece
la posibilidad de obtener un ListIterator. Esta interfaz es una extensión de Ite-
rator especial para las listas que permite trabajar de una manera más posicional
(aunque sin trabajar de manera explícita con posiciones). ListIterator permite
hacer recorridos en las dos direcciones (con previous y next), y ofrece una serie
de operaciones que permiten añadir un elemento antes del elemento actual, y
borrar éste o modificarlo.

Todas las implementaciones de colecciones de la JCF tienen como clase raíz Abs-
Convenciones
tractCollection. De la misma manera, hay otras clases que comienzan por Abs-
El hecho de que estas clases
tract (en el ámbito de este módulo: AbstractList y AbstractSequentialList). Estas abstractas comiencen por la
clases son abstractas y no implementan ninguna colección concreta. Su objeti- palabra Abstract es una con-
vención seguida por los di-
vo es implementar de manera general todo el comportamiento que puedan, evi- señadores de la JCF, pero no
por el mundo Java en general.
tando recodificar todo este código en las implementaciones concretas. Es decir,
sencillamente, utilizar la orientación a objetos (generalización + abstracción).

Las implementaciones de colecciones secuenciales más usadas proporcionadas


en la JCF son:

• ArrayList. Se trata de una implementación de List basada en un vector ex-


tensible.

• LinkedList. Implementación de List realizada con una lista doblemente enla-


zada. Aparte de las operaciones de List, proporciona operaciones para consul-
tar, borrar y añadir elementos a cualquiera de los dos extremos de la lista. Esto Podéis ver el anexo de este módulo.

permite utilizar una LinkedList como una cola, una pila o una doble cola.
Compatibilidad del JDK
• Vector. Implementación de un vector extensible. Clase mantenida por ra-
El hecho de que el JDK tenga
zones históricas. A partir de la versión 1.2 del JDK se modificó para imple- una historia de varios años
provoca que se mantengan
mentar List. Aparte de los métodos de esta interfaz, proporciona unos clases y métodos por razones
de compatibilidad (si no, las
cuantos más, heredados desde sus principios. aplicaciones antiguas dejarían
de funcionar con las nuevas
versiones de JDK).
• Stack. ExtiendeVector con las 5 operaciones del TAD Pila.
© FUOC • P06/75001/00577 • Módulo 3 59 Contenedores secuenciales

Resumen

En este módulo hemos presentado las colecciones secuenciales básicas: Pila,


Cola y Lista. Estas tres colecciones organizan los elementos secuencialmente,
pero cada una permite acceder a ellas de una manera distinta. Este hecho di-
ferencia su comportamiento, y hace que cada una sea aplicable a un conjunto
de situaciones también diferenciado.

Se ha introducido el concepto de representación encadenada, con todos los


elementos necesarios para entenderla e implementarla en el lenguaje usado en
este texto. Se han presentado los elementos básicos para la gestión de la me-
moria (apuntador/referencia, posición en memoria y liberación de memoria)
y se ha visto cómo están representados en el lenguaje usado en este texto. En
relación con todo esto, se ha efectuado una introducción del sistema que uti-
liza Java y otros lenguajes para gestionar la memoria.

Por lo que respecta a las implementaciones, se han estudiado las implementa-


ciones con vector clásicas para Pila y Cola. En la introducción de las represen-
taciones encadenadas se ha empleado como ejemplo una implementación
encadenada de Cola. Esto también nos ha servido para tener un ejemplo de
TAD con dos implementaciones (aparte del ejemplo de juguete de los natura-
les en el módulo “Tipos abstractos de datos”).

Posteriormente, se ha utilizado una representación encadenada para imple-


mentar el TAD Lista. Tanto la representación de este TAD como su implemen-
tación han servido para introducir elementos básicos en el modelo de
colección que utilizaremos en este curso y en la biblioteca de colecciones de la
asignatura: los conceptos de posición, recorrido e iterador.

En el apartado 5 del módulo se explica el redimensionamiento dinámico de vec-


tores, una técnica posible en el lenguaje Java y que permite eliminar la carencia
más importante de las representaciones con vector: la necesidad de conocer de
entrada el número de elementos máximo que guardar en una colección. Esta
técnica implica la realización de una operación de redimensionamiento que
puede ser muy costosa y malgasta el coste asintótico en el peor de los casos (O)
de las operaciones del TAD afectadas. Esto, sin embargo, no refleja la realidad de
que esta operación de redimensionamiento sólo se realiza de vez en cuando. En
estas situaciones es mucho más útil hablar de coste amortizado, que también se
explica en este apartado.

Para acabar, se describe brevemente el conjunto de clases que corresponden a


colecciones secuenciales de la Java Collections Framework, incluida en el JDK.
© FUOC • P06/75001/00577 • Módulo 3 61 Contenedores secuenciales

Ejercicios de autoevaluación

1. Proporcionad una implementación con vector de la colección Pila parecida a la propor-


cionada en el apartado 2, pero con la diferencia de que el elemento más antiguo de la pila
esté en la posición N del vector (siendo N el último elemento del vector).

2. Imaginad la siguiente situación: disponéis de dos bobinas de 50 DVD cada una. Una de
ellas la usáis para guardar películas que compráis en poco espacio. La otra la usáis como
bobina auxiliar, de manera que cuando buscáis un título vais cogiendo DVD de la prime-
ra bobina uno a uno; y los ponéis en la segunda bobina. Así, hasta que encontréis el título
buscado. Desarrollad o contestad los siguientes puntos:
a) ¿Qué TAD de los vistos es el más adecuado para representar cada una de las bobinas de
DVD?
b) ¿Podéis proporcionar una representación ad hoc para este problema, de manera que
minimicemos el espacio libre desaprovechado en una representación con vector? En caso
afirmativo, definidla.
c) Definid un TAD DobleBobina y decidid qué operaciones son necesarias para implemen-
tar el algoritmo de búsqueda de un DVD descrito al principio del enunciado. Implemen-
tad tanto las operaciones, como el algoritmo mencionado.

3. Implementad el TAD Pila utilizando una representación encadenada (de un modo pa-
recido a como se hace en el subapartado 3.4 con el TAD Cola). Una vez hecho esto,
proporcionad una implementación equivalente empleando la clase ListaEncadenada
de la biblioteca de TAD de la asignatura. Comparad las dos implementaciones. Co-
mentad cuál preferís y por qué.

4. Pretendemos representar expresiones matemáticas. Las expresiones que queremos repre-


sentar consisten en dos operandos que pueden ser o bien constantes reales o bien otras ex-
presiones, y un operador de entre los cuatro básicos: +, –, × y ÷. Podéis asumir un tamaño
máximo en cuanto al número de elementos (operadores + operandos) de una expresión.
a) Decid qué TAD de los vistos en el módulo es el más adecuado para representar las
expresiones.
b) ¿Qué problema tendríais si no pudieráis asumir un tamaño máximo en el número de-
elementos de la expresión? ¿Cómo lo solucionaríais?
c) ¿Cuál es el modo más práctico de representar las expresiones mediante este TAD?
d) Definid un algoritmo que, a partir de una expresión almacenada en una instancia del
TAD elegido, la evalúe y devuelva el resultado.

5. Implementad la colección lista de modo que el espacio libre sea gestionado por la misma im-
plementación. La implementación que proporcionéis puede ser acotada (implementad la in-
terfaz ContenedorAcotado). Pista: podéis gestionar el espacio libre como una pila de nodos.
Un vez hecho esto, comparad (y medid) el tiempo utilizado en las operaciones de vuestra
implementación de lista ante la implementación proporcionada en la biblioteca de TAD
de la asignatura. ¿Qué implementación es más eficiente? Razonad el motivo.

6. Una cadena de supermercados quiere diseñar un sistema que a partir de un cliente que
está a punto de pagar, decide de modo automático la cola en la que se debe poner. Cada
supermercado de la cadena dispone de una caja rápida (sólo para clientes con 10 elemen-
tos como máximo) y N cajas en las que los clientes pueden pagar cualquier número de
elementos.
Se os pide que defináis e implementéis un TAD denominado ColaSupermercado con las
siguientes operaciones:
• void clienteEnEspera(int numCliente). A partir de un cliente identificado con un en-
tero, lo encola en el sistema seleccionando la caja en la que el cliente se debe esperar.
• booleano hayClienteDisponible(int numCaja). Devuelve cierto si hay un cliente es-
perando ser atendido en la caja indicada.
• int atenderCliente(int numCaja). Desencola un cliente para una caja determinada. Se
ejecutará cuando la cajera de la caja en cuestión pueda atender un nuevo cliente.

7. Contestad a las siguientes preguntas:


a) ¿Es el concepto de posición exclusivo de TAD implementados mediante representacio-
nes encadenadas?
b) ¿Por qué no es aplicable el concepto de posición en los TAD Pila y Cola?
c) A partir de las respuestas anteriores, tiene sentido definir un TAD Vector que represente
un vector de elementos y que utilice posiciones?
d) En el caso de que hayáis contestado afirmativamente en el apartado c, proponed un
mínimo de dos operaciones en las que podría ser útil el concepto de posición.
e) En el caso de que hayáis contestado afirmativamente en el apartado c, proponed una
representación del TAD Vector en Java y proporcionad una implementación del TAD Po-
sicion ad hoc.
© FUOC • P06/75001/00577 • Módulo 3 62 Contenedores secuenciales

8. Resolved los siguientes apartados:


a) Implementad un algoritmo en Java que, a partir de una instancia del TAD Lista<int>,
ordene los elementos de más pequeño a más grande. Emplead únicamente recorridos y
las operaciones posicionales del TAD.
b) ¿Qué coste tiene el algoritmo implementado?
c) ¿Es posible proporcionar un algoritmo más eficiente sin utilizar ninguna estructura de
datos adicional?
d) Adaptad el algoritmo proporcionado para que sirva para listas en las que los elementos
puedan ser cualquier clase que implemente la interfaz Comparable que proporciona el JDK.

9. Queremos proporcionar una implementación del TAD Conjunto presentado en el módulo


“Tipos abstractos de datos” que utilice como representación una de las colecciones se-
cuenciales de la biblioteca de clases de la asignatura.
a) ¿Qué colección os parece más oportuna y por qué?
b) ¿Qué coste tiene cada una de las operaciones?
c) Definid e implementad una nueva operación en el TAD Conjunto denominada unión que re-
ciba dos conjuntos como parámetro, y devuelva un Conjunto que sea su unión sin modificar
los dos conjuntos de entrada. ¿Qué coste tiene esta operación? ¿Hay algún modo de mejorarlo?

10. Contestad a los siguientes apartados:


a) Explicad con vuestras palabras la diferencia entre los TAD Recorrido e Iterador (si es que
existe alguna).
b) Proporcionad una implementación del TAD Iterador para la representación del TAD
Vector que habéis definido en el apartado e del ejercicio 7.
c) Proporcionad una implementación del TAD Recorrido para la misma representación
del TAD Vector. ¿Podéis usar la delegación para reaprovechar la implementación de Itera-
dor proporcionada en el apartado b?
d) ¿Sería posible utiltzar delegación en el sentido contrario (implementar Iterador basán-
dose en la implementación de Recorrido)? Pista: examinad las implementaciones de Itera-
dor que se definen en la biblioteca de TAD de la asignatura.

11. Una empresa dispone de un número N de trabajadores de mantenimiento. Estos trabaja-


dores reciben tareas que realizar concretas (cambiar luces, instalar enchufes, abrir tabi-
ques, etc.). Cuando un encargo llega al departamento de mantenimiento, el jefe del
departamento valora el esfuerzo en intervalos de 15 minutos, y lo asigna a un trabajador
que no esté realizando ningún otro. Si todos los trabajadores están trabajando, lo asigna
a aquel trabajador que, según la valoración del trabajo, quedará libre antes.
a) Diseñad un TAD que automatice el trabajo del jefe del departamento de mantenimien-
to. Definid operaciones de modo que el jefe de mantenimiento pueda asignar automáti-
camente las tareas que le lleguen. (Por diseñar un TAD, entendemos únicamente definir
sus operaciones y su comportamiento mediante la especificiación).
b) Proponed una representación para implemantar este TAD. ¿Podéis reutilizar algunos
de los TAD estudiados en este módulo? Si es así, ¿cuáles y cómo?
c) Proporcionad una implementación del TAD basada en la representación que ha-
béis propuesto.
d) Añadid e implementad en el TAD las siguientes operaciones:
• Iterador trabajadoresOcupados(). Devuelve un iterador que permite iterar sobre los
trabajadores que están ocupados (realizando una tarea).
• Iterador trabajadoresLibres(). Devuelve un iterador que permite iterar sobre los tra-
bajadores que están libres.
e) ¿Tiene algún sentido proporcionar alguna operación que permitiera trabajar con ins-
tancias del TAD Recorrido?

12. En el apartado d del ejercicio11, habéis definido operaciones que devuelven instancias
del TAD Iterador. Es posible que un trabajador comience una tarea o bien acabe la tarea
que está realizando mientras estáis iterando sobre el conjunto de trabajadores libres u
ocupados. Contestad a las siguientes preguntas:
a) ¿Qué solución habéis proporcionado a esta situación?
b) ¿Qué posibles soluciones se os ocurren? ¿Y cuál os parece mejor en esta situación concreta?
c) Implementad variantes de Iterador para el TAD del ejercicio11 para al menos una de
las soluciones mencionadas que no hayáis elegido en el apartado d.

13. Examinad, en la documentación Javadoc del JDK, las clases System y Runtime y contestad
a las siguientes preguntas:
a) Enumerad los métodos relacionados con la gestión de memoria.
b) ¿Es necesario realizar alguna acción especial para que el garbage collector entre en acción?
c) ¿Garantiza Java que cuando un programa deja de usar un espacio de memoria, éste se
recicle inmediatamente? ¿Y después de un período de tiempo determinado?
d) ¿Cuál creéis que es el motivo?

14. ¿Es posible implementar un sistema de gestión de memoria (garbage collection) alternativo
puramente en Java? ¿Por qué?
© FUOC • P06/75001/00577 • Módulo 3 63 Contenedores secuenciales

Solucionario

Ejercicio 7

a) En absoluto. El concepto de posición es aplicable a todos aquellos TAD en los que quere-
mos definir operaciones en las que nos interese introducir, dentro de los parámetros de la
operación, información sobre la ubicación de los elementos tratados. El concepto de posición
está, por lo tanto, relacionado con el comportamiento del TAD y debe ser tenido en cuenta
en la definición de su signatura; pero no tiene nada que ver con ninguna de sus posibles im-
plementaciones.
b) Los elementos a los cuales accedemos en una pila y una cola son exclusivamente los de los
dos extremos. No tiene ninguna utilidad, por lo tanto, utilizar el concepto posición, ya que
estaríamos limitados a usarlos únicamente para dos posiciones concretas. En este caso, es
mucho más práctico tener operaciones diferenciadas para acceder a cada uno de los dos ex-
tremos.
c) Sí que puede tener sentido. Todo depende de las operaciones que nos interese definir y, en de-
finitiva, de si nos interesa trabajar posicionalmente o no. El hecho de trabajar posicionalmente
es útil sobre todo para referenciar posiciones de una colección desde otras estructuras. En el caso
de un vector, esta función, sin embargo, también se puede conseguir referenciando los índices
de las posiciones del vector (una solución mucho más simple y, por lo tanto, utilizada).
d) Por ejemplo: intercambiar(Posicion,Posicion), o modificarValor(Posicion).

Glosario

apuntador m Tipo de datos utilizado en muchos lenguajes de programación para hacer re-
ferencia a otro objeto o valor. El valor de un apuntador es, en realidad, una dirección de me-
moria (en la que está guardado este otro objeto o valor).

colección posicional f Colección en la que los elementos están guardados en unas posi-
ciones que guardan una relación determinada entre ellas. Los usuarios de la colección tienen
acceso a estas posiciones y pueden gestionar la colección utilizándolas.

estructura de datos encadenada f


sin. estructura de datos recursiva

estructura de datos recursiva f Estructura de datos que en su definición hace referencia


a ella misma. Denominada estructura de datos recursiva porque esta referencia a ella misma re-
presenta, en realidad, un encadenamiento entre elementos del mismo tipo (normalmente
denominados nodos).
sin. estructura de datos encadenada

garbage collection f
Véase recogida de basura

lista doblemente encadenada f Estructura encadenada en la que los nodos contienen


dos referencias a otros nodos: uno al siguiente y otro al anterior.

lista simplemente encadenada f Estructura encadenada en la que los nodos contienen


una única referencia a otro nodo (o bien el siguiente para todos ellos, o bien el anterior).

nodo m Elemento básico en representaciones encadenadas de colecciones.

recogida de basura f Proceso mediante el cual, mientras un programa se ejecuta, se reco-


gen aquellos trocitos de memoria que ha utilizado pero que ya no volverá a emplear. Algunos
lenguajes de programación como Java incorporan un sistema de recogida de basura. Este tipo
de sistemas permiten a los programadores no haber de devolver explícitamente al sistema
operativo la memoria descartada.
en garbage collection

referencia f Tipo de datos usado en algunos lenguajes de programación (C++) para hacer
referencia a otro objeto o valor. El concepto es equivalente al de apuntador, y únicamente pre-
senta diferencias de sintaxis en el lenguaje de programación.

referencia nula f Referencia cuyo valor no apunta a ningún objeto.

representación circular f Representación encadenada en la que el último nodo está en-


cadenado con el primero. Utilizada con cuidado, este tipo de representación nos permite ac-
ceder al primero y al último de una colección en tiempo constante y guardar una única
referencia (en el último nodo), en lugar de dos (una en el primero y otra en el último).
© FUOC • P06/75001/00577 • Módulo 3 64 Contenedores secuenciales

representación encadenada f Representación de colección que utiliza una estructura de


datos encadenada.

vector extensible m Implementación que usa como representación un vector, que es redi-
mensionado según las necesidades de espacio; es decir, se redimensiona según el número de
elementos que guardar en él.

Bibliografía

Bibliografía básica
Goodrich, M.; Tamassia, R. (2001). Data structures and algorithms in Java (2.ª ed.). John
Wiley and Sons.

Sahni, S. (2000). Data structures, algorithms, and applications in Java. Summit: McGraw-Hill.

Weiss, M. A. (2003). Data structures & problem solving using Java (2.ª ed.). Upper Saddle River:
Addison Wesley. Disponible en línea en: <http://www.cs.fiu.edu/~weiss>.

Franch, X. (2001). Estructuras de datos. Especificación, diseño e implementación (4.ª ed.). Bar-
celona: Edicions UPC. Disponible en línea en: <http://www.edicionsupc.es>.

Peña Marí, R. (2000). Diseño de programas. Formalismo y abstracción (2.ª ed.). Madrid: Pren-
tice Hall.
© FUOC • P06/05001/00577 • Mòdul 3 65 Contenedores secuenciales

Anexo

Para saber más


A lo largo del módulo hemos descrito con detalle suficente los Un tema que no vemos en este texto es la especificación alge-
TAD secuenciales básicos y las implementaciones más utilizadas. braica de los TAD. Podéis encontrar una buena descripción ge-
Hay, sin embargo, algunas variaciones tanto por lo que respecta neral y la especificación para los diferentes TAD secuenciales
al TAD como a la implementacion que no hemos descrito. Co- vistos en este módulo, tanto en la obra de Franch (1999) como
en la de Peña (1998).
mentaremos brevemente los elementos más representativos que
han quedado y las fuentes en las que os podéis documentar. En
Podéis encontrar la implementación de listas mediante un
el examen de la asignatura no se exigirá tener conocimiento pre- vector en la obra de Sahni (2000), en la que se habla de la si-
vio de los temas estudiados aquí, si bien es posible trabajarlos mulación de apuntadores y se explica cómo se puede gestio-
como ejercicio. No es recomendable revisar estos temas hasta ha- nar la memoria desde la misma implementación del TAD.
ber asimiliado el contenido del módulo.
El TAD SkipList es una variación interesante del TAD Lista que
mediante encadenamientos adicionales permite hacer operacio-
Un TAD que no hemos estudiado y que combina las operaci-
nes de búsqueda de elementos (y otras) de una manera más efici-
ones del TAD Cola con el TAD Pila es la doble cola. Una doble ente. Los TAD SkipList se utilizan habitualmente como
cola permite añadir y borrar elementos en cualquiera de los implementación del TAD Diccionario, que se ve más adelante en
dos extremos. Podéis encontrar una buena descripción de este la asignatura. Lo encontraréis en varios lugares, entre ellos, en las
TAD en la obra de Goodrich y Tamassia (2001). obras de Goodrich y Tamassia (2001) y de Sahni (2000).

También podría gustarte