Você está na página 1de 13

Definicin

Una lista es una estructura de datos secuencial.


Una manera de clasificarlas es por la forma de acceder al siguiente elemento:
- lista densa: la propia estructura determina cul es el siguiente elemento de la lista.
Ejemplo: un array.
- lista enlazada: la posicin del siguiente elemento de la estructura la determina el
elemento actual. Es necesario almacenar al menos la posicin de memoria del primer
elemento. Adems es dinmica, es decir, su tamao cambia durante la ejecucin del
programa.
Una lista enlazada se puede definir recursivamente de la siguiente manera:
- una lista enlazada es una estructura vaca o
- un elemento de informacin y un enlace hacia una lista (un nodo).
Grficamente se suele representar as:

Como se ha dicho anteriormente, pueden cambiar de tamao, pero su ventaja


fundamental es que son flexibles a la hora de reorganizar sus elementos; a cambio se
ha de pagar una mayor lentitud a la hora de acceder a cualquier elemento.
En la lista de la figura anterior se puede observar que hay dos elementos de
informacin, x e y. Supongamos que queremos aadir un nuevo nodo, con la
informacin p, al comienzo de la lista. Para hacerlo basta con crear ese nodo, introducir
la informacin p, y hacer un enlace hacia el siguiente nodo, que en este caso contiene
la informacin x.
Qu ocurre si quisiramos hacer lo mismo sobre un array?. En ese caso sera
necesario desplazar todos los elementos de informacin "hacia la derecha", para poder
introducir el nuevo elemento, una operacin muy engorrosa.

Implementacin
Para representar en lenguaje C esta estructura de datos se utilizarn punteros, un tipo
de datos que suministra el lenguaje. Se representar una lista vaca con la constante
NULL. Se puede definir la lista enlazada de la siguiente manera:
struct lista
{
int clave;
struct lista *sig;
};

Como se puede observar, en este caso el elemento de informacin es simplemente un


nmero entero. Adems se trata de una definicin autorreferencial. Pueden hacerse
definiciones ms complejas. Ejemplo:
struct cl
{
char nombre[20];
int edad;
};
struct lista
{
struct cl datos;
int clave;
struct lista *sig;
};
Cuando se crea una lista debe estar vaca. Por tanto para crearla se hace lo siguiente:
struct lista *L;
L = NULL;

Operaciones bsicas sobre listas


- Insercin al comienzo de una lista:
Es necesario utilizar una variable auxiliar, que se utiliza para crear el nuevo nodo
mediante la reserva de memoria y asignacin de la clave. Posteriormente es necesario
reorganizar los enlaces, es decir, el nuevo nodo debe apuntar al que era el primer
elemento de la lista y a su vez debe pasar a ser el primer elemento.
En el siguiente ejemplo se muestra un programa que crea una lista con cuatro
nmeros. Notar que al introducir al comienzo de la lista, los elementos quedan
ordenados en sentido inverso al de su llegada. Notar tambin que se ha utilizado un
puntero auxiliar p para mantener correctamente los enlaces dentro de la lista.
#include <stdlib.h>
struct lista
{
int clave;
struct lista *sig;
};
int main(void)
{
struct lista *L;
struct lista *p;
int i;
L = NULL; /* Crea una lista vacia */
for (i = 4; i >= 1; i--)

{
/* Reserva memoria para un nodo */
p = (struct lista *) malloc(sizeof(struct lista));
p->clave = i; /* Introduce la informacion */
p->sig = L; /* reorganiza */
L = p;
/* los enlaces */
}
return 0;
}
- Recorrido de una lista.
La idea es ir avanzando desde el primer elemento hasta encontrar la lista vaca. Antes
de acceder a la estructura lista es fundamental saber si esa estructura existe, es decir,
que no est vaca. En el caso de estarlo o de no estar inicializada es posible que el
programa falle y sea difcil detectar donde, y en algunos casos puede abortarse
inmediatamente la ejecucin del programa, lo cual suele ser de gran ayuda para la
depuracin.
Como se ha dicho antes, la lista enlazada es una estructura recursiva, y una posibilidad
para su recorrido es hacerlo de forma recursiva. A continuacin se expone el cdigo de
un programa que muestra el valor de la clave y almacena la suma de todos los valores
en una variable pasada por referencia (un puntero a entero). Por el hecho de ser un
proceso recursivo se utiliza un procedimiento para hacer el recorrido. Ntese como
antes de hacer una operacin sobre el elemento se comprueba si existe.
int main(void)
{
struct lista *L;
struct lista *p;
int suma;
L = NULL;
/* crear la lista */
...
suma = 0;
recorrer(L, &suma);
return 0;
}
void recorrer(struct lista *L, int *suma)
{
if (L != NULL) {
printf("%d, ", L->clave);
*suma = *suma + L->clave;
recorrer(L->sig, suma);
}
}

Sin embargo, a la hora de hacer un programa, es ms eficaz si el recorrido se hace de


forma iterativa. En este caso se necesita una variable auxiliar que se desplace sobre la
lista para noperder la referencia al primer elemento. Se expone un programa que
hace la misma operacin que el anterior, pero sin recursin.

int main(void)
{
struct lista *L;
struct lista *p;
int suma;
L = NULL;
/* crear la lista */
...
p = L;
suma = 0;
while (p != NULL) {
printf("%d, ", p->clave);
suma = suma + p->clave;
p = p->sig;
}
return 0;
}
A menudo resulta un poco difcil de entender la instruccin p = p->sig; Simplemente
cambia la direccin actual del puntero p por la direccin del siguiente enlace. Tambin
es comn encontrar instrucciones del estilo: p = p->sig->sig; Esto puede traducirse en
dos instrucciones, de la siguiente manera:
p = p->sig;
p = p->sig;
Obviamente slo debe usarse cuando se sepa que p->sig es una estructura no vaca,
puesto que si fuera vaca, al hacer otra vez p = p->sig se producira una referencia a
memoria no vlida.
Y si queremos insertar en una posicin arbitraria de la lista o queremos borrar un
elemento? Como se trata de operaciones algo ms complicadas (tampoco mucho) se
expone su desarrollo y sus variantes en los siguientes tipos de listas: las listas
ordenadas y las listas reorganizables. Asimismo se estudiarn despus las listas que
incorporan cabecera y centinela. Tambin se estudiarn las listas con doble enlace.
Todas las implementaciones se harn de forma iterativa, y se deja propuesta por ser
ms sencilla su implementacin recursiva, aunque es recomendable utilizar la versin
iterativa.

Listas ordenadas
Las listas ordenadas son aquellas en las que la posicin de cada elemento depende de
su contenido. Por ejemplo, podemos tener una lista enlazada que contenga el nombre
y apellidos de un alumno y queremos que los elementos -los alumnos- estn en la lista
en orden alfabtico.
La creacin de una lista ordenada es igual que antes:
struct lista *L;
L = NULL;
Cuando haya que insertar un nuevo elemento en la lista ordenada hay que hacerlo en
el lugar que le corresponda, y esto depende del orden y de la clave escogidos. Este

proceso se realiza en tres pasos:


1.- Localizar el lugar correspondiente al elemento a insertar. Se utilizan dos
punteros: anterior y actual, que garanticen la correcta posicin de cada enlace.
2.- Reservar memoria para l (puede hacerse como primer paso). Se usa un puntero
auxiliar (nuevo) para reservar memoria.
3.- Enlazarlo. Esta es la parte ms complicada, porque hay que considerar la diferencia
de insertar al principio, no importa si la lista est vaca, o insertar en otra posicin. Se
utilizan los tres punteros antes definidos para actualizar los enlaces.
A continuacin se expone un programa que realiza la insercin de un elemento en una
lista ordenada. Suponemos claves de tipo entero ordenadas ascendentemente.
#include <stdio.h>
#include <stdlib.h>
struct lista
{
int clave;
struct lista *sig;
};
/* prototipo */
void insertar(struct lista **L, int elem);
int main(void)
{
struct lista *L;
L = NULL; /* Lista vacia */
/* para probar la insercion se han tomado 3 elementos */
insertar(&L, 0);
insertar(&L, 1);
insertar(&L, -1);
return 0;

}
void insertar(struct lista **L, int elem)
{
struct lista *actual, *anterior, *nuevo;

/* 1.- se busca su posicion */


anterior = actual = *L;
while (actual != NULL && actual->clave < elem) {
anterior = actual;
actual = actual->sig;
}
/* 2.- se crea el nodo */
nuevo = (struct lista *) malloc(sizeof(struct lista));
nuevo->clave = elem;
/* 3.- Se enlaza */
if (anterior == NULL || anterior == actual) { /* inserta al principio

*/

nuevo->sig = anterior;
*L = nuevo; /* importante: al insertar al principio actuliza la
cabecera */

}
else {
nuevo->sig = actual;
anterior->sig = nuevo;
}

/* inserta entre medias o al final */

}
Se puede apreciar que se pasa la lista L con el parmetro **L . La razn para hacer
esto es que cuando se inserta al comienzo de la lista (porque est vaca o es donde
corresponde) se cambia la cabecera.
Un ejemplo de prueba: suponer que se tiene esta lista enlazada: 1 -> 3 -> 5 -> NULL
Queremos insertar un 4. Al hacer la bsqueda el puntero actual apunta al 5. El
puntero anterior apunta al 3. Y nuevo contiene el valor 4. Como no se inserta al
principio se hace que el enlace siguiente a nuevo sea actual, es decir, el 5, y el enlace
siguiente a anterior ser nuevo, es decir, el 4.
La mejor manera de entender el funcionamiento es haciendo una serie de seguimientos
a mano o con la ayuda del depurador.
A continuacin se explica el borrado de un elemento. El procedimiento consiste en
localizarlo y borrarlo si existe. Aqu tambin se distingue el caso de borrar al principio o
borrar en cualquier otra posicin. Se puede observar que el algoritmo no tiene ningn
problema si el elemento no existe o la lista est vaca.
void borrar(struct lista **L, int elem)
{
struct lista *actual, *anterior;
/* 1.- busca su posicion. Es casi igual que en la insercion, ojo al (<)
*/
anterior = actual = *L;
while (actual != NULL && actual->clave
< elem) {
anterior = actual;
actual = actual->sig;
}
/* 2.- Lo borra si existe */
if (actual != NULL && actual->clave == elem) {
if (anterior == actual)
/* borrar el primero */
*L = actual->sig;
/* o tambien (*L)->sig; */
else
/* borrar en otro sitio */
anterior->sig = actual->sig;
free(actual);
}
}

Ejemplo: para borrar la clave '1' se indica as: borrar(&L, 1);

Listas reorganizables

Las listas reorganizables son aquellas en las que cada vez que se accede a un
elemento ste se coloca al comienzo de la lista. Si el elemento al que se accede no
est en la lista entonces se aade al comienzo de la misma. Cuando se trata de borrar
un elemento se procede de la misma manera que en la operacin de borrado de la lista
ordenada. Notar que el orden en una lista reorganizable depende del acceso a un
elemento, y no de los valores de las claves.
No se va a desarrollar el procedimiento de insercin / acceso en una lista, se deja
como ejercicio. De todas formas es sencillo. Primero se busca ese elemento, si existe
se pone al comienzo de la lista, con cuidado de no perder los enlaces entre el
elemento anterior y el siguiente. Y si no existe pues se aade al principio y ya est. Por
ltimo se actualiza la cabecera.

Cabecera ficticia y centinela


Como se ha observado anteriormente, a la hora de insertar o actualizar elementos en
una lista ordenada o reorganizable es fundamental actualizar el primer elemento de la
lista cuando sea necesario. Esto lleva un coste de tiempo, aunque sea pequeo salvo
en el caso de numerosas inserciones y borrados. Para subsanar este problema se
utiliza la cabecera ficticia.
La cabecera ficticia aade un elemento (sin clave, por eso es ficticia) a la estructura
delante del primer elemento. Evitar el caso especial de insertar delante del primer
elemento. Grficamente se puede ver as:

Se declara una lista vaca con cabecera, reservando memoria para la cabecera, de la
siguiente manera:
struct lista {
int clave;
struct lista *sig;
}
...
struct lista *L;
L = (struct lista *) malloc(sizeof(struct lista));
L->sig = NULL;
Antes de implementar el proceso de insercin en una lista con cabecera, se explicar el
uso del centinela, y se realizarn los procedimientos de insercin y borrado
aprovechando ambas ideas.
El centinela es un elemento que se aade al final de la estructura, y sirve para acotar
los elementos de informacin que forman la lista. Pero tiene otra utilidad: el lector
habr observado que a la hora de buscar un elemento de informacin, ya sea en la

insercin o en el borrado, es importante no dar un paso en falso, y por eso se


comprueba que no se est en una posicin de informacin vaca. Pues bien, el
centinela evita ese problema, al tiempo que acelera la bsqueda.
A la hora de la bsqueda primero se copia la clave que buscamos en el centinela, y a
continuacin se hace una bsqueda por toda la lista hasta encontrar el elemento que
se busca. Dicho elemento se encontrar en cualquier posicin de la lista, o bien en el
centinela en el caso de que no estuviera en la lista. Como se sabe que el elemento est
en algn lugar de la lista (aunque sea en el centinela) no hay necesidad de comprobar
si estamos en una posicin vaca.
Cuando la lista est vaca la cabecera apunta al centinela. El centinela siempre se
apunta a si mismo. Esto se hace as por convenio.
Grficamente se puede representar as:

A continuacin se realiza una implementacin de lista enlazada ordenada, que incluye


a la vez cabecera y centinela.
struct lista
{
int clave;
struct lista *sig;
};
/* lista con cabecera y centinela */
struct listacc
{
struct lista *cabecera,
*centinela;
};

Procedimiento de inicializacin (ntese el *LCC):


void crearLCC(struct listacc *LCC)
{
LCC->cabecera = (struct lista *) malloc(sizeof(struct lista));
LCC->centinela = (struct lista *) malloc(sizeof(struct lista));
LCC->cabecera->sig = LCC->centinela;
LCC->centinela->sig = LCC->centinela; /* opcional, por convenio */
}

Procedimiento de insercin:
void insertarLCC(struct listacc LCC, int elem)
{

struct lista *anterior, *actual, *nuevo;


/* 1.- busca */
anterior = LCC.cabecera;
actual = LCC.cabecera->sig;
LCC.centinela->clave = elem;
while (actual->clave < elem) {
anterior = actual;
actual = actual->sig;
}
/* 2.- crea */
nuevo = (struct lista *) malloc(sizeof(struct lista));
nuevo->clave = elem;
/* 3.- enlaza */
nuevo->sig = actual;
anterior->sig = nuevo;
}
Procedimiento de borrado:
void borrarLCC(struct listacc LCC, int elem)
{
struct lista *anterior, *actual;
/* 1.- busca */
anterior = LCC.cabecera;
actual = LCC.cabecera->sig;
LCC.centinela->clave = elem;
while (actual->clave < elem) {
anterior = actual;
actual = actual->sig;
}
/* 2.- borra si existe */
if (actual != LCC.centinela && actual->clave == elem) {
anterior->sig = actual->sig;
free(actual);
}
}

Ejemplo de uso:
#include <stdio.h>
#include <stdlib.h>
struct lista
{
int clave;
struct lista *sig;
};
struct listacc
{
struct lista *cabecera,
*centinela;
};
void crearLCC(struct listacc *LCC);

void insertarLCC(struct listacc LCC, int elem);


void borrarLCC(struct listacc LCC, int elem);
int main(void)
{
struct listacc LCC;
crearLCC(&LCC);
insertarLCC(LCC, 3);
borrarLCC(LCC, 3);
return 0;
}
La realizacin de la lista reorganizable aprovechando la cabecera y el centinela se deja
propuesta como ejercicio.

Listas doblemente enlazadas


Son listas que tienen un enlace con el elemento siguiente y con el anterior. Una ventaja
que tienen es que pueden recorrerse en ambos sentidos, ya sea para efectuar una
operacin con cada elemento o para insertar/actualizar y borrar. La otra ventaja es que
las bsquedas son algo ms rpidas puesto que no hace falta hacer referencia al
elemento anterior. Su inconveniente es que ocupan ms memoria por nodo que una
lista simple.
Se realizar una implementacin de lista ordenada con doble enlace que aproveche el
uso de la cabecera y el centinela. A continuacin se muestra un grfico que muestra
una lista doblemente enlazada con cabecera y centinela, para lo que se utiliza un nico
nodo que haga las veces de cabecera y centinela.

- Declaracin:
struct listaDE
{
int clave;
struct listaDE *ant,
*sig;
};

- Procedimiento de creacin:
void crearDE(struct listaDE **LDE)
{
*LDE = (struct listaDE *) malloc(sizeof(struct listaDE));
(*LDE)->sig = (*LDE)->ant = *LDE;
}

- Procedimiento de insercin:
void insertarDE(struct listaDE *LDE, int elem)
{
struct listaDE *actual, *nuevo;
/* busca */
actual = LDE->sig;
LDE->clave = elem;
while (actual->clave < elem)
actual = actual->sig;
/* crea */
nuevo = (struct listaDE *) malloc(sizeof(struct listaDE));
nuevo->clave = elem;
/* enlaza */
actual->ant->sig = nuevo;
nuevo->ant = actual->ant;
nuevo->sig = actual;
actual->ant = nuevo;
}

- Procedimiento de borrado:
void borrarDE(struct listaDE *LDE, int elem)
{
struct listaDE *actual;
/* busca */
actual = LDE->sig;
LDE->clave = elem;
while (actual->clave < elem)
actual = actual->sig;
/* borra */
if (actual != LDE && actual->clave == elem) {
actual->sig->ant = actual->ant;
actual->ant->sig = actual->sig;
free(actual);
}
}

Para probarlo se pueden usar las siguientes instrucciones:


struct listaDE *LDE;
...
crearDE(&LDE);
insertarDE(LDE, 1);
borrarDE(LDE, 1);

Listas circulares
Las listas circulares son aquellas en las que el ltimo elemento tiene un enlace con el
primero. Su uso suele estar relacionado con las colas, y por tanto su desarrollo se
realizar en el tema de colas. Por supuesto, se invita al lector a desarrollarlo por su
cuenta.

Algoritmos de ordenacin de listas

* Un algoritmo muy sencillo:


Se dispone de una lista enlazada de cualquier tipo cuyos elementos son todos
comparables entre s, es decir, que se puede establecer un orden, como por ejemplo
nmeros enteros. Basta con crear una lista de tipo ordenada e ir insertando en ella los
elementos que se quieren ordenar al tiempo que se van borrando de la lista original
sus elementos. De esta manera se obtiene una lista ordenada con todos los elementos
de la lista original. Este algoritmo se llama Insercin Directa; ver Algoritmos de
Ordenacin. La complejidad para ordenar una lista de n elementos es: cuadrtica en el
peor caso (n * n) -que se da cuando la lista inicial ya est ordenada- y lineal en el
mejor (n) -que se da cuanda la lista inicial est ordenada de forma inversa.
Para hacer algo ms rpido el algoritmo se puede implementar modificando los enlaces
entre los elementos de la lista en lugar de aplicar la idea propuesta anteriormente, que
requiere crear una nueva lista y borrar la lista no ordenada.
El algoritmo anterior es muy rpido y sencillo de implementar, pues ya estn creadas
las estructuras de listas ordenadas necesarias para su uso. Eso s, en general es
ineficaz y no debe emplearse para ordenar listas grandes. Para ello se emplea la
ordenacin por fusin de listas.
* Un algoritmo muy eficiente: ordenacin por fusin o intercalacin (ver Ordenacin
por fusin).

Problemas propuestos:

- La ordenacin por fusin no recursiva: consiste en desarrollar un algoritmo para


fusionar dos listas pero que no sea recursivo. No se trata de desarrollar una
implementacin iterativa del programa anterior, sino de realizar una ordenacin por
fusin ascendente. Se explica mediante un ejemplo:
3 -> 2 -> 1 -> 6 -> 9 -> 0 -> 7 -> 4 -> 3 -> 8
se fusiona el primer elemento con el segundo, el tercero con el cuarto, etctera:
[(3) -> (2)] -> [(1) -> (6)] -> [(9) -> (0)] -> [(7) -> (4)] -> [(3) -> (8)]
queda:
2 -> 3 -> 1 -> 6 -> 0 -> 9 -> 4 -> 7 -> 3 -> 8
se fusionan los dos primeros (primera sublista) con los dos siguientes (segunda
sublista), la tercera y cuarta sublista, etctera. Observar que la quinta sublista se
fusiona con una lista vaca, lo cual no supone ningn inconveniente para el algoritmo
de fusin.
[(2 -> 3) -> (1 -> 6)] -> [(0 -> 9) -> (4 -> 7)] -> [(3 -> 8)]
queda:
1 -> 2 -> 3 -> 6 -> 0 -> 4 -> 7 -> 9 -> 3 -> 8
se fusionan los cuatro primeros con los cuatro siguientes, y aparte quedan los dos
ltimos:
[(1 -> 2 -> 3 -> 6) -> (0 -> 4 -> 7 -> 9)] -> [(3 -> 8)]
queda:
0 -> 1 -> 2 -> 3 -> 4 -> 6 -> 7 -> 9 -> 3 -> 8
se fusionan los ocho primeros con los dos ltimos, y el resultado final es una lista
totalmente ordenada:
0 -> 1 -> 2 -> 3 -> 3 -> 4 -> 6 -> 7 -> 8 -> 9

Para una lista de N elementos, ordena en el mejor y en el peor caso en un tiempo


proporcional a: NlogN. Observar que para ordenar una lista de 2 elementos requiere
un paso de ordenacin, una lista de 4 elementos requiere dos pasos de ordenacin,
una lista de 8 elementos requiere tres pasos de ordenacin, una lista de 16 requiere
cuatro pasos, etctera. Es decir:
log 2 = 1
log 4 = 2
log 8 = 3
log 16 = 4
log 32 = 5
De ah el logaritmo en base 2.
N aparece porque en cada paso se requiere recorrer toda la lista, luego el tiempo es
proporcional a NlogN.
Se pide: codificar el algoritmo de ordenacin por fusin ascendente.

Você também pode gostar