Bueno - Listas Enlazadas

Descargar como docx, pdf o txt
Descargar como docx, pdf o txt
Está en la página 1de 16

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

Introducción Volver arriba

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.
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.

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.

Si queremos reservar un número muy grande de bytes el programa nos dará error.

¿En qué consisten las listas enlazadas? Volver arriba

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.

Ejercicios: Volver arriba

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: Volver arriba


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.

Paso por valor: Volver arriba

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.

Paso por referencia: Volver arriba

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: Volver arriba

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


Números primos Volver arriba

(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);
}

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.

Suprimir por cabeza Volver arriba

1. 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.
2. Después haremos que primero apunte a primero -> siguiente. O sea, que el nuevo
primer elemento será el segundo (claro, si eliminamos al primero…).
3. Por último ya podemos liberar aux.
Volver arriba

Suprimir por el final

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.

Eliminación de un elemento cualquiera de una lista


Volver arriba
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.

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.

También podría gustarte