Tema 3: Listas Enlazadas

Anuncio
Programación. Tema 3: Listas Enlazadas
(16/Mayo/2004)
Apuntes elaborados por: Eduardo Quevedo/ Raquel López García
Revisado por: Javier Miranda el ????
Tema 3: Listas Enlazadas
En este tema se va a trabajar con memoria de tipo dinámico, organizada
por nodos y por punteros. Igual que hicimos en el tema del array,
comenzaremos considerando que el contenido de la lista no es necesario
mantenerlo ordenado (Lista no ordenada).Antes de comenzar con algunos
procedimientos que podemos realizar con listas, veremos las declaraciones
necerias para la interfaz y el cuerpo de nuestra lista
Interfaz de la lista enlazada
package Lista_Dinamica is
Llena : exception;
No_Encontrado : exception;
type T_Lista is limited private;
procedure Insertar ( Lista : in out T_Lista,
Valor : in
Integer);
-- Excepciones : Llena
-- Resto de procedimientos y funciones.
...
private
...
end Lista_Dinamica;
Como puede verse, ahora ya no se especifica ningún tamaño, pues la
lista puede llegar a ocupar toda la memoria RAM del ordenador.
La especificación limited private se utiliza para impedir que una variable
de tipo T_Lista se pueda copiar en otra variable de tipo T_Lista. Es decir, sirve
para que cada variable de tipo T_Lista se corresponda con un único contenido.
Si no se prohibe la copia el programador puede hacer disparates como el
siguiente:
Lista_1 :T_Lista;
Lista_2 :T_Lista;
Begin
Lista_2 = Lista_1
1
Programación. Tema 3: Listas Enlazadas
(16/Mayo/2004)
Veamos ahora en detalle cómo se declaran los nodos en la parte privada
de la interfaz:
private
type T_Nodo;
-- La declaracion de este tipo se utiliza para romper la referencia mutua que se
-- genera despues entre T_Nodo y T_Nodo_Ptr, indica que en el programa va -- a aparecer un T_Nodo
type T_Nodo_Ptr is access T_Nodo;
type T_Nodo is
record
Valor : Integer;
Siguiente : T_Nodo_Ptr := null;
end record;
-- El null dice que ese puntero esta inicializado para no apuntar a nada
type T_Lista is
record
Primero : T_Nodo.Ptr := null;
end record;
Cuerpo de la lista enlazada
with Unchecked_Deallocation;
package body Lista_Dinamica is
procedure Free is
new Unchecked_Deallocation (T_Nodo, T_Nodo_Ptr);
-- Cuerpo de todos los subprogramas
end Lista;
El procedimiento Free es un genérico, en el cual no se observan los
parámetros y en donde T_Nodo y T_Nodo_Ptr se utilizan para construir el
genérico. La finalidad del procedimento Free (como su propio nombre indica)
es liberar un nodo.
Inserción en lista enlazada
Si la lista no está ordenada la forma más rápida de insertar un elemento
es insertarlo siempre por el principio de la lista. Para realizar la inserción hay
que crear un nuevo nodo y arreglar 2 punteros (el puntero a este nuevo primer
elemento, y en este nuevo nodo hay que guardar copia de la dirección del nodo
que estaba antes como primer nodo, y que ahora pasa a ser el segundo). Si los
datos deben estar ordenados (Lista Ordenada) entonces la rutina de inserción
2
Programación. Tema 3: Listas Enlazadas
(16/Mayo/2004)
deberá buscar (mediante un bucle) la posición de la inserción.Pasaremos a ver
algunas formas de implementar este procedimiento, empezaremos con un
programa mas largo q iremos optimizando:
procedure Insertar (Lista : in out T_Lista
Valor : in Integer) is
Nuevo : T_Nodo_Ptr;
begin
Nuevo
: new T_Nodo;
-- Se crea un nuevo nodo
Nuevo.Valor
:= Valor;
-- Se llena el nodo que se acaba de crear
Nuevo.Siguiente := Lista.Primero;
-- El primero de la lista se copia en el siguiente del nuevo
-- Con ello se apunta a donde apunta nuevo
Lista.Primero
:= Nuevo
exception
when Storage_Error =>
-- Nos avisa de quen estamos llenando la memoria del ordenador, con
-- la finalidad de que borremos algo
raise Llena;
end Insertar;
Se llega a una simplificación del procedimiento de Insertar si los valores
de Nuevo se inicializan al declarar la variable:
procedure Insertar (Lista : in out T_Lista
Valor : in Integer) is
Nuevo : T_Nodo_Ptr := new T_Nodo (Valor
=> Valor,
Siguiente => Lista.Primero);
begin
Lista.Primero
:= Nuevo
exception
when Storage_Error =>
raise Llena;
end Insertar;
Ahora veremos un procedimiento que insertará los elementos de uan
lista de forma ordenada para ello supondremos que Clave es un string y que
Long_Clave es la longitud de dicho string:
procedure Insertar_Ordenadamente (Lista : in out T_Lista;
Objeto : in T_Informacion) is
-- Creación de un nuevo nodo
Nuevo : T_Nodo_Ptr := new T_Nodo'(Libro => Libro,
Siguiente => null);
-- Creación de las variabes puntero, seguro, estas serán punteros que nos
-- ayudaran a realizar un avance seguro al recorrer la lista
Puntero : T_Nodo_Ptr := Lista.Primero;
3
Programación. Tema 3: Listas Enlazadas
(16/Mayo/2004)
Seguro : T_Nodo_Ptr := null;
begin
-- Caso de lista vacía
if Lista.Primero = null then
Lista.Primero := Nuevo;
-- Caso de lista no vacía
else
-- Avance seguro teniendo en cuenta que el siguiente sea /= null
while Puntero.Siguiente /= null and
Puntero.Objeto.Clave (1 .. Puntero.Objeto.Long_Clave) <
Objeto.Clave (1 .. Objeto.Long_Clave) loop
Seguro := Puntero;
Puntero := Puntero.Siguiente;
end loop;
-- Observamos 3 casos: Va en el primer lugar, va al final, o va en medio
-- 1) Está al principio
if Puntero = Lista.Primero then
Nuevo.Siguiente := Lista.Primero;
Lista.Primero := Nuevo;
-- 2) Está al final de la lista
elsif Puntero = null then
Puntero.Siguiente := Nuevo;
3) Está en el medio
else
Nuevo.Siguiente := Puntero;
Seguro.Siguiente := Nuevo;
end if;
end if;
end Insertar_Ordenadamente;
Búsqueda en lista enlazada
Para buscar un elemento en una lista no ordenada hay que recorrer los
nodos. Es importante recordar que los nodos están repartidos por toda la
memoria del ordenador (a nosotros nos da igual donde estén), y hay que hay
que tener en cuenta que los nodos no estan en posiciones de memoria
consecutivas por lo que los atributos de Ada ´Pred y ´Succ no pueden ser
utilizados en memoria dinámica. Como siempre, la función que realiza la
búsqueda retornará un True en caso de que encuentre el elemento buscado.
Borrado en lista enlazada
Para borrar un nodo hay primero que buscarlo (mediante un bucle), pero
hay que tener en cuenta que al borrarlo el puntero del nodo anterior hay que
modificarlo para que apunte al que esté después del nodo que borramos. Para
recordar la posición del nodo anterior sin tener que recorrer de nuevo todos los
elementos de la lista se utiliza la técnica de avance seguro. En general cada
4
Programación. Tema 3: Listas Enlazadas
(16/Mayo/2004)
vez que se borra hay que arreglar dos punteros, excepto cuando borramos el
primero o el último elemento, donde sólo hay que arreglar un puntero.
procedure Borrar (Lista
: in out T_Lista;
Clave
: in
String;
Long_Clave : in
Natural)
is
-- Creación de dos punteros: el segundo es para el avance seguro
Puntero : T_Nodo_Ptr := Lista.Primero;
Seguro : T_Nodo_Ptr := null;
begin
-- Caso de lista vacía
if Lista.Primero = null then
raise No_Encontrado;
-- Caso de lista no vacía
else
-- Avance seguro teniendo en cuenta que el puntero sea /= null
while Puntero /= null and then
Puntero.Objeto.Clave (1 .. Puntero.Objeto.Long_Clave) /=
Clave (1 .. Long_Clave) loop
Seguro := Puntero;
Puntero := Puntero.Siguiente;
end loop;
-- 3 casos: Está en el primer lugar, está al final, o está en medio
-- 1) Está al principio
if Puntero = Lista.Primero then
Lista.Primero := Puntero.Siguiente;
Free (Puntero);
-- 2) Está al final
elsif Puntero = null then
raise No_Encontrado;
-- 3) Está en medio
else
Seguro.Siguiente := Puntero.Siguiente;
Free (Puntero);
end if;
end if;
end Borrar;
Algunos ejercicios más de listas simplemente enlazadas:
Ahora se veran algunos otros procedimientos útiles para trabajar con
listas, al igual que en temas anteriores se empezará con una version algo mas
larga para ir optimizandola poco a poco lo q ayudará a la comprensión de los
algoritmos.
5
Programación. Tema 3: Listas Enlazadas
(16/Mayo/2004)
Función que calcule el número de elementos de una lista:
La idea de este algoritmo es bastante sencilla, lo que tendremos q hacer
para ver la longitud de una lista es simplemente recorrer la lista hasta el final e
ir contando el número de saltos. El principal motivo por el que deberíamos
implementar es que nos permite aprender y comprender el manejo de los
nodos.
Versión 1.0:
function Longitud (Lista : in T_Lista) return Natural
is
Contador : Natural := 0;
-- Inicializamos actual a null
Actual : T_Nodo_Ptr := null;
begin
-- Comprobamos que la lista no está vacia
if Lista.Primero = null then
-- Retornamos cero ya que no hay elementos en la lista
return 0;
else
-- Avanzaremos en la lista utilizando el puntero actual
Actual := Lista.Primero;
-- Iremos recorriendo la lista hasta que nos encontremos
while Actual /= null loop
-- Aumentaremos el contador que nos proporcianará la longitud
Contador := Contador + 1;
Actual := Actual.Siguiente;
end loop;
end if;
-- retornamos la variable contador que será la longitud de la lista
return Contador;
end Longitud;
Versión final:
Vemos en la versión anterior que estamos comprobando dos veces si la
lista está vacía, así que esta será la optimización que realicemos en esta
versión final de nuestro algoritmo.
function Longitud (Lista : in T_Lista) return Natural
is
Contador : Natural := 0;
-- Inicializaremos ahora Actual a Lista.Primero para evitar comprobar dos
-- veces que la lista esté vacía.
Actual : T_Nodo_Ptr := Lista.Primero;
begin
while Actual /= null loop
Contador := Contador + 1;
6
Programación. Tema 3: Listas Enlazadas
(16/Mayo/2004)
Actual := Actual.Siguiente;
end loop;
return Contador;
end Longitud;
Procedimiento de Copiar:
Debido a que mediante el limited private se ha limitado la copia directa
en listas sería bueno tener un procedimiento que realizase dicha copia, como
normalmente cuando asignamos variables el destino lo ponemos a la izquierda
y el origen a la derecha aquí también se tendrá en cuenta dicho orden, es decir
el primer parámetro del procedimiento será el destino y el segundo el origen:
procedure Copiar (Destino : in out T_Lista
Origen : in
T_Lista
Is
-- Irá recorriendo la lista Origen
Actual : T_Nodo_Ptr := Origen.Primero;
-- Irá recorriendo la lista Destino
Nuevo : T_Nodo_Ptr := new T_Nodo'(Valor => Actual.Valor,
Siguiente => null);
begin
-- Se mira si se dan las condiciones propicias para que se haga la copia.
-- Primero comprobaremos que la lista Destino esté vacía.
if Destino.Primero /= null then
raise Parametro_Erroneo;
-- Seguidamente comprobaremos que la Origen contenga algo
elsif Origen.Primero = null then
-- Saldríamos del procedimiento
return;
-- Comenzamos el duplicado de la lista
else
Actual
:= Origen.Primero;
Destino.Primero := Nuevo;
-- Debemos tener cuidado con esta condición ya que so pusiéramos
-- Actual.Siguente /= null no copiaríamos el ultimo elemento de la lista
while Actual /= null loop
-- Avanzamos en la lista Origen
Actual
:= Actual.Siguiente;
Nuevo.Siguiente :0= new T_Nodo;
-- Avanzamos en la lista Destino
Nuevo
:= Nuevo.Siguiente;
-- Copiamos la lista
Nuevo.Valor
:= Actual.Valor;
end loop;
end if;
end Copiar;
7
Programación. Tema 3: Listas Enlazadas
(16/Mayo/2004)
Procedimiento de intercambiar consecutivamente:
El procedimiento de intercambiar consecutivamente
serviría para
intercambiar en el procedimiento de ordenación por burbuja, pues intercambia
nodos consecutivos, para selección o inserción habría que hacer un
procedimiento que intercambiase dos nodos cualesquiera. Con este
procedimiento aprenderemos la importancia del orden de las asignaciones para
no perder ningún nodo en el camino:
procedure Intercambiar_Consecutivo (Lista : in out T_Lista;
Puntero_1 : in T_Nodo_Ptr;
Puntero_2 : in T_Nodo_Ptr)
is
Anterior : T_Nodo_Ptr := Lista.Primero;
begin
-- Se hacen las comprobaciones previas
-- Ninguno de los punteros puede estar apuntando a null
if Puntero_1 = null or Puntero_2 = null then
raise Parametro_Nulo;
-- Los nodos deben ser consecutivos
elsif Puntero_1.Siguiente /= Puntero_2 then
raise Parametro_Erroneo;
-- Si el Puntero_1 es el primero elemento de la lista es un caso sencillo.
-- realizamos las asiganciones
elsif Lista.Primero = Puntero_1 then
Lista.Primero
:= Puntero_2;
Puntero_1.Siguiente := Puntero_2.Siguiente;
Puntero_2.Siguiente := Puntero_1;
-- Si no, nos encontramos ante un caso general
else
-- Debemos comprobar que el puntero este en la lista
while Anterior /= null and then Anterior.Siguiente /= Puntero_1 loop
Anterior := Anterior.Siguiente;
end loop;
-- Debemos evitar salirnos de la lista
if Anterior = null then
raise No_Encontrado;
-- Realizamos las asiganciones
else
Anterior.Siguiente := Puntero_2;
Puntero_1.Siguiente := Puntero_2.Siguiente;
Puntero_2.Siguiente := Puntero_1;
end if;
end if;
end Intercambiar_Consecutivo;
Se observa que las líneas:
Puntero_1.Siguiente := Puntero_2.Siguiente;
Puntero_2.Siguiente := Puntero_1;
Se pueden sacar factor común.
8
Programación. Tema 3: Listas Enlazadas
(16/Mayo/2004)
Lista doblemente enlazada:
La lista doblemente enlazada tiene la capacidad de recorrer la lista tanto
hacia delante como hacia atrás, por ello es una lista mucho más flexible, en la
interfaz bastará con definir un nodo “anterior” inicializado como siempre a null;
(no apunta a nada) y en el record de T_Lista aparte del primero, es
recomendable tener un puntero al último nodo (para poder recorrerla hacia
atrás y para insertar rápidamente un nodo al final de la lista). La parte privada
de la interfaz de la lista doblemente enlazada es:
private
type T_Nodo;
type T_Nodo_Ptr is access T_Nodo;
type T_Nodo is
record
Valor
: Integer;
Siguiente : T_Nodo_Ptr := null;
Anterior : T_Nodo_Ptr := null;
end record;
type T_Lista is
record
Primero : T_Nodo_Ptr := null;
Ultimo
: T_Nodo_Ptr := null;
end record;
end Lista_Dinamica;
Algunos ejercicios de listas doblemente enlazadas:
Procedimiento de Insertar en lista no ordenada
procedure Insertar (Lista : in out T_Lista
Valor : in Integer) is
Nuevo : T_Nodo_Ptr := new T_Nodo (Valor
=> Valor,
Siguiente => null,
Anterior => null);
begin
if Lista.Primero = null then
Lista.Primero := Nuevo;
Lista.Ultimo := Nuevo;
else
Nuevo.Siguiente
:= Lista.Primero;
Lista.Primero.Anterior := Nuevo;
Lista.Primero
:= Nuevo;
end if;
exception
when Storage_Error =>
raise Llena;
end Insertar;
9
Programación. Tema 3: Listas Enlazadas
(16/Mayo/2004)
Procedimiento de intercambiar consecutivamente:
Es el análogo para listas doblemente enlazadas del ya hecho para las
simplemente enlazadas, teniendo que arreglar dos punteros. Los comentarios
serás los mismos que para el de listas simplemente enlazadas:
procedure Intercambiar_Consecutivo (Lista : in out T_Lista;
Puntero_1 : in T_Nodo_Ptr;
Puntero_2 : in T_Nodo_Ptr)
is
Anterior : T_Nodo_Ptr := Lista.Primero;
begin
-- Se hacen las comprobaciones previas
if Puntero_1 = null or Puntero_2 = null then
raise Parametro_Nulo;
elsif Puntero_1.Siguiente /= Puntero_2 then
raise Parametro_Erroneo;
-- Se intercambia
else
if Puntero_1.Anterior /= null then
Puntero_1.Anterior.Siguiente := Puntero_2;
else
Lista.Primero := Puntero_2;
end if;
if Puntero_2.Siguiente /= null then
Puntero_2.Siguiente.Anterior := Puntero_1;
else
Lista.Ultimo := Puntero_1;
end if;
P2.Anterior := P1.Anterior;
P1.Siguiente := P2.Siguiente;
P1.Anterior := P2;
P2.Siguiente := P1;
end Intercambiar_Consecutivo;
10
Descargar