Listas enlazadas
1. Introducción
2. Memoria dinámica
2.1 Malloc y free
3. ¿En qué consisten las listas enlazadas?
3.1. Ejercicios
4. Paso de punteros a funciones
4.1 Paso por valor
4.2. Paso por referencia
5. Liberar la memoria de una lista enlazada
6. Solución a los ejercicios
6.1. Ejercicio de números primos
7. Eliminar un elemento de una lista enlazada
7.1. Suprimir por cabeza
7.2. Suprimir por el final
7.3. Eliminación de un elemento cualquiera de una lista enlazada
8. Ventajas y desventajas de las listas enlazadas respecto a los arrays
9. Acabando
En esta segunda entrega vamos a ver lo que son las listas enlazadas y para ello trataremos la memoria dinámica. Es un tema muy utilizado en la práctica.
Antes de empezar hago una pequeña aclaración que seguramente es innecesaria:
Arrays = tabla = vector (si es de una dimensión)
Tupla = estructura = record
Lo dejo claro desde el principio por si os encontráis con alguna de estos términos, aunque intentaré no mezclarlos para no liar al personal.
Cuando queremos utilizar una variable la declaramos al principio del código y como ya sabemos tenemos varios tipos de variables. También sabemos que podemos agrupar estas variables en arrays. El inconveniente es que necesitamos saber el tamaño del array cuando lo declaramos.
Esto es una limitación muy grande, por ejemplo, si programamos una agenda deberemos saber a cuanta gente tendremos como máximo, o si nos diera por hacer un programa que calculase números primos tendríamos que poner un máximo (lamentablemente hay infinitos números primos J ); si reservábamos mucha memoria para curarnos en salud y nunca agotar una tabla, estamos desperdiciando mucha memoria; si por el contrario reservamos poca memoria podemos agotar la memoria que tenemos reservada.
Para optimizar la memoria, lo ideal sería reservar memoria en el momento que la necesitásemos y no como hasta la fecha, que lo hacíamos al principio del programa.
Estas dos funciones nos servirán para asignar dinámicamente memoria. Estas 2 funciones se encuentran en la librería <alloc.h>
| malloc(); |
Reserva memoria y devuelve la posición de memoria del comienzo de ésta, por lo que deberemos guardar el resultado en un puntero (más información de punteros en el número 13 de la revista de hackxcrack) . Si no se ha podido reservar memoria, malloc devolverá el valor NULL, así que el puntero apuntará a NULL. Es importante asegurarnos de que se ha podido reservar la memoria, haremos algo así: |
puntero = (Tipo_variable *) malloc (bytes_reservar);
if(puntero = (int *) malloc (sizeof(char)))
{puts (“Correcto”);} |
| free() |
Cuando ya no necesitemos el espacio que habíamos reservado, liberaremos la memoria, haremos: |
free(puntero);
Donde el puntero que pasamos como parámetro apunta al principio de la memoria que habíamos reservado.
Veremos un ejemplo:
#include <stdio.h>
#include <alloc.h>
void main()
{
unsigned long int bytes;
char *texto;
printf("Cuantos bytes quieres reservar: ");
scanf("%li",&bytes);
texto = (char *) malloc(bytes);
/* Comprobamos si ha tenido éxito la operación */
if (texto)
{
printf("Memoria reservada: %li bytes = %li kbytes = %li Mbytes\n",
bytes, bytes/1024,bytes/(1048576));
printf("El bloque comienza en la dirección: %p\n", texto);
/* Ahora liberamos la memoria */
free( texto );
}
else printf("No se ha podido reservar memoria\n");
}
|
Si queremos reservar un número muy grande de bytes el programa nos dará error.
¿En qué consisten las listas enlazadas? |
|
Aquí entran en juego varios temas, se trata de combinar las estructuras con los punteros para acabar por fin con la limitación de los arrays, ya no hará falta indicar el tamaño del array al principio. Después comentaremos los pros y los contras de las listas enlazas respecto a los arrays.
Las listas enlazadas pueden ser simples, dobles o circulares. De momento veremos las simples. Estas listas tendrán una estructura como la de la figura:

Para crear listas necesitaremos estructuras asignación dinámica de memoria. Vamos a ver como utilizar una lista en el ejemplo de la agenda:
struct _agenda {
char nombre[20];
char telefono[12];
struct _agenda *siguiente;
};
|
|
De esta forma hemos creado una estructura formada por un nombre y un teléfono como datos principales y visibles para el usuario, lo que sucede esta vez es que no sé a cuanta gente voy a meter en mi agenda, por lo que utilizo un puntero que apunta a otro elemento con esta estructura . Cada vez que queramos meter a un nuevo elemento en la agenda deberemos reservar memoria para este elemento con el malloc. |
nuevo = (_agenda *) malloc (sizeof(_agenda));
Nos quedará algo así:
Llenaremos los campos de ese nuevo elemento y lo meteremos en la lista. Para meter un elemento en la lista tendremos que hacer que el puntero del elemento anterior apunte a este nuevo elemento.
Si nos paramos a pensar ya vemos que necesitamos un puntero que nos vaya indicando donde está el elemento actual, pero es que también necesitaremos un puntero que nos indique donde comienza la lista para poder recorrerla desde el principio ya que en cada elemento de agenda tenemos un puntero hacia el siguiente elemento, pero no tenemos ninguno hacia el elemento anterior (si lo tuviéramos estaríamos hablando de una lista doblemente enlazada).
Ejemplo : Veremos un ejemplo que nos servirá de gran ayuda:
#include <stdio.h>
#include <conio.h>
#include <stdlib.h> //exit()
#include <alloc.h>
typedef struct _agenda {
char nombre[20];
char telefono[12];
_agenda *siguiente;
};
void mostrar_menu();
void anadir_elemento();
void mostrar_lista();
_agenda *primero, *ultimo;
/*De momento lo haremos con variables globales,
más adelante veremos como pasar punteros a funciones*/
void main() {
char opcion;
primero = (_agenda *) NULL;
ultimo = (_agenda *) NULL;
do {
mostrar_menu();
opcion = getch();
switch ( opcion ) {
case '1': anadir_elemento();
break;
case '2': printf("Ir pensando como hacer esto :D\n");
break;
case '3': mostrar_lista();
break;
case '4': exit( 1 );
default: printf( "Opción no válida\n" );
break;
}
} while (opcion!='4');
}
void mostrar_menu() {
printf("\n\nMenú:\n=====\n\n");
printf("1.- Añadir elementos\n");
printf("2.- Borrar elementos\n");
printf("3.- Mostrar lista\n");
printf("4.- Salir\n\n");
printf("Escoge una opción: ");fflush(stdin);
}
/* Con esta función añadimos un elemento al final de la lista */
void anadir_elemento() {
_agenda *nuevo;
/* reservamos memoria para el nuevo elemento */
nuevo = (_agenda *) malloc (sizeof(_agenda));
if (nuevo==NULL) printf( "No hay memoria disponible!\n");
else {
printf("\nNuevo elemento:\n");
printf("Nombre: "); fflush(stdin);
gets(nuevo->nombre);
printf("Teléfono: "); fflush(stdin);
gets(nuevo->telefono);
/* el campo siguiente va a ser NULL por ser el último elemento
la lista */
nuevo->siguiente = NULL;
/* ahora metemos el nuevo elemento en la lista. lo situamos
al final de la lista */
/* comprobamos si l0a lista está vacía. si primero==NULL es que no
hay ningún elemento en la lista. también vale ultimo==NULL */
if (primero==NULL) {
printf( "Primer elemento\n");
primero = nuevo;
ultimo = nuevo;
}
else {
/* el que hasta ahora era el último tiene que
apuntar al nuevo */
ultimo->siguiente = nuevo;
/* hacemos que el nuevo sea ahora el último */
ultimo = nuevo;
}
}
}
void mostrar_lista() {
_agenda *auxiliar; /* lo usamos para recorrer la lista */
int i;
i=0;
auxiliar = primero;
printf("\nMostrando la lista completa:\n");
while (auxiliar!=NULL) {
printf( "Nombre: %s, Teléfono: %s\n",
auxiliar->nombre,auxiliar->telefono);
auxiliar = auxiliar->siguiente;
i++;
}
if (i==0) printf( "\nLa lista está vacía!!\n" );
}
|
En el ejemplo vemos una función por terminar, la de eliminar un elemento. ¿Os atrevéis?
En este ejemplo también vemos algo nuevo, “->”, esta flecha la utilizamos en punteros cuando nos referimos a algún campo apuntado por el puntero. Por ejemplo,
ultimo->siguiente = nuevo;
En esta línea decimos que el puntero que siguiente (apuntado por ultimo) coge el valor de nuevo. Recordemos que último está apuntando a toda una estructura, con la flecha nos podemos referir a cada uno de los campos.
1-Crear la función eliminar un elemento de la lista enlazada.
2-Crear un programa para calcular números primos y almacenarlos en una lista enlazada y de esta forma poder seguir calculando números hasta que queramos. Podéis utilizar la función kbhit() (esta función no es ANSI C, C estándar) que nos devuelve cierto cuando pulsamos una tecla. Así podemos calcular números primos hasta que pulsemos una tecla. Si no queréis utilizar kbhit() podéis calcular números primos hasta un cierto número.
Paso de punteros a funciones: |
|
En este tema hemos utilizado variables globales, algo que como ya hemos comentado durante el curso debemos evitar. En este apartado veremos como pasar punteros a funciones por valor y por referencia para que no necesitemos recurrir a variables globales.
Observaremos el ejemplo
#include <stdio.h>
void cambia (int *p);
void main()
{
int i = 10;
int *p;
p = &i;
printf ("\n1-MAIN\nDirección: %p\nValor: %i", p,*p);
cambia (p);
printf ("\n\n3-MAIN\nDespués de cambiar\nDirección: %p
\nValor: %i",p, *p);
}
void cambia (int *p) {
int j = 20;
p = &j;
printf ("\n\n2-FUNCION\nDirección: %p\nValor: %i", p,*p);
}
|
En este ejemplo vemos que el paso por valor lo hacemos igual que hasta ahora, hay que tener en cuenta que en la definición de la función ponemos int *p porque este es el tipo de variable.
Veremos el ejemplo anterior modificado para que ahora sea paso por referencia:
#include <stdio.h>
void cambia (int **p);
void main()
{
int i = 10;
int *p;
p = &i;
printf ("\n1-MAIN\nDirección: %p\nValor: %i", p,*p);
cambia (&p);
printf ("\n\n3-MAIN\nDespués de cambiar\n
Dirección: %p\nValor: %i",p, *p);
}
void cambia (int **p)
{
int j = 20;
*p = &j;
printf ("\n\n2-FUNCION\nDirección: %p\nValor: %i", *p,**p);
}
|
Al igual que hacemos con el resto de variables, en la llamada añadimos &: cambia (&p). En los parámetros añadimos un * en la variable y en el cuerpo de la función añadimos un * cuando queremos cambiar su valor y al imprimirlo.
Liberar la memoria de una lista enlazada: |
|
Ya hemos utilizado la función free para liberar la memoria que ocupa un puntero cuando ya no lo necesitamos, vamos a ver un ejemplo de cómo liberar la memoria de toda una lista enlazada:
void liberar()
{
primos *aux, *ant;
ant = primero;
for (aux = primero->siguiente; aux != NULL; aux = aux->siguiente)
{
free (ant);
ant = aux;
}
}
|
Vemos en el ejemplo que no basta sólo con una variable auxiliar, hemos utilizado aux y ant ya que si liberamos aux ya no podremos pasar al siguiente elemento.
Es muy recomendable liberar la memoria cuando ya no la necesitemos.
Solución a los ejercicios
(Lo publiqué hace algún tiempo en el foro de hackxcrack).
http://www.hackxcrack.com/phpBB2/viewtopic.php?p=55255#55255
He hecho que el programa sea ANSI C para que así lo compile cualquier compilador. De todas formas he dejado una función como comentario para que no se compile ya que no es ANSI C, no la he borrado porque me parece interesante tenerla.
No os asustéis, son bastantes líneas pero es que le he metido muchas opciones. Hay unas limitaciones o puntos débiles de este programa que podrían ser mejorados, son:
Puntos débiles de mi programa:
-Ocupa mucha memoria, ocupa más el puntero que apunta al siguiente elemento de la lista que el propio número primo.
-Los ficheros son de texto y podría haber la opción de crear el fichero el binario, así ocuparía menos memoria y sería más rápido. Aunque de esta forma no se podrían abrir con el bloc de notas.
He hecho un par de punteros como variables globales y eso de las variables globales no está muy bien visto. Pongo la excusa de que es para no complicar más el código .
Sugerencias:
-Podríamos tener la opción de que nos dijera si un número concreto es primo. Si el número ya está en el fichero no habría problema, en caso contrario podríamos leer hasta la raíz del número que nos han dicho y comprobarlo.
-En la estructura podríamos meter por ejemplo un vector de 100 longs y puntero hacia el siguiente elemento, de esta forma ahorraríamos mucha memoria pero se complicaría bastante el algoritmo: deberíamos ir recorriendo los números del vector y a su vez los vectores de la lista, además lo más probable es que el último vector de la lista no estuviera lleno, por lo que tendríamos que usar un centinela.
#include <math.h>
#include <stdlib.h>//malloc, free
#include <stdio.h>
//#include <conio.h> //kbhit, clrscr (No es ANSIC)
#include <time.h>
enum boolean {FALSE, TRUE};
typedef struct _primos{ //Así lo puedo referenciar aquí dentro
unsigned long int num;
struct _primos *siguiente; //Aún no está definido el tipo
} primos; //Nombre del nuevo tipo
primos *primero, *ultimo; //Estas son las variables globales
void imprimir_primos ();
void liberar();
boolean esprimo (unsigned long int i);
boolean insertar_primo (unsigned long int x);
void leer_fichero_txt ();
void guardar_fichero_txt();
void menu();
void contar();
void main()
{
unsigned long int i=3, max;
char opcion;
boolean salir = FALSE;
clock_t start, end;
start = clock();
//1 primo//
primero = (primos *) malloc (sizeof (primos));
primero -> siguiente = NULL;
primero -> num = 1;
ultimo = primero;
/////
insertar_primo (2); /*caso raro, para tener referencia*/
do{
// clrscr(); //No es ANSCI C
menu();
fflush (stdin);
opcion = getc(stdin);
switch (opcion)
{
//Esta función la he comentado por no ser ANSIC por el kbhit()//
/* case '1': puts ("CALCULANDO...");
start = clock();
for (; !kbhit(); i+=2 )
{
if (esprimo(i))
{
if (!insertar_primo (i)) break;;
}
}
end = clock();
printf ("\aFINALIZADO\n\nÚltimo nº comprobado:
%li\nTiempo empleado: %f\nPresiona una tecla para continuar"
, i, (end-start)/ CLK_TCK);
fflush(stdin);
getc(stdin);
// break; */
case '2': printf ("\n Hasta qué nº quieres buscar? ");
scanf ("%li", &max);
puts ("CALCULANDO...");
start = clock();
for (; i < max; i=i+2 )
{
if (esprimo(i))
{
if (!insertar_primo (i)) break;
}
}
end = clock ();
printf ("\aFINALIZADO\n\nÚltimo nº comprobado:
%li\nTiempo empleado:
%f\nPresiona una tecla para continuar", i, (end-star
t) / CLK_TCK);
fflush(stdin);
getc(stdin);
break;
case '3': leer_fichero_txt (); break;
case '4': guardar_fichero_txt (); break;
case '5': imprimir_primos (); break;
case '6': salir = TRUE; break;
case '7': contar(); break;
default : puts ("OPCION INCORRECTA");
}
} while (!salir);
liberar();
}
boolean esprimo (unsigned long int i)
{
unsigned long int max;
primos *aux;
max = (long int) (sqrt (i));
//Probamos los menores o igual a la raíz
aux = primero->siguiente; /*apunto al 2*/
while (aux -> num <= max)
//aux no puede ser NULL pq nunca será el último elemento
{
if (i % aux -> num == 0) return FALSE;
aux = aux -> siguiente;
}
return TRUE;
}
void imprimir_primos ()
{
primos *aux;
aux = primero;
while (aux -> siguiente != NULL)
{
printf ("%5li ",aux->num);
aux = aux -> siguiente;
}
getc(stdin);
}
boolean insertar_primo (unsigned long int x)
{
primos *aux;
if ((aux = (primos *) malloc (sizeof (primos)))!=NULL)
//reservo memoria para siguiente primo
{
ultimo -> siguiente = aux; //último apunta al siguiente
ultimo = aux; //último primo es el nuevo
ultimo -> num = x; //inserto el número en último
ultimo -> siguiente = NULL; //último apunta a NULL
return TRUE;
}
else
{
printf ("\n\nOUT OF MEMORY\n");
return (FALSE);
}
}
void liberar()
{
primos *aux, *ant;
ant = primero;
for (aux = primero->siguiente; aux != NULL; aux = aux->siguiente)
{
free (ant);
ant = aux;
}
}
void menu()
{
puts ("\nMenu");
puts ("====\n");
//puts ("1-Calcular números primos hasta que presionemos una tecla");
//La opción1 utiliza kbhit que no es ANSIC
puts ("2-Calcular hasta un nº primo");
puts ("3-Cargar fichero de números primos");
puts ("4-Guardar primos en fichero");
puts ("5-Imprimir números primos calculados hasta el momento");
puts ("6-Salir");
puts ("7-Contar memoria");
}
void leer_fichero_txt ()
{
unsigned long i;
FILE *f;
if ((f = fopen ("primos.txt", "rt")) != NULL)
{
fscanf (f, "%li ", &i);
fscanf (f, "%li ", &i); //as¡ ya he leido hasta el 2
printf ("\n\nLeyendo fichero primos.txt");
while (!feof(f))
{
fscanf (f, "%li ", &i);
insertar_primo(i);
}
printf ("\nFichero leido\nPresiona cualquier tecla
para continuar");
fclose(f);
}
else printf ("\n\nERROR AL ABRIR EL FICHERO");
getc(stdin);
}
void guardar_fichero_txt ()
{
primos *aux;
FILE *f;
if ((f = fopen ("primos.txt", "wt")) != NULL)
{
printf ("\n\nGuardando primos.txt");
for (aux = primero; aux != NULL; aux = aux -> siguiente)
{
fprintf (f, "%li ", aux -> num);
}
printf ("\nFichero guardado\nPresiona cualquier
tecla para continuar");
fclose(f);
}
else (printf ("\n\nERROR AL GUARDAR EL FICHERO"));
getc(stdin);
}
void contar ()
{
primos *aux;
unsigned long i = 0;
for (aux = primero; aux != NULL; aux = aux -> siguiente)
{
i++;
}
printf ("\n\nHay un total de %li nums primos\n
Ocupan %li * %i bytes = %li bytes", i, i, sizeof(primos),
i*sizeof(primos));
getc(stdin);
}
|
Eliminar un elemento de una lista enlazada |
|
Este es un punto en el que pido concentración para no perdernos, si se imagina la situación es algo que resulta muy sencillo.
Veremos las diferentes situaciones que nos podemos encontrar a la hora de eliminar un elemento de una lista enlazada, además si se entiende podréis utilizar las mismas explicaciones para insertar un elemento en la lista enlazada en diferentes zonas.
Nos podemos encontrar con 3 casos: que el elemento sea el primero de la lista, que sea el último o que esté entre medio.
Usaremos el puntero “ primero ” que apunta al primer elemento de la lista, el puntero “ aux ” que será auxiliar y el puntero “ ant ” que será el anterior. No necesitaremos los 3 siempre. Analizamos los 3 casos paso a paso pero el código os lo dejo a vosotros.
- Necesitaremos 2 punteros. Se trata de eliminar el primer elemento pero si lo eliminamos directamente la lista quedará perdida en la memoria (no sabremos donde comienza). Así que haremos que guardaremos la posición del primer elemento en aux.
- Después haremos que primero apunte a primero -> siguiente. O sea, que el nuevo primer elemento será el segundo (claro, si eliminamos al primero…).
- Por último ya podemos liberar aux.



- Hay que encontrar el último nodo y eliminarlo pero también tendremos que buscar el penúltimo para hacer que apunte a NULL. Tendremos que recorrer toda la lista con aux y utilizar ant para que apunte al elemento anterior a aux.
- Eliminamos el último nodo (aux) y hacemos que ant apunte a NULL.
Eliminación de un elemento cualquiera de una lista enlazada |
|
Este caso es parecido al anterior, sólo se diferencia en que en vez de hacer que el elemento anterior al que eliminamos apunte a NULL deberemos hacer que apunte al elemento apuntado por el elemento que eliminamos. Ahora lo vemos con el dibujo (perdonad mi poca gracia dibujando). Supongamos que queremos borrar el segundo elemento.
- El primer paso es buscar el elemento a borrar que será apuntado por aux y el elemento anterior será apuntado por ant.
- ant -> siguiente cogerá el valor de aux -> siguiente.
- Liberamos aux.
Ventajas y desventajas de las listas enlazadas respecto a los arrays |
|
Bueno, después de haber visto las listas enlazadas no debemos pensar que son siempre mejores que los arrays, depende para que los vayamos a utilizar.
Ventajas de las listas enlazadas respecto a los arrays
- Tamaño dinámico. Lo que implica optimización de la memoria.
Desventajas de las listas enlazadas respecto a los arrays
- El acceso es secuencial (para llegar a una posición deberemos pasar por todas las anteriores). Esto significa lentitud. Imaginad por un momento el tinglado que tendríamos que montar para ordenar una lista enlazada. Si buscamos un elemento de una lista ordenada también tardaremos más, no vale la búsqueda dicotómica que vimos en la Ampliación 1 de C (métodos de clasificación en memoria dinámica).
- El código se complica.
Espero que esta breve explicación sea de vuestro agrado, ya sabéis que para cualquier sugerencia o duda me podéis encontrar en el foro de hackxcrack.
Este texto se puede distribuir libremente mencionando siempre al autor junto con mi Web
http://www.josemariabea.com
http://www.hackxcrack.com/phpBB2/index.php
Última revisión: 24 de febrero de 2006 |