Estructuras Dinámicas: Listas y Punteros
Estructuras Dinámicas: Listas y Punteros
Introducción
Las estructuras de datos son una forma de organizar los datos en la
computadora, de tal manera que nos permita realizar unas operaciones con ellas
de forma muy eficiente.
Las estructuras de datos pueden ser de dos tipos: Internas y externas. Además:
Estructura de datos estática: Una estructura de datos estática es aquella en la
que el tamaño ocupado en memoria se define antes de que el programa se
ejecute y no pueda modificarse dicho tamaño durante la ejecución del programa
entre las estructuras de datos estáticas se encuentran los array (vectores y
matrices).
Estructura de datos dinámica: Una estructura de datos dinámica es aquella en
la que el tamaño ocupado en memoria puede modificarse durante la ejecución del
programa.
Las variables que se crean y están disponibles durante la ejecución del programa
se llaman variables continuas.
De esta manera se pueden adquirir posiciones adicionales de memoria a medida
que se necesiten durante la ejecución del programa y liberarlas cuando no se
necesiten.
Las estructuras dinámicas de datos se pueden dividir en dos grandes grupos:
Lineales y no lineales.
1
Mapa conceptual
La lista enlazada nos permite almacenar datos de una forma organizada, al igual
que los vectores pero, a diferencia de estos, esta estructura es dinámica, por lo
que no tenemos que saber "a priori" los elementos que puede contener.
En una lista enlazada, cada elemento apunta al siguiente excepto el último que no
tiene sucesor y el valor del enlace es null. Por ello los elementos son registros
que contienen el dato a almacenar y un enlace al siguiente elemento. Los
elementos de una lista, suelen recibir también el nombre de nodos de la lista.
Una lista enlazada es una colección lineal de elementos llamados nodos. El orden
entre ellos se establece mediante punteros; direcciones o referencias a otros
nodos.
2
Un nodo está constituido por dos partes:
Un campo INFORMACIÓN: Que será del tipo de los datos que se quiera
almacenar en la lista.
3
Los datos se almacenan en forma dinámica en una lista enlazada; se crea cada
nodo según sea necesario. Un nodo puede contener datos de cualquier tipo,
incluyendo objetos de otras clases.
1.3.1 Punteros
Un puntero es una variable que contiene la dirección de memoria de otra
variable. Los punteros permiten código más compacto y eficiente; utilizándolos en
forma ordenada dan gran flexibilidad a la programación.
No hay que confundir una dirección de memoria con el contenido de esa dirección
de memoria.
Dirección
Punteros
Entonces los punteros tienen dos “elementos”.
Dirección de memoria en la que se almacena el valor
El valor en sí
En c++ podemos trabajar con ambos elementos por separado declarando la
variable como puntero. ¿Cómo declaramos un puntero?
int* a;
En este caso hemos declarado una variable a tipo puntero a entero, ¿qué quiere
decir esto? que a apuntará a un entero. Como no le hemos dado ningún valor,
inicialmente no está apuntando a nada. Observar siguiente código.
4
int c = 32;
int* a = &c;
int c = 32;
int* a = &c;
cout << a;
Contrariamente a lo que se podría esperar, no se imprimiría por pantalla
32, porque la variable a es una dirección de memoria. Entonces, ¿cómo
accedemos al valor al que apunta a? A través del operador dirección.
int c = 32;
int* a = &c;
cout << *a; //Muestra por pantalla 32.
Pero, ¿cuál es la gracia de los punteros?, veamos el siguiente ejemplo de código:
int c = 32;
int* a = &c;
cout << *a; // Muestra por pantalla 32.
c = 25;
cout << *a; // Muestra por pantalla 25.
*a = 16;
cout << c; // Muestra por pantalla 16.
Del mismo modo, a un puntero le podríamos asignar otro puntero.
int c = 32;
int* a = &c;
int* b = a; //a y b apuntan al valor de c
5
cout << c; // Muestra por pantalla 22.
cout << *a; // Muestra por pantalla 22.
cout << *b; // Muestra por pantalla 22.
Inicializando un puntero
En el ejemplo anterior hemos visto que a un puntero le podemos asignar la
dirección de otra variable del mismo tipo. Pero también podemos crear un
espacio nuevo de memoria.
Una lista enlazada simple necesita una estructura con varios campos, los campos
que contienen los datos necesarios (nombre y teléfono) y otro campo que contiene
un puntero a la propia estructura. Este puntero se usa para saber dónde está el
siguiente elemento de la lista, para saber la posición en memoria del siguiente
elemento.
6
struct _agenda {
char nombre[20];
char telefono[12];
struct _agenda *siguiente;
};
7
al inicio
en cualquier
Eliminación parte
al final
Visualización al final
en cualquier
Inicialización parte
1.5.1 Inicialización
void inicializacion (Lista *lista);
Esta operación debe ser hecha antes de otra operación sobre la lista.
Esta comienza el puntero inicio y el puntero fin con el puntero NULL, y
el tamaño con el valor 0.
1.5.2 Inserción
A continuación el algoritmo de inserción y el registro de los elementos, pasos:
1. declaración del elemento que se va a insertar;
2. asignación de la memoria para el nuevo elemento;
3. llena el contenido del campo de datos;
4. actualización de los punteros hacia el primer y último elemento si es
necesario.
8
Caso particular: en una lista con un único elemento, el primero es al mismo tiempo
el último. Actualizar el tamaño de la lista
Casos para insertar elementos a una lista enlazada:
a) en una lista vacía,
b) al inicio de la lista,
c) al final de la lista,
d) en otra parte de la lista
Las etapas son asignar memoria para el nuevo elemento, completa el campo de
datos de ese nuevo elemento, el puntero siguiente de este nuevo elemento
apuntará hacia NULL (ya que la inserción es realizada en una lista vacía, se utiliza
la dirección del puntero inicio que vale NULL), los punteros inicio y fin apuntaran
hacia el nuevo elemento y el tamaño es actualizado.
nuevo_elemento->siguiente = NULL;
lista->inicio = nuevo_elemento;
lista->fin = nuevo_elemento;
lista->tamaño++; 9
return 0;
}
b) Inserción al inicio de la lista
nuevo_elemento->siguiente = lista->inicio
lista->inicio = nuevo_elemento;
lista->tamaño++;
return 0; 10
}
c) Inserción al final de la lista,
actual->siguiente = nuevo_elemento;
nuevo_elemento->siguiente = NULL;
lista->fin = nuevo_elemento;
lista->tamaño++;
return 0;
}
11
d) Inserción en otra parte de la lista
int ins_lista (Lista *lista, char *dato,int pos);
12
/* inserción en otra parte de la lista */
int ins_lista (Lista * lista, char *dato, int pos){
if (lista->tamaño < 2)
return -1;
if (pos < 1 || pos >= lista->tamaño)
return -1;
Element *actual;
Element *nuevo_elemento;
int i;
actual = lista->inicio;
for (i = 1; i < pos; ++i)
actual = actual->siguiente;
if (actual->siguiente == NULL)
return -1;
strcpy (nuevo_elemento->dato, dato);
nuevo_elemento->siguiente = actual->siguiente;
actual->siguiente = nuevo_elemento;
lista->tamaño++;
return 0;
13
1.5.3 Eliminación
14
b) Eliminación en otra parte de la lista.
sup_elemento = actual->siguiente;
actual->siguiente = actual->siguiente->siguiente;
if(actual->siguiente == NULL)
lista->fin = actual;
free (sup_elemento->dato);
free (sup_elemento);
lista->tamaño--;
return 0;
}
15
1.5.4 Visualización
Para mostrar la lista entera hay que posicionarse al inicio de la lista (el
puntero inicio lo permitirá). Luego usando el puntero siguiente de cada elemento la
lista es recorrida del primero al último elemento.
/* Visualización de la lista */
void visualización (Lista * lista){
Element *actual;
actual = lista->inicio;
while (actual != NULL){
printf ("%p - %s\n", actual, actual->dato);
actual = actual->siguiente;
}
}
16
1.6 Declaración de Listas circulares.
Una lista circular es una lista lineal en la que el último nodo a punta al primero.
Las listas circulares evitan excepciones en las operaciones que se realicen sobre
ellas. No existen casos especiales, cada nodo siempre tiene uno anterior y uno
siguiente.
struct nodo {
int dato;
struct nodo *siguiente;
};
Los tipos que definiremos normalmente para manejar listas cerradas son los
mismos que para para manejar listas abiertas:
Lista es el tipo para declarar listas, tanto abiertas como circulares. En el caso de
las circulares, apuntará a un nodo cualquiera de la lista.
A pesar de que las listas circulares simplifiquen las operaciones sobre ellas,
también introducen algunas complicaciones. Por ejemplo, en un proceso de
búsqueda, no es tan sencillo dar por terminada la búsqueda cuando el elemento
buscado no existe.
17
Por ese motivo se suele resaltar un nodo en particular, que no tiene por qué ser
siempre el mismo. Cualquier nodo puede cumplir ese propósito, y puede variar
durante la ejecución del programa.
Otra alternativa que se usa a menudo, y que simplifica en cierto modo el uso de
listas circulares es crear un nodo especial de hará la función de nodo cabecera.
De este modo, la lista nunca estará vacía, y se eliminan casi todos los casos
especiales.
18
1.7 Listas doblemente enlazadas.
Una lista doblemente enlazada es una lista lineal en la que cada nodo tiene dos
enlaces, uno al nodo siguiente, y otro al anterior.
El nodo típico es el mismo que para construir las listas que hemos visto, salvo que
tienen otro puntero al nodo anterior:
struct nodo {
int dato;
struct nodo *siguiente;
struct nodo *anterior;
};
19
Lista es el tipo para declarar listas abiertas doblemente enlazadas. También es
posible, y potencialmente útil, crear listas doblemente enlazadas y circulares.
Algoritmo de inserción
20
if(!actual || actual->valor > v) {
/* Añadimos la lista a continuación del nuevo nodo */
nuevo->siguiente = actual;
nuevo->anterior = NULL;
if(actual) actual->anterior = nuevo;
if(!*lista) *lista = nuevo;
}
else {
/* Avanzamos hasta el último elemento o hasta que el
siguiente tenga
un valor mayor que v */
while(actual->siguiente && actual->siguiente->valor <=
v)
actual = actual->siguiente;
/* Insertamos el nuevo nodo después del nodo anterior
*/
nuevo->siguiente = actual->siguiente;
actual->siguiente = nuevo;
nuevo->anterior = actual;
if(nuevo->siguiente) nuevo->siguiente->anterior =
nuevo;
}
}
Arreglos
Listas enlazadas
1.8.1 Pilas
Una pila es una estructura dinámica que «apila»
elementos de forma que para llegar al primero,
hay que quitar todos los nodos que se hayan
añadido después. Utiliza LIFO (Last Input First
Output) que significa que el último que entra es el
primero que saldrá, acrónimo que refleja la
característica más importante de las pilas.
22
Ya que la inserción es siempre hecha al inicio de la lista, el primer elemento de la
lista será el último elemento ingresado, por lo tanto estará en la cabeza de la pila.
Una pila es un tipo especial de lista en la que sólo se pueden insertar y eliminar
nodos en uno de los extremos de la lista. Estas operaciones se conocen como
"push" y "pop", respectivamente "empujar" y "tirar". Además, las escrituras de
datos siempre son inserciones de nodos, y las lecturas siempre eliminan el nodo
leído.
1.8.1.1 Implementación.
Declaraciones de tipos para manejar pilas en C.
a) Inicialización
Esta operación debe ser realizada antes de cualquier otra operación sobre la pila.
Esta inicializa el puntero inicio con el valor NULL y el tamaño con el valor 0.
b) Visualización
24
La condición para detenerse es determinada por el tamaño de la pila.
/* visualización de la pila */
void muestra (Pila * tas){
Elemento *actual;
int i;
actual = tas->inicio;
for(i=0;i<tas->tamaño;++i){
printf("\t\t%s\n", actual->dato);
actual = actual->siguiente;
}
}
c) Insertar
25
d) Recuperación del dato en la cabeza de la pila
e) Eliminar
Las etapas:
26
1.8.1.3 Ejemplo en C
/* Crear un nodo nuevo */
#include <stdlib.h> nuevo =
#include <stdio.h> (pNodo)malloc(sizeof(tipoNodo)
);
typedef struct _nodo { nuevo->valor = v;
int valor;
struct _nodo *siguiente; /* Añadimos la pila a
} tipoNodo; continuación del nuevo nodo */
nuevo->siguiente = *pila;
typedef tipoNodo *pNodo; /* Ahora, el comienzo de
typedef tipoNodo *Pila; nuestra pila es en nuevo nodo
*/
/* Funciones con pilas: */ *pila = nuevo;
void Push(Pila *l, int v); }
int Pop(Pila *l);
int Pop(Pila *pila)
int main() {
{ pNodo nodo; /* variable
Pila pila = NULL; auxiliar para manipular nodo
pNodo p; */
int v; /* variable
Push(&pila, 20); auxiliar para retorno */
Push(&pila, 10);
Push(&pila, 40); /* Nodo apunta al primer
Push(&pila, 30); elemento de la pila */
nodo = *pila;
printf("%d, ", Pop(&pila)); if(!nodo) return 0; /* Si
printf("%d, ", Pop(&pila)); no hay nodos en la pila
printf("%d, ", Pop(&pila)); retornamos 0 */
printf("%d\n", Pop(&pila)); /* Asignamos a pila toda la
pila menos el primer elemento
system("PAUSE"); */
return 0; *pila = nodo->siguiente;
} /* Guardamos el valor de
retorno */
void Push(Pila *pila, int v) v = nodo->valor;
{ /* Borrar el nodo */
pNodo nuevo; free(nodo);
return v;
}
27
1.8.2 Colas
Una cola es un tipo especial de lista abierta en la que sólo se puede insertar nodos
en uno de los extremos de la lista y sólo se pueden eliminar nodos en el otro.
Además, como sucede con las pilas, las escrituras de datos siempre son
inserciones de nodos, y las lecturas siempre eliminan el nodo leído.
Este tipo de lista es conocido como lista FIFO (First In First Out), el primero en
entrar es el primero en salir.
El símil cotidiano es una cola para comprar, por ejemplo, las entradas del cine. Los
nuevos compradores sólo pueden colocarse al final de la cola, y sólo el primero de
la cola puede comprar la entrada.
1.8.2.1 Implementación.
Declaraciones de tipos para manejar colas en C.
El nodo típico para construir cola es el mismo que vimos en los apartados
anteriores para la construcción de listas y pilas:
struct nodo {
int dato;
struct nodo *siguiente;
};
Los tipos que definiremos normalmente para manejar colas serán casi los mismos
que para manejar listas y pilas, tan sólo cambiaremos algunos nombres:
28
1.8.2.2 Operaciones básicas con colas
a) Crear
cola CREAR () {
cola C;
C = (tcola *) malloc(sizeof(tcola));
if (C == NULL)
error("Memoria insuficiente.");
C->ant = C->post = (celda *)malloc(sizeof(celda));
if (C->ant == NULL) error("Memoria insuficiente.");
C->ant->siguiente = NULL;
return C;
}
29
b) Insertar (encolar)
La inserción en las colas se realiza por la cola de las mismas, es decir, se inserta
al final de la estructura.
Para llevar a cabo esta operación únicamente hay que reestructurar un par de
punteros, el último nodo debe pasar a apuntar al nuevo nodo (que pasará a ser el
último) y el nuevo nodo pasa a ser la nueva cola de la cola.
c) Eliminar (Desencolar)
Se quita de la cola el primer elemento. Para ello, una variable auxiliar apunta al
inicio de la cola. Luego el siguiente elemento de la cola se mueve al inicio. Y
finalmente se borra (delete) la variable auxiliar.
/* Desencolar elemento
return num;
}
d) Visualizar
Utilizando un “while” se recorre toda la cola, desde la cabeza hasta el último elemento
de la misma.
/* Mostrar Cola
void muestraCola( struct cola q )
{
struct nodo *aux;
aux = q.delante;
while( aux != NULL )
{
cout<<" "<< aux->nro ;
30
aux = aux->sgte;
}
}
#include <stdlib.h>
#include <stdio.h>
int main()
{
pNodo primero = NULL, ultimo = NULL;
31
Anadir(&primero, &ultimo, 20);
printf("Añadir(20)\n");
Anadir(&primero, &ultimo, 10);
printf("Añadir(10)\n");
printf("Leer: %d\n", Leer(&primero, &ultimo));
Anadir(&primero, &ultimo, 40);
printf("Añadir(40)\n");
Anadir(&primero, &ultimo, 30);
printf("Añadir(30)\n");
printf("Leer: %d\n", Leer(&primero, &ultimo));
printf("Leer: %d\n", Leer(&primero, &ultimo));
Anadir(&primero, &ultimo, 90);
printf("Añadir(90)\n");
printf("Leer: %d\n", Leer(&primero, &ultimo));
printf("Leer: %d\n", Leer(&primero, &ultimo));
system("PAUSE");
return 0;
}
32
v = nodo->valor;
/* Borrar el nodo */
free(nodo);
/* Si la cola quedó vacía, ultimo debe ser NULL también*/
if(!*primero) *ultimo = NULL;
return v;
}
33