ESTRUCTURAS DE DATOS Listas ligadas Listas ligadas 2 Es una colección lineal de elementos homogéneos. Lineal significa que cada elemento de la colección tiene un predecesor (excepto el primero) y un único sucesor (excepto el último). El acceso a la lista se efectúa de manera secuencial Listas ligadas Listas ligadas (cont.) 3 La lista tiene dos componentes especiales llamados head y tail. Estos apuntan al primer y último elemento de la lista Las listas ligadas son una representación simple de conjuntos dinámicos y que realiza todas operaciones : [ Search(), Insert(), Delete(), Minimum(), Maximum(), Sucessor() y Predecessor()] Listas ligadas 4 Representación de un elemento para la lista typedef struct nodo { int valor; //valor o información que guarda el nodo struct nodo* sig;//referencia al siguiente nodo de //la lista } nodo_t; El nodo consiste de un dato y un apuntador. valor sig Representación de head y tail 5 El apuntador head se declara como un apuntador a tipo nodo_t y con la ayuda de un auxiliar ptr que irá desplazándose a través de los elementos de la lista. nodo_t *head; nodo_t *ptr; Mientras que el tail está implícito en el último elemento de la lista. El apuntador de ese elemento apunta a NULL. Listas ligadas En resumen 6 La estructura lista tiene dos apuntadores al primero y al último nodo. Cada nodo tiene un apuntador al siguiente nodo El último nodo es un apuntador a NULL Listas ligadas Operaciones de la lista 7 Estas operaciones nos facilitarán la manipulación de los elementos de las listas. create(): Generar una nueva lista isEmpty() : Indica si la lista esta vacía. insert(nodo_t n): Inserta un nuevo elemento n a la lista (puede tener variantes) print() : Muestra el contenido de la lista Listas ligadas Operaciones de la lista (cont.) 8 freeList(): Vacía la lista. search(v): Busca un elemento n en la primera ocurrencia de la lista y devuelve su posición. delete(v) : Borra un elemento de la lista deleteAll(v): Elimina todas la ocurrencias de v en la lista. (Implementaremos para LLD) reset() : Lleva el apuntador que recorre la lista al inicio. Listas ligadas create() 9 Se generan la variables apuntador head y ptr y ambas tendrán la dirección nula (NULL), lo que indica que tenemos una lista vacía head ptr x x El método isEmpty() devolvería 1 a esta nueva lista. Listas ligadas insert() 10 Insertar al inicio de una lista Si es una lista vacía, luego de crear el nuevo nodo hacemos que head y ptr apunten al nuevo nodo ptr head ptr x x head Si 9 contiene al menos un elemento, head apunta a la dirección de memoria del nuevo nodo, el apuntador sig va a ser el mismo que ptr Listas ligadas insert() al inicio 11 ptr ptr head 9 head 3 9 El desarrollo de la función insert() que recibe por referencia el puntero al primer nodo de lista más un valor de tipo int para agregarlo como el primer elemento. Listas ligadas insert() al final 12 Al final de la lista La idea es recorrer la lista avanzando sobre cada uno de sus nodos hasta llegar al último. El último se identifica por el valor NULL en su referencia al siguiente nodo. 3 ptr 5 7 1 6 head Para ello utilizaremos el apuntador ptr y le asignamos la dirección contenida por head. Listas ligadas insert() al final 13 Para saber si ptr apunta al último nodo preguntamos si ptr->sig es NULL. Observando la figura, ptr->sig (el siguiente de ptr) no es NULL ya que tiene la dirección del nodo con valor 7 ptr head 5 7 1 6 El próximo paso será hacer que ptr apunte al siguiente nodo: ptr = ptr->sig ptr head 5 7 1 6 insert() al final 14 En realidad, este proceso lo haremos dentro de un ciclo de repeticiones que itere mientras que “el siguiente de ptr” sea distinto de NULL //: l->ptr = l->head; while((l->ptr)->sig != NULL) { l->ptr = l->ptr->sig; }//: ptr comienza apuntando al primer nodo de la lista y en cada iteración apuntará al siguiente. La condición del while se dejará de cumplir cuando ptr apunte a un nodo sin elemento. insert() al final 15 ptr 3 head 5 7 1 6 El último paso será crear un nuevo nodo y enlazarlo al final. Esto es convertirlo en el siguiente del último nodo de la lista. ptr head 5 7 1 6 3 //creamos un nuevo nodo nodo_t* nuevo = (nodo_t*)malloc(sizeof(nodo_t)); //asignamos su valor y NULL en su siguiente nuevo->valor=3; nuevo->sig = NULL; //lo enlazamos como siguiente de ptr l->ptr->sig = nuevo; void insertAtTail(list_t *l, int v) { node_t *nuevo = (node_t *)malloc(sizeof(node_t)); nuevo->valor = v; nuevo->sig = NULL; if(l->head == NULL ) { //primer elemento de l l->head = nuevo; } else { l->ptr = l->head; while((l->ptr)->sig != NULL) { l->ptr = l->ptr->sig; } l->ptr->sig = nuevo; } } // Enlazar el nuevo nodo print() 18 Hacemos un recorrido a la lista no vacía e imprimimos el valor almacenado. Nos posicionamos en el primer nodo, mostramos su valor y avanzamos al siguiente. ptr x 5 7 1 head l->ptr = l->head; while(l->ptr != NULL) { printf(" %d ",l->ptr->valor); l->ptr = l->ptr->sig; } print() 19 ptr Efectuando debugging 5 7 1 head l->ptr = l->head; while(l->ptr != NULL) { printf(" %d ",l->ptr->valor); l->ptr = l->ptr->sig; } ptr 5 7 1 head l->ptr = l->head; while(l->ptr != NULL) { printf(" %d ",l->ptr->valor); l->ptr = l->ptr->sig; } Listas ligadas print() 20 Entrando al while ptr 5 7 ptr 1 head l->ptr = l->head; while(l->ptr != NULL) { printf(" %d ",l->ptr->valor); l->ptr = l->ptr->sig; } 5 7 1 head l->ptr = l->head; while(l->ptr != NULL) { printf(" %d ",l->ptr->valor); l->ptr = l->ptr->sig; } Listas ligadas print() 21 Continuamos con el debugging ilustrado ptr head 5 7 ptr 1 l->ptr = l->head; while(l->ptr != NULL) { printf(" %d ",l->ptr->valor); l->ptr = l->ptr->sig; } head 5 7 1 l->ptr = l->head; while(l->ptr != NULL) { printf(" %d ",l->ptr->valor); l->ptr = l->ptr->sig; } Listas ligadas print() 22 ptr head 5 7 1 l->ptr = l->head; while(l->ptr != NULL) { printf(" %d ",l->ptr->valor); l->ptr = l->ptr->sig; } ptr head 5 7 1 l->ptr = l->head; while(l->ptr != NULL) { printf(" %d ",l->ptr->valor); l->ptr = l->ptr->sig; } Listas ligadas print() final debugging 23 ptr head 5 7 NULL 1 l->ptr = l->head; while(l->ptr != NULL) { printf(" %d ",l->ptr->valor); l->ptr = l->ptr->sig; } ptr head 5 7 NULL 1 l->ptr = l->head; while(l->ptr != NULL) { printf(" %d ",l->ptr->valor); l->ptr = l->ptr->sig; } Listas ligadas print() diagrama de flujo 24 En la función, simplemente recibimos el puntero al primer nodo de la lista (del tipo nodo_t*), Esto es porque no vamos a modificarlo. print list_t *l l->ptr ← l->head l->ptr != NULL l->ptr->valor l->ptr ← ptr->sig freeList() 25 La memoria gestionada por malloc es persistente y permanece asignada durante la ejecución del programa. Por ello es nuestra responsabilidad liberar la memoria cuando ya no la necesitemos freeList list_t* l l->head !=NULL prox ← (l->head)->sig free(l->head) l->head ← prox freeList() 26 La función libera la memoria que utiliza la lista ligada direccionada por head. Al finalizar, debe asignar el valor NULL a head. La lista quedará vacía. Por ello hay que recibir a head por referencia (list_t *l) La función finaliza cuando head tenga el valor NULL. Como l->head es la dirección del primer nodo entonces (l->head)->sig es la dirección del segundo Listas ligadas freeList() 27 La asignación prox=(l->head)->sig asigna a la variable prox la dirección del segundo elemento de la lista. Los paréntesis se colocan ya que el operador -> tiene precedencia sobre el operador * y si no lo utilizamos estaríamos hablando del “siguiente de head” y el apuntador no tiene ese campo. Después de la asignación prox=(l->head)->sig podemos liberar la memoria asignada por l->head Listas ligadas freeList() 28 prox head 5 7 1 Ahora debemos hacer que el primer nodo de la lista sea el que está siendo apuntado por prox. La asignación l->head=prox descarta, definitivamente, el primer nodo (ya liberado mediante la función free) y hace que la lista comience desde el segundo elemento. Listas ligadas freeList() 29 prox 5 7 1 head El hecho de liberar con free no implica que la información se haya borrado. El espacio de memoria ya no pertenece más a nuestro programa y, no tenemos formar de accederlo porque no lo tenemos apuntado con ningún puntero. Quedó des referenciado. Listas ligadas void freeList(list_t *l){ while (l->head != NULL){ nodo_t* prox = (l->head)->sig; free(l->head); l->head = prox; } } 30 Cuando prox apunte al último nodo de la lista la asignación l->head = prox estaremos asignando NULL a head con lo que, finalmente, la lista quedará vacía y la memoria que ocupaba estará liberada. Listas ligadas search(v) 31 Vamos a determinar si la lista contiene un valor determinado v Búsqueda secuencial en los valores de los nodos Si existe el valor en la lista se entrega la dirección en memoria del nodo Puede darse el caso que no exista el elemento en la lista, por lo que la función podría arrojarnos NULL como resultado. Listas ligadas search(v) 32 Para la lista ligada de la figura, la llamada a search(l,7) regresa un puntero al segundo elemento, y la llamada a search(l,9) retorna NULL ptr 5 7 1 head search list_t *l, int v l->ptr ← l->head l->ptr!=NULL && l->ptr->val!= v l->ptr ← ptr->sig return ptr Listas ligadas delete(v) 33 La función delete(v) desreferencia, un elemento que contiene el valor v, de la lista ligada. Con la ayuda de un apuntador aux, ptr recorre la lista hasta encontrar el elemento con el valor indicado, aux recorre la lista pero quedando referenciado a un elemento atrás del que apunta ptr. O podría resolverse más sencillo si los elementos de la lista apunta tanto al predecesor como al siguiente. Listas ligadas delete(v) 34 Se utilizan dos apuntadores: uno al nodo que se va a elimiar(aux) y otro que apunte al nodo anterior(ant). Listas doblemente ligadas 35 Similar a la estructura de una lista ligada simple. Tienen dos apuntadores, al nodo anterior y al nodo siguiente. Mejora los algoritmos de los métodos definidos para un lista ligada simple. Nodo sig valor prev Listas doblemente ligadas El apuntador al anterior del primer nodo y el apuntador al siguiente del último son NULL. La lista puede recorrerse en ambas direcciones. Las operaciones insertar y eliminar utilizan menos instrucciones. También se manejan dos apuntadores p y q que apuntan al primer y al último nodo. Listas doblemente ligadas Para diseñar un algoritmo que defina el comportamiento de una Listas Doblemente Ligadas se deben considerar 2 casos para cada operación (buscar, insertar y eliminar): Estructura vacía (caso extremo). Estructura con elemento(s) (caso base). Lista doblemente ligada circular Una lista doblemente ligada circular (o lista doble circular) es una lista doblemente ligada modificada, donde la referencia siguiente (NEXT) del elemento que se encuentra al final de la lista (tail) en lugar de apuntar a nulo, apunta al primer elemento de la lista (head). Es posible recorrer la lisa a través de la referencia al predecesor (PREV) de cada nodo, hay que tener en cuenta el número de elementos de la lista, ya que el primer elemento apunta al final de la estructura y, por tanto, se puede recorrer de manera infinita. head tail PREV NEXT