Bueno - Listas Enlazadas
Bueno - Listas Enlazadas
Bueno - Listas Enlazadas
1. Introducción
2. Memoria dinámica
3.1. Ejercicios
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.
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.
Memoria dinámica Volver arriba
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.
Si queremos reservar un número muy grande de bytes el programa nos dará error.
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;
};
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).
#include <stdio.h>
#include <conio.h>
#include <stdlib.h> //exit()
#include <alloc.h>
void mostrar_menu();
void anadir_elemento();
void mostrar_lista();
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);
}
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 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.
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.
Observaremos el ejemplo
#include <stdio.h>
void cambia (int *p);
void main()
{
int i = 10;
int *p;
p = &i;
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;
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.
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.
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:
-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>
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();
void imprimir_primos ()
{
primos *aux;
aux = primero;
getc(stdin);
}
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;
}
else printf ("\n\nERROR AL ABRIR EL FICHERO");
getc(stdin);
}
void guardar_fichero_txt ()
{
primos *aux;
FILE *f;
void contar ()
{
primos *aux;
unsigned long i = 0;
Volver arriba
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.
1. 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.
2. Eliminamos el último nodo (aux) y hacemos que ant apunte a NULL.
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.
1. El primer paso es buscar el elemento a borrar que será apuntado por aux y el
elemento anterior será apuntado por ant.
2. ant -> siguiente cogerá el valor de aux -> siguiente.
3. Liberamos aux.