Capitulo V Listas Enlazadas Muere lentamente, quien abandona un proyecto antes de iniciarlo, no preguntando de un asunto que desconoce o no respondiendo cuando le indagan sobre algo que sabe. Evitemos la muerte en suaves cuotas, recordando siempre que estar vivo exige un esfuerzo mucho mayor que el simple hecho de respirar. Pablo Neruda 5.1. Introducción a las Estructuras de Datos Dinámica Hasta ahora, todos los tipos de datos que se han visto, ya sean simples o estructurados, tienen una propiedad común: son estáticos. Esto significa que las variables que se declaran en un programa de alguno de estos tipos mantendrán la misma estructura durante la ejecución del mismo. Son variables estáticas y se definen en tiempo de compilación. Ejemplo 1: Si se declara un Vector de 5 elementos de tipo int, éste podrá cambiar su contenido, pero no su estructura. Hay muchas situaciones en las que se desea cambiar el tamaño de las estructuras usadas. La técnica usada para manejar estas situaciones es la asignación dinámica de memoria. Con este tipo de asignación se tendrán variables dinámicas o referenciadas, que pueden crearse y destruirse en tiempo de ejecución. Ejemplo 2: Si se pide diseñar un programa para gestionar una agenda de teléfonos con los datos de personas (nombre, apellidos, dirección, cumpleaños, teléfono, email, etc...). Qué estructura puede utilizarse para realizarla? Programación II 65 Lic. Katya Pérez Martínez
Una respuesta inmediata pareciera ser la de usar un array (vector o matriz), de la siguiente forma: #define MAX 50 Struct Persona char nombre[10]; char apellidos[10]; char dirección[15]; char email[20] ; Persona per[max]; Si se usa un arreglo de dimensión MAX, pueden surgir los siguientes problemas: Qué pasa si se quieren insertar más personas de MAX en la agenda? Hay que recompilar el programa para ampliar MAX Si el número de personas de la agenda es mucho menor que MAX se está desperdiciando memoria que podría ser utilizada por otros programas En muchos otros casos no es posible conocer anteladamente el numero de elementos que se van a usar. Ejemplo 3: Bases de datos para almacenar los datos de los estudiantes de la universidad Elementos de un dibujo de pantalla, cuyo tamaño depende del dibujo trazado 5.2. Mecanismos Para Enlazar Información a. LISTAS o Listas simplemente enlazadas Solo hay un enlace por nodo Solo se puede recorrer en una dirección o Listas doblemente enlazadas o Lista circular simplemente enlazada o Lista circular doblemente enlazada b. ÁRBOLES Y GRAFOS Programación II 66 Lic. Katya Pérez Martínez
5.3. El Tipo Puntero. Siempre que se habla de punteros hay que diferenciar claramente entre lo que es: Variable referencia o apuntadora (o simplemente puntero) Variable referenciada o apuntada. Una variable de tipo Puntero (variable referencia) contendrá una referencia, es decir la dirección en memoria de una variable de un tipo determinado (variable referenciada). Por tanto, siempre existirá asociado al tipo Puntero, otro tipo que es el de la variable referenciada. En C la definición será: Tipo *TipoP tipo de la variable referenciada. Un puntero es una representación simbólica de una dirección de memoria, es decir, contiene la dirección de un objeto o variable. También se define como una variable que contiene una dirección de memoria. Un puntero apunta a una posición en memoria Operadores * y & & permite devolver la dirección de memoria donde se encuentra almacenada el valor de una variable & Contiene la dirección o posición de memoria en la cual se ha almacenado una variable. El operador & es unario, es decir, tiene un solo operando, y devuelve la dirección de memoria de dicho operando. Supongamos el siguiente ejemplo: Para int x = 4; se tiene * (operador de indirección) Representa el contenido de una dirección de memoria del mismo tipo que ha sido declarada. Programación II 67 Lic. Katya Pérez Martínez
El operador * es unuario y toma a su operando como una dirección de memoria, entonces el operador accesa al contenido de esa dirección. Por ejemplo, suponga las variables declaradas anteriormente y la asignación, Declaración de puntero: TipoDato *NombrePuntero; Ejemplo 4: int *p; // declaración de puntero a una variable entera flota *pf; // declaración de puntero a una variable real Ejemplo 5: int *p; int y; p=&x; y=*p; // Declaración puntero a una variable entera // Declaración de variable de tipo entera // a p se le asigna la dirección de almacenamiento de la variable // se asigna a y el contenido donde apunta p Note que solamente un a puntero se le puede asignar una dirección de memoria. Gráficamente tenemos: Ejemplo 6: En un momento dado, la variable p será un puntero, o lo que es lo mismo, contendrá la dirección en memoria de una variable de tipo entero, que a su vez, contendrá un entero. Para acceder a la variable apuntada (referenciada) hay que hacerlo a través de la variable puntero, ya que aquella no tiene nombre (por esto, también se le denomina variable anónima). Programación II 68 Lic. Katya Pérez Martínez
NOTA Para intercambiar los valores de los parámetros actuales, las funciones deben recibir la dirección de memoria donde se encuentran las variables. main() TipoDato a,b;...... funcion(&a,&b);...... /*Traspaso de direcciones de las variables*/ funcion( TipoDato *x, TipoDato *y) */... /*Los parámetros apuntan a las direcciones Ejemplo : void intercambia(int *x, int *y) int temp; temp=*x; *x=*y; *y=temp; EJERCICIOS: PROGRAMAS DE PUNTEROS // Programa PUNTEROS. Uso de & y * #include <iostream.h> void main (void ) int a; int *P; a = 7; P = &a; // a es un entero // P es un puntero a un entero // P asignado a la dirección de a cout << "La dirección de a es " << &a << endl << "\n El valor de P es " << P << endl << endl; cout << "El valor de a es " << a << endl << "El valor de *P es " << *P << endl << endl; cout << "Demostrando que * y & son complementos de " << "cada uno. " << endl << "&*P = " << &*P << endl << "*&P = " << *&P <<endl; return 0; Programación II 69 Lic. Katya Pérez Martínez
// Programa PUNTEROS. Uso de & y * #include <iostream.h> main ( ) int a; int *aptr; a = 7; aptr = &a; // a es un entero // aptr es un apuntador a un entero // aptr asignado a la dirección de a cout << "La dirección de a es " << &a << endl << "El valor de aptr es " << aptr << endl << endl; cout << "El valor de a es " << a << endl << "El valor de *aptr es " << *aptr << endl << endl; cout << "Demostrando que * y & son complementos de " << "cada uno. " << endl << "&*aptr = " << &*aptr << endl << "*&aptr = " << *&aptr <<endl; return 0; // Programa Nº2 PUNTEROS.suma de dos números A y B y resultado en C #include <iostream.h> #include<conio.h> void main (void) int A, B, C; int *P1, *P2; // A, B y C son enteros // P1 y P2 son punteros a enteros A = 4; B = 6; P1 = &A; // a P1 se le asigna la dirección de A. P2 = &B; // a P2 se le asigna la dirección de B. C = (*P1) + (*P2); Cout<< el resultado de la suma de << *P1 << + <<*P2<< = <<C; getch(); Las listas enlazadas son estructura de datos naturales donde el número de elementos que la integran no es conocido, o en su mejor caso, la cantidad de los mismos puede variar. Programación II 70 Lic. Katya Pérez Martínez
Una lista enlazada es una estructura de datos en la que los objetos están ubicados linealmente. En lugar de índices de arreglo aquí se emplean punteros para agrupar linealmente los elementos. La lista enlazada permite implementar todas las operaciones de un conjunto dinámico. Si el predecesor de un elemento es NULL, se trata de la cabeza de la lista. Si el sucesor de un elemento es NULL, se trata de la cola de la lista. Cuando la cabeza es NULL, la lista está vacía o no existe la lista Toda lista cuenta con un encabezado y nodos. Gráficamente una lista simplemente enlazada se representa de la siguiente forma: Cabeza de la Lista Dirección de memoria Nodo Ultimo elemento de la Lista P Info 23 12 4 enlace NOTA Las listas almacenadas en arreglos están sujetos al tamaño del arreglo para su crecimiento. Si estos están vacíos se esta ocupando memoria sin uso. Además las listas reflejan el orden lógico y el orden físico de los elementos en el arreglo. Características: Las listas son una secuencia ordenada de elementos llamados nodo Toda lista tiene una cabecera y una cola. Todos los nodos de la lista son del mismo tipo. Los nodo pueden ser agregados o suprimidos en cualquier momento en la lista Programación II 71 Lic. Katya Pérez Martínez
Una lista enlazada es una secuencia ordenada de elementos llamados nodos 5.4. Tipo De Dato Abstracto Nodo Un NODO es una estructura compuesta básicamente de dos partes: Un campo de información, en cual se almacenan datos o estructuras Un campo de dirección, en el cual se almacena la dirección del nodo siguiente. La implementación de un nodo podría ser de la siguiente forma: El nodo puede contener objetos de cualquier tipo. Valor que almacenará el nodo. struct Nodo int info; nodo *sig; ; typedef Nodo *LISTA; Todo nodo contará con un apuntador hacia el siguiente nodo. 5.5. Punteros La construcción y manipulación de una lista enlazada requiere el acceso a los nodos de la lista a través de uno o más punteros a nodos. Normalmente, un programa incluye un puntero al primer nodo (cabeza) y un puntero al último nodo (cola). De cualquier forma el último elemento de la lista contiene un valor de 0, esto es, un puntero nulo (NULL) que señala el final de la lista. 5.5.1. Operador -> Programación II 72 Lic. Katya Pérez Martínez
Si p es un puntero a una estructura y m es un elemento de esa estructura entonces p->m accede al miembro m de la estructura puntada por p 5.6. Operaciones Basicas Sobre Listas Enlazadas Existen cinco operaciones básicas que se pueden hacer sobre listas enlazadas: Creación: de una lista vacía Vacía: Retorna verdadero si la lista L está vacía y falso en caso contrario. Inserción: Agregar un elemento al final de la lista. Acceso: Examinar el primer elemento de la lista. Eliminar: Se puede quitar el primer elemento de la lista, o remover todos los elementos de la misma. Buscar: ver si la lista contiene un elemento determinado. Anula: Permite convertir la lista en una lista vacía Imprime Lista: Imprime los elementos de la lista L Creación de una lista vacía A continuación implementamos el procedimiento para crear una lista vacía. void CREAR_LISTA (LISTA *L) *L = NULL; cout<< \n lista creada ; Inserción a) Inserción al Final de la lista En el siguiente segmento de código se ejemplifica solamente el caso en el que el elemento se adiciona al final de la lista tal y como si se tratase de una estructura Pila o Cola. Programación II 73 Lic. Katya Pérez Martínez
Se crea un nuevo nodo asociado al puntero T void Inserta_Final (LISTA *p, int elem) LISTA Q, T; T = new Nodo; T -> Info = elem; T -> sig = NULL; if(*p!= NULL) Q = *p; while ( Q-> sig!= NULL) Q = Q -> sig; Q -> sig = T; else *p = T; Recorrido hasta el final de la lista Se realiza el enlace al final de la lista b) Inserción al Final de la lista En el siguiente segmento de código se implementa el caso en el que el nuevo nodo se inserta en la cabeza de la lista: void Inserta_Inicio(LISTA *p, int elem) LISTA Q; Q = new Nodo; Q - > Info = elem; Q - > sig = *p; *p = Q; Para el siguiente ejemplo se ilustra el proceso de adición de un nuevo nodo p = Inserta_Inicio(&p,10); Programación II 74 Lic. Katya Pérez Martínez
Recorrido de una Lista: Para mostrar los elementos de la lista se realiza el recorrido de la misma, a partir del nodo cabeza de la lista hasta encontrar el último nodo. Recordemos que el último nodo tiene la propiedad que su miembro SIG es igual a NULL. void Recorrido (LISTA p) LISTA Q; Q = p; while ( Q!= NULL) cout<< \n << Q - > Info; Q= Q-> sig; Eliminar el nodo del inicio de la Lista La siguiente función recibe una lista y devuelve esa misma lista, sin el nodo que ocupaba inicialmente la posición de la cabeza. La función será la siguiente: Programación II 75 Lic. Katya Pérez Martínez
void Elimina_Inicio( LISTA *P) LISTA Q; Q = *P; if ( Q->sig!=NULL) *P = Q -> sig; delete Q; *P = NULL; Eliminar el nodo del Final de la Lista void Elimina_Final( LISTA *P) LISTA Q, T ; Q = *P; if ( Q->sig!=NULL) while (Q->sig!= else *p=null; delete Q; T = Q; Q = Q-> sig; T->sig = NULL; NULL) A continuación realizamos la implementación del programa completo de listas simples: Programación II 76 Lic. Katya Pérez Martínez
#include<iostream.h> #include<conio.h> struct Nodo int info; nodo *sig; ; typedef Nodo *LISTA; void CREAR_LISTA (LISTA *L) *L = NULL; cout<< \n lista creada ; void Inserta_Inicio(LISTA *p, int elem) LISTA Q; Q = new Nodo; Q - > Info = elem; Q - > sig = *p; *p = Q; void Recorrido (LISTA p) LISTA Q; Q = p; while ( Q!= NULL) cout<< \n << Q - > Info; Q= Q-> sig; void Elimina_Inicio( LISTA *P) LISTA Q; Q = *P; if ( Q->sig!=NULL) *P = Q -> sig; delete Q; *P = NULL; void Elimina_Final( LISTA *P) LISTA Q, T ; Q = *P; if ( Q->sig!=NULL) while (Q->sig!= NULL) T = Q; Q = Q-> sig; T->sig = NULL; else *p=null; delete Q; //PROGRAMA PRINCIPAL void main (void) LISTA A ; int n, e; CREAR_LISTA(&A) ; clrscr() ; cout<< «cuantos elementos desea ingresar a la lista «; for (int i = 1; i<=n; i++) cout<< ingrese elemento ; cin>>e; Inserta_Inicio(&A, e); Recorrido (A); getch(); Programación II 77 Lic. Katya Pérez Martínez
EJERCICIO Escribir un procedimiento para concatenar dos listas L1 y L2. L2 debe estar concatenado al final de L1. void CONCATENA (LISTA L1, LISTA L2) LISTA Q; Q = L1; while ( Q - >sig!= NULL) Q = Q->sig; Q->sig = L2; Recorrido (L1) ; EJERCICIOS 1. Escriba un programa que permita adicionar elementos no repetidos a una lista. Es decir antes de insertar un determinado elemento se debe verificar antes que éste no se encuentre en la lista. 2. Escriba las subrutinas que permitan adicionar elementos a una lista de forma ordenada 3. Escriba un programa que permita eliminar el elemento de la cola de una lista 4. Escriba un programa que permita contar los elementos de una lista Programación II 78 Lic. Katya Pérez Martínez