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