Tabla de fichas Indice del Tema 1103
1101 1102 1103 1104 1105 1106 1107 1108

ASIGNACIÓN DINÁMICA Y ESTRUCTURAS NO LINEALES







Introducción. Concepto de nodo.
Los elementos de que constan las estructuras no lineales se conocen con el nombre de nodos. Un nodo es una estructura, con una particularidad: al menos uno de los campos de la estructura es un puntero de una estructura de ese mismo tipo. Esto es,la forma de un nodo es la siguiente:
struct Nodo {
 struct Nodo * puntero;
 /*
  Otros campos...
 */
};
Aunque pueda parecer una definición incorrecta, por ser recursiva, no lo es, porque el primer campo es un struct Nodo *, que no es lo mismo que un struct Nodo. Todos los punteros ocupan 4 bytes, así el "coste" adicional de un puntero suele ser una fracción pequeña del tamaño total del nodo.
Un ejemplo más real de nodo puede ser el siguiente:
struct Nodo {
 /* Campos de información */
 char modelo[10];
 int fecha;
 char fabricante[20];
 float peso;
 /* Campo de enlace */
 struct Nodo * sig;
};
Este nodo tiene como último campo un puntero llamado sig; como cabía esperar es un puntero de struct Nodo. Obsérvese que los nodos no son otra cosa que registros de una base de datos dotados de uno o más punteros adicionales, destinados a crear estructuras de datos no lineales más flexibles que las tradicionales.
struct Nodo2 {
 /* Campos de información */
 char modelo[10];
 int fecha;
 char fabricante[20];
 float peso;
 /* Campos de enlace */
 struct Nodo * ant;
 struct Nodo * sig;
};
Otra posibilidad es dotar al nodo de dos punteros, en lugar de uno. Esto exige que la estructura asociada posea dos campos de enlace en lugar de uno; el coste de memoria sigue sin ser excesivo y supone ventajas importantes, como comprobará más adelante.
La estructura de la izquierda está dotada de los dos campos de enlace mencionados. Este tipo de nodo se emplea para construir listas doblemente enlazadas.

Los campos de enlace son la base de la construcción de estructuras dinámicas no lineales. Una de las estructuras no lineales más importantes es la lista enlazada, que se estudia a continuación.

Concepto de lista enlazada.
Las estructuras lineales se caracterizan porque sus elementos ocupan posiciones contiguas de memoria. La ventaja de esta distribución es la gran rapidez de acceso que permite, y la desventaja es la facilidad con que se puede fragmentar el cúmulo, al emplearse bloques de memoria relativamente grandes. Para evitar el problema de fragmentación del cúmulo se puede emplear una lista enlazada como la que puede verse a continuación.
Lista simplemente enlazadaUna lista enlazada es una colección de nodos organizados de tal modo que un campo de enlace del primer nodo de la lista contiene la dirección del segundo, el campo de enlace del segundo nodo contiene la dirección del tercero, y así sucesivamente hasta llegar al último nodo de la lista. Este último nodo posee también un campo de enlace, pero el valor de ése campo es NULL, para indicar que allí finaliza la lista. En la imagen, el campo de enlace se representa como una flecha que sale del primer nodo (a la izquierda) y llega al segundo, otra flecha que sale del segundo nodo y llega al tercero, etc. El nodo final (situado en el extremo derecho) se ha representado con un campo de enlace sin flecha, cruzado por una línea diagonal.
Las listas enlazadas admiten múltiples operaciones, entre las cuales cabe mencionar las siguientes:
Para facilitar estas operaciones, es útil contar con dos punteros que señalarán siempre el primer y último nodo de la lista.El puntero de nodo inicial nos permite conocer en todo momento la dirección del primer elemento de la lista; a partir de él será posible recorrer al lista sin más que ir pasando al nodo siguiente mediante un código similar a este:
struct Nodo temp = ppio;
while (temp->sig != NULL)
	{
		printf("%d", temp->dato);
		temp = temp->sig;
	}
Por su parte, el puntero de nodo final marca la posición del último nodo de la lista, y permite añadir nodos al final de la lista sin recorrerla. Si no mantiene el puntero de fin de lista, será preciso determinar su posición mediante un código análogo al fragmento anterior.
struct Nodo temp = ppio;
while (temp->sig != NULL)
	{
		temp = temp->sig;
	}
En efecto, téngase en cuenta que temp queda apuntando al último elemento de la lista (aquel tal que su campo de nodo siguiente tiene el valor NULL).
Lista con principio y fin La utilidad de estos punteros se apreciará mejor en las próximas secciones. Ciertamente, se puede prescindir del puntero del nodo final, simplificándose en cierto modo las operaciones, pero esto tiene un coste: la adición de nodos al final de la lista pasa a ser una operación mucho más costosa que la adición de nodos al principio.
Inserción y eliminación de nodos en una lista simplemente enlazada
El problema de la inserción tiene tiene varias posibilidades relevantes, que se exponen en la tabla siguiente. Obsérvese que, finalmente, la inserción de un nodo en una lista enlazada se reduce a cuatro posibles acciones, asociadas a las operaciones de inserción al principio, inserción al final o inserción en posición intermedia. Visto de otra manera, la inserción de un nodo en una lista conlleva las operaciones (y tareas) siguientes, en donde los números denotan las tareas descritas después.

Tipo de lista Operaciones de inserción
Al principio Al final Ordenada
Lista vacía 1 1 1
Un único nodo 2 3 2, 3
Dos o más nodos 2 3 4

Descripción de las tareas de inserción.
En términos de estas tareas, los algoritmos de inserción en una lista enlazada pueden expresarse en la forma siguiente:
Algoritmo de inserción al principio.
Si la lista está vacía, se ejecuta Tarea_1. Si no está vacía, se ejecuta la Tarea_2. Internamente, añadir un nodo al principio conlleva mantener la integridad de los punteros ppio y fin. Si la lista está vacía, es preciso modificar el valor de ppio y el de fin, para hacer que el valor de ambos pase a ser la dirección del nuevo (y único) nodo. Si la lista no está vacía, entonces sólo es preciso modificar el puntero ppio, que debe tomar como valor la dirección del nuevo nodo. Por supuesto, el puntero de nodo siguiente de éste nuevo nodo debe tomar como valor la dirección del que antes fuera primer nodo. En este segundo caso, el puntero fin no resulta modificado. De aquí vienen las tareas 1 y 2 mencionadas.
Algoritmo de inserción al final.
Si la lista está vacía, se ejecuta la Tarea_1. Si no está vacía, se ejecuta la Tarea_3. Internamente, añadir un nodo al final conlleva mantener la integridad de los punteros ppio y fin. Si la lista está vacía, es preciso modificar el valor de ppio y el de fin, para hacer que el valor de ambos pase a ser la dirección del nuevo (y único) nodo. Si la lista no está vacía, entonces sólo es preciso modificar el puntero fin, que debe tomar como valor la dirección del nuevo nodo. Por supuesto, el puntero de nodo siguiente del que fuera el último nodo debe tomar como valor la dirección del nuevo nodo final. En este segundo caso, el puntero ppio no resulta afectado. De aquí vienen las tareas 1 y 3 mencionadas.
Algoritmo de inserción en posición intermedia.
Si la lista está vacía, se ejecutta Tarea_1 y hemos terminado. Si no está vacía, se comprueba si el nodo añadido es anterior al primero. Si es anterior al primero, se ejecuta la Tarea_2 y hemos terminado. Si no es anterior al primero, se comprueba si es posterior al último. Si es posterior al último, se ejecuta la Tarea_3 y hemos terminado. Si no es posterior al último (tampoco era anterior al primero), entonces se ejecuta la Tarea_4.

La Tarea_4 se especializa en buscar la posición del nodo en listas de dos o más nodos. Una vez hallado el puntero de nodo anterior, se procede a la inserción del nodo, manteniendo siempre la integridad de la lista. Esto exige dejar la lista en un estado adecuado para que sea posible seguir añadiendo o eliminando nodos.

Téngase en cuenta que la Tarea_4 sólo llega a ejecutarse cuando hay dos o más nodos en la lista. En efecto, el primer nodo añadido dará lugar a la ejecución de la Tarea_1 exclusivamente. El segundo nodo añadido da lugar a la ejecución de Tarea_2 o de Tarea_3, porque su valor sólo puede ser menor o igual que el almacenado en el nodo señalado por ppio, y entonces se añade delante de éste (Tarea_2) o bien mayor o igual que el valor almacenado en el nodo señalado por ppio, y entonces se añadirá detrás de ppio (Tarea_3). Al añadirse un tercer nodo se abre la posibilidad de ejecutar la Tarea_4, pero únicamente a partir del tercer nodo.


El problema de la eliminación de nodos en una lista enlazada se puede abordar en dos fases: búsqueda y eliminación. El primer paso de la búsqueda consiste en determinar si la lista está vacía. De ser así, el nodo no está presente en ella; esto vamos a denominarlo Caso 0, y el puntero proporcionado será NULL.

Si la lista no está vacía, se determina si hay un único nodo. Si ese único nodo es el que debe eliminarse, se denomina Caso 1 y se devuelve el puntero ppio.

Si el primer y único nodo no es el buscado, se tiene de nuevo el Caso 0 y se devuelve el puntero NULL.

Si no hay un sólo nodo, entonces hay dos o más. Si el nodo buscado es el primero, se denomina Caso 2 y se devuelve el puntero ppio. Si el nodo buscado es el último, se denomina Caso 3 y se devuelve el puntero del penúltimo nodo.



En caso contrario tiene una lista con dos o más nodos, de la cual se sabe que el nodo buscado no es el primero ni el último. Si sólo hay dos nodos, entonces el nodo buscado no existe. Si hay tres o más nodos, se hace una búsqueda del posible nodo anterior al buscado, en que se recorre la lista con dos punteros, uno de nodo anterior (que inicialmente es igual a ppio) y otro de nodo examinado (que inicialmente apunta a ppio->sig, lo cual tiene sentido porque hay dos o más nodos). El recorrido se detiene cuando el nodo examinado es fin, o cuando el nodo examinado tiene el valor buscado. Si al finalizar el recorrido se observa que el nodo examinado tiene el valor buscado, entonces el puntero de nodo anterior nos permite efectuar la eliminación. Si no está en la lista, se tiene de nuevo el Caso 0 y se devuelve el puntero NULL.










Fase de Eliminación
Una vez concluida la búsqueda, se realiza la eliminación (si procede). Es preciso estudiar los casos del 1 al 4; el Caso 0 es trivial y no es preciso hacer nada.

Caso 1.- Es preciso eliminar el único nodo de la lista. Basta entonces con liberar el nodo y dar a ppio y fin el valor NULL. De esta manera la lista queda vacía y en un estado coherente; si se intenta añadir o eliminar un nodo, será posible detectar que no hay otros nodos ya existentes. Es imprescindible mantener la coherencia de la lista, dejándola siempre dispuesta para que funcione correctamente en operaciones posteriores.

Caso 2.- Es preciso eliminar el primer nodo de una lista con dos o más nodos. Basta hacer que ppio avance al nodo siguiente y eliminar el (viejo) nodo inicial. Téngase en cuenta la necesidad de disponer del puntero del (eliminado) primer nodo para liberar la memoria asociada al mismo. Por tanto, primero se toma nota del valor de ppio. Después, se hace que ppio pase a valer ppio->sig. Finalmente, se libera el primer nodo, de cuya dirección se ha tomado nota previamente.
Caso 3.- Es preciso eliminar el último nodo de una lista con dos o más nodos. Esto implica dar al puntero de nodo siguiente del penúltimo nodo el valor NULL, para mantener la integridad de la lista, y hacer que fin apunte al (nuevo) último nodo. Todo es fácil porque se nos proporciona el puntero del penúltimo nodo. La primera operación consiste en liberar la memoria asociada al último nodo, señalado por fin. La segunda consiste en hacer que el puntero fin tome como valor el del puntero del penúltimo nodo, que nos ha proporcionado la operación de búsqueda. De este modo se mantiene la integridad de la lista.

Caso 4.- Esta consiste en hacer que el nodo anterior al eliminado (el examinado) pase a tener como nodo siguiente el nodo siguiente al eliminado. Si el nodo eliminado es el penúltimo de la lista, habrá que modificar el valor de fin, además de efectuar las operaciones habituales de eliminación. Esta eliminación es posible porque se recibe el puntero del nodo anterior al eliminado, obtenido en la fase de búsqueda. Es preciso seguir un orden de operaciones, como en el caso anterior. En primer lugar, se toma nota del puntero del nodo eliminado. A continuación, se hace que el nodo señalado por el puntero de nodo anterior tenga como nodo siguiente el nodo que sigue al eliminado. Por último, se libera la memoria del nodo eliminado.


Ejercicios propuestos



Para todos los ejercicios siguientes, las estructuras de datos empleadas como base son las siguientes:

struct Nodo {
 int dato;
 struct Nodo * sig;
};
struct Lista {
	struct Nodo *ppio, *fin;
};


  1. Ejercicio 1103r01.- Sea una lista simplemente enlazada y basada en las estructuras anteriores. Se pide construir una función que admita como argumento un puntero de Lista y el puntero de un nodo cuyos campos ya están cumplimentados, y que realice la tarea de insertar un nodo único (tarea 1). Se supone que la lista está vacía y no es preciso comprobarlo.

  2. Ejercicio 1103r02.- Sea una lista simplemente enlazada y basada en las estructuras anteriores. Se pide construir una función que admita como argumento un puntero de Lista y el puntero de un nodo cuyos campos ya están cumplimentados, y que realice la tarea de poner un nodo en primera posición (tarea 2). Se supone que la lista no está vacía y no es preciso comprobarlo.

  3. Ejercicio 1103r03.- Sea una lista simplemente enlazada y basada en las estructuras anteriores. Se pide construir una función que admita como argumento un puntero de Lista y el puntero de un nodo cuyos campos ya están cumplimentados, y que realice la tarea de poner un nodo en última posición (tarea 3). Se supone que la lista no está vacía y no es preciso comprobarlo.

  4. Ejercicio 1103r04.- Sea una lista simplemente enlazada y basada en las estructuras anteriores. Se pide construir una función que admita como argumento un puntero de Lista y el puntero de un nodo cuyos campos ya están cumplimentados, y que realice la tarea de poner un nodo en su posición ordenada dentro de la lista (tarea 4). Se supone que la lista posee dos o más nodos y no es preciso comprobarlo.

  5. Ejercicio 1103r05.- Construir una función adecuada para buscar un nodo con el fin de eliminarlo. La función recibe como argumentos un puntero de Lista y un puntero de nodo que contiene el valor deseado. Debe proporcionar dos datos como resultado:

    Una indicación de caso que seguirá la exposición anterior:

    Y un puntero cuyo valor es el siguiente:


  6. Ejercicio 1103r06.- Construir una función que reciba como argumento un puntero de Lista. La función debe eliminar el primer nodo de la lista, supuesto único.

  7. Ejercicio 1103r07.- Construir una función que reciba como argumento un puntero de Lista y un puntero, pen, que señala su penúltimo nodo. La función debe eliminar el último nodo de la lista.

  8. Ejercicio 1103r08.- Construir una función que admita como argumento un puntero de Lista y un puntero, ant, que señala el nodo anterior al que se desea eliminar. La función debe eliminar el nodo deseado.

  9. Ejercicio 1103r09.- Construir un programa adecuado para manejar la inserción de nodos en una lista ordenada, basado en las funciones construidas anteriormente.

  10. Ejercicio 1103r10.- Construir un programa adecuado para manejar la eliminación de nodos en una lista ordenada, basado en las funciones construidas anteriormente.

  11. Ejercicio 1103r11.- Construir una función que admita como argumento el nombre de un archivo de y proporcione como resultado el contenido del archivo, almacenando cada línea en un nodo de una lista enlazada. La función proporcionará su resultado mediante un puntero de Lista adecuado.

  12. Ejercicio 1103r12.- Se dispone de un archivo binario formado por estructuras de formato conocido. Construir un programa adecuado para leer todos los nodos del archivo, almacenándolos en una lista enlazada ordenada por el campo Apellido.