Documentos de Académico
Documentos de Profesional
Documentos de Cultura
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
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
Resumen .................................................................................................... 59
Solucionario ............................................................................................. 63
Glosario ..................................................................................................... 63
Bibliografía .............................................................................................. 64
Anexo.......................................................................................................... 65
© FUOC • P06/75001/00577 • Módulo 3 5 Contenedores secuenciales
Introducción
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.
• 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, 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
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.
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
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
<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).
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
p = <llamada al constructor>();
p.apilar(7);
p.cima();
p.apilar(4);
p.desapilar();
p.apilar(2);
p.apilar(8);
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.
• 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
Figura 3
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.
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).
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”.
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.
• constructor() O(DEFAULT_MAX)
– Llama al constructor con DEFAULT_MAX como número de elementos
máximo (O(DEFAULT_MAX)).
• 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
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.
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 PilaVectorImpl() {
this(MAXIMO_ELEMENTOS_POR_DEFECTO);
}
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
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.
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.
• 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.*;
for(int i = 0;i<numPaginas;i++)
pila.apilar(paginas[i]);
while(!pila.estaVacio())
documentoInvertido.insertar(pila.desapilar());
return documentoInvertido;
}
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.
Como sucede con las pilas, encontramos colas en muchas situa- Figura 5
Figura 6
Sistemas peer to peer
2.1. Operaciones
<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.
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
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
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:
Figura 8
Figura 9
Figura 10
Figura 11
© FUOC • P06/75001/00577 • Módulo 3 23 Contenedores secuenciales
p + 1 % elementos.length
• constructor() O(DEFAULT_MAX)
– Llama al constructor con DEFAULT_MAX como número de elementos máxi-
mo (O(DEFAULT_MAX)).
• 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
ColaVectorImpl.java
package uoc.ei.tads;
public ColaVectorImpl() {
this(MAXIMO_ELEMENTOS_POR_DEFECTO);
}
public E desencolar() {
E elem = elementos[primero];
elementos[primero] = null;
primero = siguiente(primero);
n--;
return elem;
}
...
}
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.
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.
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 }
Figura 12
© FUOC • P06/75001/00577 • Módulo 3 27 Contenedores secuenciales
Variable maximo:
Utilidades.leerString("",System.in).
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.
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.
Persona.java
package uoc.ei.exemples.modul3.referencia;
public class Persona {
String nombre;
Persona padre;
Persona madre;
Figura 13
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.
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.
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.
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’.
• 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.
Figura 15
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.
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.
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.
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
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.
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.
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.
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
colección Lista<E> es
constructor()
Crea una lista vacía.
@pre Cierto.
@post Devuelve una lista sin ningún 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.
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
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.
Figura 17
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.
Figura 18
constructor() O(1)
Pone último a ‘null’.
Pone el número de elementos a 0.
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.
Figura 19
© FUOC • P06/75001/00577 • Módulo 3 44 Contenedores secuenciales
Figura 20
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.
ListaEncadenada.java
package uoc.ei.tads;
ListaDoblementeEncadenada.java
package uoc.ei.tads;
...
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;
}
}
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.
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
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
• 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.
ListaEncadenadaConIterador.java
package uoc.ei.ejemplos.modulo3.lista;
import ...
public class ListaEncadenadaConIterador<E>
extends ListaEncadenada<E> {
IteradorLista(ListaEncadenadaConIterador<EI> ll) {
this.ultimo = ll.ultimo;
if (ultimo! = null)
siguiente = ultimo.getSiguiente();
}
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.
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
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.
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>();
}
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.
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
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.
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.
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
Razonad por qué, en realidad, es suficiente con garantizar que el número de posiciones
libres después del redimensionamiento sea proporcional a n.
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
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:
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.
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).
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
Ejercicios de autoevaluación
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é.
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.
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.
garbage collection f
Véase recogida de basura
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.
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