Facultad Ingeniería y Tecnología Informática Tecnicatura en Programación de Computadoras Estructura de Datos: Lista Autora: Lunes a viernes de 9 a 21 h. Torre Universitaria, Zabala 1837, primer nivel inferior. C1426DQG - CABA Teléfono: 4788-5400, internos 5002 y 2122. Email: fasciculos@ub.edu.ar www.ub.edu.ar 004300
"La informática tiene que ver con los ordenadores lo mismo que la astronomía con los telescopios" Edsger W. Dijkstra Tipos Abstractos de Datos Lista dinámica Introducción Las estructuras de datos son necesarias para almacenar grandes cantidades de datos de una forma organizada para que el procesamiento y recuperación de la información sea lo más óptima posible. La estructura de datos básica por excelencia es la variable, pero la dificultad que encontramos es que almacena un dato por vez, si quisiéramos almacenar mucha más información simultáneamente, necesitaríamos definir en nuestro programa muchas variables, cada una con un nombre distinto, haciendo confusa sino imposible el procesamiento y recuperación posterior de la información. Los lenguajes de alto nivel poseen estructuras de datos "primitivas" como los arreglos para poder almacenar muchos datos simultáneamente. En un arreglo los datos se almacenan en secuencia en el mismo bloque de memoria y se distinguen por la posición física que ocupan, pero también, los lenguajes nos permiten "armar" estructuras de datos más complejas donde cada dato se almacena individualmente y se enlaza con el resto de la información mediante direcciones de memoria. Las estructuras de datos complejas, para que sean fáciles de manipular se deben de implementar formando lo que se denomina un Tipo Abstracto de Datos que encapsula la representación en memoria del dato y el conjunto de funciones para manipular la información de manera tal que con este conjunto de funciones se defina una biblioteca de operaciones que se pueden aplicar a los datos como si fueran funciones pertenecientes al lenguaje. Un Tipo Abstracto de Datos permite destacar la funcionalidad o características de la estructura de datos y ocultar al usuario la forma en la que se representa en memoria. Dentro de los tipos abstractos de datos encontramos la Lista. Vamos a comenzar a introducir el concepto de Lista como estructura de datos. Clasificación de las Estructuras de Datos Las estructuras de datos pueden ser de dos tipos según sea el tipo de almacenamiento que 1
se use: estáticas, cuyo ejemplo básico es el arreglo y dinámicas cuyo ejemplo clásico es la lista dinámica. En definitiva es esencial saber que tanto un arreglo como una lista organizan la información de forma secuencial, esto quiere decir que cada elemento tiene un antecesor y un predecesor a ecepción del primero que no tiene antecesor y el último que no tiene sucesor. Una lista almacenada en un arreglo estático tiene una desventaja muy importante que es que la cantidad de datos máxima que podrá contener debe quedar fijada antes de su uso. Cuando un programa usa arreglos debe estar sujeto a futuras modificaciones para mofidicar el tamaño de los arreglos adecuándolos al incremento o decremento del volumen de datos a procesar, pero también puede suceder que se reserve más memoria de la que se necesita y por lo tanto no se use todo el espacio reservado con lo cual estamos desperdiciando memoria que podría ser usada por otros procesos. Una estructura de datos almacenada en forma dinámica ajusta su tamaño a las necesidades del momento, crecerá o disminuirá su tamaño a medida que el programa se ejecute dependiendo del volumen de datos. Una estructura así se denomina Lista Dinámica. Una lista implementada en forma dinámica adapta su tamaño a la cantidad de datos a procesar haciendo que el programa no se deba modificar si cambia el volumen de datos. Esto se traduce en que cuando se ingresa un nuevo dato se solicita memoria, trámite que lleva a cabo el compilador del lenguaje en combinación con el sistema operativo. La solicitud de memoria se realiza en tiempo de ejecución por eso este tipo de implementación se denomina dinámica. Listas Una lista es una estructura secuencial <d 1, d 2, d 3,..., d n >, donde n indica la cantidad de elementos de la lista, si n = 0 se dice que la lista está vacía y se denota por <>. El primer elemento es d 1 y el último elemento es d n. Cada elemento de la lista tiene un elemento predecesor y un elemento sucesor con excepción del primero y último, d 1 no tiene elemento anterior a él y d n no tiene un elemento posterior a él. Dado d i con 1 < i < n, tiene como predecesor a d i-1 y como sucesor a d i+1. Las listas se pueden almacenar en memoria de diferentes formas, las dos formas básicas son la estática y la dinámica. Ya mencionamos la desventanja en tener un almacenamiento estático, por lo tanto, en este apunte vamos a tratar el almacenamiento dinámico. Vale la pena aclarar que en el comienzo de la computación, cuando los lenguajes de alto nivel eran muy pocos y se desconocía el almacenamiento dinámico, la única forma de representar una lista era a través de arreglos. Listas Dinámicas Las listas dinámicas se clasifican en listas simplemente encadenadas y listas doblemente encadenadas. En ambos casos el acceso a cada uno de los elementos es secuencial, esto 2
significa que para acceder a un elemento determinado, para llegar a él hay que acceder a todos los elementos anteriores a él, es decir que si queremos acceder a di debemos pasar por d 1, d 2, d 3,..., d i-1. Una lista simplemente encadenada, enlaza cada uno de los elementos usando un puntero mientras que una lista doblemente encadenada usa dos punteros, ambos casos se pueden simular usando arreglos pero en este apunte vamos a usar punteros. Lista Simplemente Encadenada Una lista está compuesta de elementos enlazados entre sí a través de direcciones de memoria que se denomina nodo. Cada nodo de una lista consta de dos partes: un dato, di, y un puntero al elemento siguiente, pi, dando por resultado el par [di,pi], con 1 <= i <= n. El último nodo tiene la característica que pi representa una dirección nula pues no hay un elemento siguiente. La figura siguiente representa una lista simplemente encadenada: Un ejemplo de lista se muestra en la siguiente figura: 3
En nuestra notación, la lista de la figura anterior es: <[2,2184],[4,3225],[6,5571],[7,NULL]> donde, el primer elemento está almacenado en la dirección 2003 y estamos suponiendo que el par [dato, puntero] ocupa 2 bytes. En la figura anterior se puede apreciar que los enlaces son hacia adelante, hacia el último elemento, este tipo de encadenamiento no permite retroceder. TAD: Lista Una lista como tipo abstracto de dato es una caja negra. Esto significa que tanto la forma en la cual representemos los datos en memoria y las funciones que se definan sobre esa representación quedan encapsuladas formando una unidad. Esta forma de trabajar con las estructuras de datos permite escribir programas más óptimos permitiendo que el mantenimiento del mismo sea mucho más fácil. Cuando debamos modificar la representación de los datos para actualizarla a las necesidades del momento sabremos en qué unidad está y lo mismo sucede con el conjunto de funciones definidas. Las funciones hacen las operaciones definidas sobre la lista. Las operaciones que se pueden realizar son: insertar un elemento nuevo modificar un elemento dado eliminar un elemento dado buscar un elemento imprimir los elementos de la lista esvacia (<>) Existe otro conjunto de operaciones básicas sobre un elemento de la lista: obtener el dato obtener el puntero al siguiente asignar un dato asignar un puntero Para completar la abstracción hacen falta dos datos más que son la dirección del primer elemento de la lista y el elemento que está siendo procesado en un determinado momento al que se denomina elemento actual. Todas las operaciones salvo algunas puntuales se hacen a través de el elemento actual, entonces podemos tener que insertar o eliminar el elemento anterior al actual o el elemento posterior al actual, obtener el dato del elemento actual, modificar el valor del elemento actual. 4
Implementación en Lenguaje C Para llevar la abstracción a la implementación en lenguaje C debemos hacer uso del tipo struct. Usaremos el struct para encapsular los datos del nodo y de la lista. Vamos a definir el nodo sólo con dos variables miembro, el dato y el puntero al nodo siguiente, mientras que la lista estará definida por la dirección del primer nodo, que es la única dirección realmente necesaria para acceder a todos los elmentos de la lista y el elemento actual, pero podemos agregar más información útil como por ejemplo la longitud de la lista, mantener este dato hace que sea más fácil determinar si la lista está vacía, longitud igual a 0, y además nos evita contar los elementos de la lista cada vez que es necesario conocer su longitud. Para llevar a cabo la implementación vamos a hacer uso de lo que se denomina Nodo Cabecera, este nodo no llevará información útil en sí mismo sino que se lo utilizará de comodín para que ciertas operaciones sean más fáciles de implementar. Por ejemplo para poder insertar un elemento nuevo en una lista se deben tener en cuenta las siguientes posibilidades: insertar en una lista vacía insertar delante del primer elemento insertar entre dos elementos cualesquiera de la lista insertar después del último Al tener una lista con un elemento cabecera nunca va a estar vacía físicamente, siempre habrá un elemento que es el nodo cabecera por lo tanto nunca tendremos que insertar en una lista vacía. Otro elemento que vamos a incorporar es el Nodo Centinela para facilitar la operación de eliminación cuando debemos eliminar el nodo último. Todas las operaciones se realizarán sobre el elemento actual salvo algunas excepciones. Vamos a tener en cuenta que los nodos centinela y cabecera permiten considerar que la lista no está vacía desde el punto de vista físico, pero la lista está vacía desde el punto de vista lógico porque aún no contiene información, la información por la cual estamos construyendo la lista. TDA Nodo #ifndef _nodo #define _nodo typedef struct nodo #endif tdato dato; struct nodo * prox; Nodo; 5
TDA Lista #ifndef ListaSimple #define ListaSimple #include <stdio.h> #include <stdlib.h> #include "nodo.h" typedef Nodo * Actual; typedef struct Nodo * com; Nodo * actual; int longitud; Lista; void crearlista (Lista * l) Nodo * p = (Nodo *) malloc (sizeof(nodo)); Nodo * q = (Nodo *) malloc (sizeof(nodo)); p->prox = q; q->prox = NULL; l->longitud = 0; l->com = p; l->actual = p; //cambiar el contenido del actual void setdato (Lista * l, tdato t) l->actual->dato = t; //obtener el dato del actual tdato veractual (Lista *l) return l->actual->dato; //verificar el estado actual de la lista int eslistavacia (Lista *l) return l->longitud == 0; //nodo cabecera //nodo centinela //el nodo cabecera apunta al nodo centinela cuando la //lista está vacía //el primer nodo es el nodo cabecera //el elemento actual es el nodo cabecera cuando la // lista está vacía //verificar si el elemento actual es el primero de la lista lógica int esprimero (Lista * l) return l->actual == l->com->prox; //asigna al elemento actual la dirección del primer elemento lógico void primero (Lista * l) if (!esprimero(l)) l->actual = l->com->prox; //verifica que el elemento actual apunte al nodo centinela int esultimo (Lista * l) return l->actual->prox == NULL; 6
//avanza al siguiente elemento de la lista a partir del elemento actual void siguiente (Lista * l) if (!esultimo(l)) l->actual = l->actual->prox; //recorre la lista mostrando la información almacenada en cada nodo void mostrarlista (Lista * l) primero(l); while(!esultimo(l)) mostrardato(veractual(l)); siguiente(l); printf("\n"); //inserta el dato d después del elemento actual void insertardespues (Lista * l, tdato d) Nodo * p = (Nodo *) malloc (sizeof(nodo)); //mostrardato corresponde a la //abstracción de la //información almacenada en el nodo //pide memoria para el nuevo // dato p->dato = d; //actualiza los punteros para que el nodo p quede enlazado entre el actual y // el próximo p->prox = l->actual->prox; l->actual->prox = p; l->actual = p; (l->longitud)++; //incrementa en 1 el tamaño de la lista //inserta el nuevo dato d antes del elemento actual void insertarantes (Lista *l, tdato d) Nodo * p ; if (l->longitud > 0) p = (Nodo *) malloc (sizeof(nodo)); p->prox = l->actual->prox; l->actual->prox = p; p->dato = l->actual->dato; l->actual->dato = d; else insertardespues(l, d); //si la lista está vacía inserta después del //nodo cabecera (l->longitud)++; //elimina el nodo actual void eliminaractual (Lista *l) Nodo * p = l->actual->prox; (l->longitud)--; //actualiza la longitud de la lista decrementando en 1 // el campo longitud l->actual->dato = l->actual->prox->dato; l->actual->prox = l->actual->prox->prox; free(p); 7
//recorre la lista buscando el dato d retornando verdadero o falso int buscardato (Lista *l, tdato d) tdato e; int encontrado = 0; if (!eslistavacia(l)) primero (l); while (!esultimo(l)) e = veractual(l); if (compara(e, d) == 0) encontrado = 1; break; siguiente (l); return encontrado; //obtiene la dirección del elemento actual Actual getactual(lista * l) return l->actual; //la función compara es necesaria //dependiendo de la complejidad del // dato almacenado en la lista //obtiene la dirección del siguiente dato al actual en la lista Actual getsiguiente (Lista * l) return l->actual->prox; //obtiene la longitud de la lista int getlongitud (Lista *l) return l->longitud; //el elemento actual se ubica en el k-ésimo elemento de la lista void posicionar (Lista *l, int k) int i=1; if (l->longitud > = k) primero (l); while (i<k) siguiente (l); i++; //sitúa el elemento actual en el anterior a él void anterior (Lista *l) Nodo * p = l->actual; primero(l); while (getsiguiente(l)!= p) siguiente(l); 8
//convierte la lista en un archivo almacenando los datos en un archivo void listaaarchivo (Lista *l, char *arch) tdato e; FILE *fp; if((fp=fopen(arch,"wb"))==null) printf("\n\n Error, no se puede abrir el archivo: %s\n",arch); exit(0); primero(l); while(!esultimo(l)) e=veractual(l); fwrite(&e,sizeof(tdato),1,fp); siguiente(l); fclose(fp); //vacía la lista, eliminando cada nodo de ella sin eliminar los nodos cabecera y centinela void vaciarlista(lista *l) int i; primero(l); while (!esultimo(l)) eliminaractual(l); l->actual = l->com; //Crea una lista a partir de los datos almacenados en un archivo void archivoalista(lista *l, char *arch) tdato e; FILE *fp; vaciarlista(l); if((fp=fopen(arch,"rb"))==null) printf("\n\n Error, no existe el archivo: %s\n",arch); exit(0); while(fread(&e,sizeof(tdato),1,fp)==1) insertardespues(l,e); fclose(fp); //destruye la lista vaciándola y destruyendo el nodo cabecera y el nodo centinela void destruir(lista *l) vaciarlista(l); free(l->com->prox); free(l->com); 9
//crea una lista cuyos datos están ordenados de menor a mayor void insertarenorden(lista *l, tdato d) if(eslistavacia(l)) insertardespues(l,d); else primero(l); while(!esultimo(l)) if(compara(d,veractual(l))>0) siguiente(l); else break; insertarantes(l,d); //elimina el elemento de la posición k void eliminarpos (Lista *l, int k) posicionar(l, k); eliminaractual(l); //inserta un nuevo dato en la posición k void insertaren (Lista *l, tdato d, int k) posicionar(l, k-1); insertardespues(l, d); #endif El hombre tiene mil planes para sí mismo. El azar, sólo uno para cada uno. Mencio o Meng Zi, filósofo chino confucciano 10
Facultad Ingeniería y Tecnología Informática Tecnicatura en Programación de Computadoras Estructura de Datos: Lista Autora: Lunes a viernes de 9 a 21 h. Torre Universitaria, Zabala 1837, primer nivel inferior. C1426DQG - CABA Teléfono: 4788-5400, internos 5002 y 2122. Email: fasciculos@ub.edu.ar www.ub.edu.ar 004300