int *vector

Anuncio
INF 154
Laboratorio de
Programación
Programación en C
(Segunda Parte)
Índice
• Implementación de Listas
Archivos.
Doblemenente Enlazadas.
Memoria dinámica.
• Implementación de Listas
Abstracción de datos.
Doblemenente Enlazadas
Utilización de un TDA.
con Centinela.
Tipos de estructuras de
datos lineales.
• Ejercicios.
• Implementación de Pila con
arreglo.
• Implementación de Cola
con arreglo circular.
•
•
•
•
•
Programación en C
Archivos
Definición de fichero
• ¿Por qué se necesita utilizar almacenamiento
secundario?
– Proceso de gran cantidad de datos.
– Limitación de la memoria del ordenador.
– Programas que tratan cierta información de la que han de
disponer en posteriores ejecuciones.
Concepto de fichero
• Fichero o archivo
– Cadena de bytes consecutivos que termina en un carácter
especial llamado EOF (End Of File)
Byte 1
Byte 2
Byte 3
Byte 4
...
Byte n-1 Byte n
EOF
Ficheros de texto y binarios
• Forma en la que el programa que trata con el fichero
va a interpretar la información contenida en él.
• Una vez se ha creado un fichero y la información se
ha insertado en forma binaria o de texto, siempre se
deberá trabajar del mismo modo.
Tipos de acceso
• Secuencial
– Leer o escribir datos comenzando siempre desde el
principio del archivo.
– Añadir datos a partir del final del archivo.
– Poco eficiente.
Tipos de acceso
• Aleatorio
– Acceder directamente a cualquier posición dada de un
archivo para actualizar los valores contenidos a partir de
dicho punto.
– Implica la utilización de operaciones nuevas que permitan
situarse en cualquier punto del archivo.
Apertura y cierre de ficheros
• Apertura
FILE *fopen (char *pathname, char *type)
– pathname : cadena de caracteres que indica el nombre del
fichero, incluido el camino
– type : cadena que contiene el tipo de fichero y su modo de
apertura
• Cierre
int fclose (FILE *file)
Modos de apertura
Archivo de
texto
a
r
w
Archivo
binario
ab
rb
wb
a+
ab+
r+
rb+
w+
wb+
Significado
Abre el fichero para añadir datos al final. Si no existe, se crea.
Abre el fichero para lectura. El fichero debe existir.
Abre el fichero para escritura desde el principio. Si no existe,
se crea.
Abre el fichero para lectura y para añadir datos al final. Si no
existe, se crea.
Abre el fichero para lectura y escritura. Los datos se escriben
desde el principio. El fichero debe existir.
Abre el fichero para lectura y escritura. Los datos se escriben
desde el principio. Si no existe, se crea.
Ejemplo: apertura y cierre
#include <stdio.h>
void main() {
FILE *pf;
pf = fopen(“c:\\temp\\fichero.txt”, “w”);
if (pf==NULL) {
printf (“Error en la apertura del fichero\n”);
exit (-1);
}
...
fclose (pf);
}
Archivos de texto secuenciales: Lectura
•Función fscanf
int fscanf (FILE *pf, char *formato, &variables)
–lee hasta encontrar un blanco o final de línea
–devuelve el número de ítems leídos o EOF si llega a final
de fichero
•Función fgets
char* fgets (char *s, int n, FILE *pf)
–lee n caracteres de pf o hasta que encuentra un carácter de
final de línea y los almacena en s junto con ‘\0’
–devuelve la cadena leída o NULL si llega a final de fichero
Ejemplo de fscanf
• Lectura de varias cadenas de caracteres separadas
por blancos con fscanf
#include <stdio.h>
void main() {
FILE *pf;
char vector[50];
pf = fopen(“c:\\temp\\fichero.txt”, “r”);
while (fscanf (pf, “%s”, vector) != EOF)
printf (“Leido: %s\n”, vector);
fclose (pf);
}
Ejemplo de fscanf
• Lectura de datos de distintos tipos de un archivo de
texto
#include <stdio.h>
void main() {
FILE *pf;
float precio; int unidades; char pieza[50];
pf = fopen(“c:\\temp\\fichero.txt”, “r”);
fscanf (pf, “%s%d%f”, pieza, &unidades, &precio);
printf (“Pieza: %s, cantidad: %d y precio: %f\n”,
pieza, unidades, precio);
fclose (pf);
}
Ejemplo de fgets
• Lectura de varias cadenas de caracteres con fgets
#include <stdio.h>
void main() {
FILE *pf;
char vector[50];
pf = fopen(“c:\\temp\\fichero.txt”, “r”);
while (fgets (vector, 50, pf)!=NULL)
printf (“Leido: %s\n”, vector);
fclose (pf);
}
Archivos de texto secuenciales: Escritura
• Función fprintf
int fprintf (FILE *pf, char *formato, argumentos)
– escribe el contenido de los argumentos
– devuelve el número de ítems escritos en el fichero o un
número negativo en caso de error
Ejemplo de fprintf
• Escritura de datos de distintos tipos en un archivo de
texto.
#include <stdio.h>
void main() {
FILE *pf;
float precio; int unidades; char pieza[50];
printf (“Introduce pieza, cantidad y precio:\n”);
scanf (“%s%d%f”, pieza, &unidades, &precio);
pf = fopen(“c:\\temp\\fichero.txt”, “w”);
fprintf (pf, “%s %d %f ”, pieza, unidades, precio);
fclose (pf);
}
Archivos de texto aleatorios
• Se utilizan las mismas funciones para lectura y
escritura que para ficheros de texto de acceso
secuencial:
– fscanf
– fgets
– fprintf
Archivos de texto aleatorios
• Es necesario utilizar funciones que permitan posicionar el
puntero del fichero.
int fseek (FILE *pf, long int desplaz, int modo)
– desplaz es el desplazamiento en bytes a efectuar
– modo es el punto de referencia que se toma para efectuar dicho
desplazamiento:
• SEEK_SET: principio del fichero
• SEEK_CUR: posición actual
• SEEK_END: final del fichero
– devuelve 0 si se ha realizado el desplazamiento o distinto de cero si
ha ocurrido algún error
Ejemplo de fseek
• Supongamos que el fichero fichero.txt contiene la cadena “Este es el
texto del fichero”. Este programa lee diversas palabras del mismo.
#include <stdio.h>
void main() {
FILE *pf;
char cadena[50];
pf = fopen(“c:\\temp\\fichero.txt”, “r”);
fscanf (pf, “%s”, cadena);
printf (“Primera palabra: %s\n”, cadena);
fseek (pf, 4, SEEK_CUR);
fscanf (pf, “%s”, cadena);
printf (“Tercera palabra: %s\n”, cadena);
Ejemplo de fseek
fseek (pf, -7, SEEK_END);
fscanf (pf, “%s”, cadena);
printf (“Ultima palabra: %s\n”, cadena);
fseek (pf, 11, SEEK_SET);
fscanf (pf, “%s”, cadena);
printf (“Cuarta palabra: %s\n”, cadena);
fclose (pf);
}
• La salida del programa sería la siguiente:
Primera palabra: Este
Tercera palabra: el
Ultima palabra: fichero
Cuarta palabra: texto
Archivos binarios
• Se utilizan funciones que leen y escriben sin
interpretar los datos, es decir, son funciones que leen
y escriben bytes:
– fread, para lectura
– fwrite, para escritura
Ejemplo: archivos binarios
#include <stdio.h>
void main(){
FILE *pf;
float v[]={1.43, 4e-5, 32.01, 0.2e1};
pf=fopen (“c:\\temp\\fichero.dat”, “wb”);
fwrite (v, sizeof(float), 4, pf);
fread (v, sizeof(float), 4, pf);
printf (“Los numeros leidos son: %f %f %f %f\n”,
v[0],v[1],v[2],v[3]);
fclose (pf);
}
Programación en C
Memoria Dinámica
Conceptos básicos
• Operador de dirección de memoria (&)
– sobre objetos en memoria
&a : dirección de memoria de ‘a’
• Operador de indirección (*)
– sobre operandos que representen direcciones de
memoria
*(&a) : contenido de ‘a’
Punteros
• Puntero: variable que almacena una dirección de
memoria
p = &a
• Declaración de punteros
tipo_dato *nombre_variable_puntero;
int a, *p;
• NULL : valor nulo de un puntero
– librería stdlib.h
Ejemplos
float v, *pv=&v;
int u, *pu=&u;
int v;
int *pv;
v=8;
*pv=v;
int u=3, v;
int *pv, *pu;
pu=&u;
v=*pu;
pv=&v;
printf (“u=%d, v=%d, *pv=%d,
*pu=%d”, u,v,*pv,*pu);
int u1, u2;
int v=3;
int *pv;
pv=&v;
u1=2*(v+5);
u2=2*(*pv+5);
Ejemplos
int v=3, *pv;
pv=&v;
*pv=0;
printf (“\n v=%d *pv=%d”, v, *pv);
int *pv, *pu, u, v=1;
pv=&v;
*pv=*pv+10;
*pv=*pv+1;
u=*pv+2;
pu=pv;
Paso de punteros a funciones
• Implementa el paso por referencia
#include <stdio.h>
#include <stdlib.h>
void fun_invocada (int *vble_recibir){
*vble_recibir = *vble_recibir+1;
}
void fun_invocadora (){
int vble_enviar=4;
fun_invocada (&vble_enviar);
printf (“Valor de la variable:
%d\n”,vble_enviar);
}
main() {fun_invocadora();}
Memoria Dinámica
Además de la reserva de espacio estática (cuando se
declara una variable), es posible reservar memoria de
forma dinámica.
Funciones de gestión de memoria dinámica:
– void* malloc(size_t): Reserva memoria
dinámica.
– free(void*): Libera memoria dinámica.
– void* realloc(void*,size_t): Ajusta el
espacio de memoria dinámica.
Memoria Dinámica
int
int*
char*
a,b[2];
i;
c;
i=(int*)malloc(sizeof(int));
c=(char*)malloc(sizeof(char));
Estática
Dinámica
a
b[0]
b[1]
i
c
a
b[0]
b[1]
i
c
*i
*c
free(i);
a
c=(char*)realloc(c,sizeof(char)*9); b[0]
b[1]
i
c
*c
Asignación dinámica de memoria
• Reserva de memoria en tiempo de ejecución
• Función malloc de la librería stdlib.h
void *malloc (int num_bytes_memoria)
malloc (num_elementos*sizeof(tipo_dato));
• Función free de la librería stdlib.h
void free (void *)
Ejemplos
#include <stdlib.h>
main (){
int *dato_simple;
dato_simple = (int *) malloc (1*sizeof(int));
}
#include <stdlib.h>
main (){
int *dato_simple, int num;
scanf (“%d”, &num);
dato_simple=(int *) malloc (num*sizeof(int));
}
Punteros y vectores
• El nombre del vector es una dirección de memoria
• Significado físico de un vector
int datos[3];
datos[0] = dir. datos
datos[1] = dir. (datos+1)
datos[2] = dir. (datos+2)
Operaciones sobre vectores
• Operador &:
&datos[0]
&datos[1]
datos
(datos+0)
(datos+1)
• Operador *:
datos[0]
datos[1]
*datos
*(datos+1)
• Operador [ ] (indexación):
nombre_vector[i]*
(nombre_vector+i)
Diferencia entre vectores estáticos y
dinámicos
int datos[3];
int *datos;
…
datos = (int *) malloc (3*sizeof(int));
…
free(datos);
Ejemplo : Lectura de un vector dinámico
#include <stdio.h>
#include <stdlib.h>
int leervector(int *vector, int longitud){
int i;
for (i=0; i<longitud; i++){
printf (“\n Introduce un numero: “);
scanf (“%d”, vector+i);
}
}
main (){
int *vector, num, val;
printf (“¿Cuantos numeros seran introducidos? “);
scanf (“%d”, &num);
vector = (int *) malloc (num*sizeof(int));
leervector (vector, num);
free (vector);
}
Matrices dinámicas
• Método 1: vector de vectores
– permite usar las matrices con 2 índices
int **matriz, i, j, filas, cols;
...
matriz = (int **) malloc (filas * sizeof(int*));
for (i=0; i<filas; i++)
matriz[i] = (int *) malloc (cols * sizeof(int));
matriz[0][2] = 3;
Matrices dinámicas
• Método 2: matriz lineal
– más sencilla de declarar pero más complicada de
utilizar (un solo índice)
int *matriz, indice, filas, cols;
...
matriz = (int *) malloc (filas*cols*sizeof(int));
for (i=0; i<filas; i++)
for (j=0; j<cols; j++){
indice = i*cols + j;
matriz[indice] = 0;
}
Estructuras dinámicas
•
•
•
Para dar claridad a las estructuras de un programa es común que se defina un nuevo
tipo y así se le pueda asignar un nombre que represente de mejor forma el tipo de
dato. Por ejemplo:
typedef struct node_tag Node_Type;
Se crea así un nuevo tipo que si requiere se puede usar como:
Node_Type miNodo;
Los campos de una estructura se pueden acceder con el operador . /*un punto*/
Por ejemplo: miNodo.next, cuando next es un campo de la estructura Node_Type.
Cuando se tiene un puntero a una estructura, hay dos formas de acceder a los
miembros de ésta.
Vía operador ->, como en:
Sea Node_Type *p, miNode;
/* otras instrucciones, que asignan valor a p */
miNode.next = p-> next;
Vía operador *, como en
miNode.next = (*p).next; /* paréntesis son
requeridos por precedencia de . sobre * */
Estructuras dinámicas
• Cuando un puntero no tiene definido su valor, sus campos no deben
ser accedidos para no incurrir en errores de acceso fuera del espacio
de memoria del usuario (segmentation fault).
• Un puntero p=NULL; queda mejor definido para explícitamente indicar
que su valor no ha sido definido.
NULL está definido en <stdio.h>
• Para solicitar memoria de la zona de memoria dinámica se emplea el
siguiente llamado al sistema:
Sea Node_Type *p;
p = (Node_Type *) malloc (sizeof(Node_Type));
malloc asigna un espacio de memoria de tamaño en bytes dado por el
argumento y retorna un puntero a esa zona de memoria.
Estructuras dinámicas
• sizeof(Node_Type) es un operador interpretado por el
compilador y retorna el tamaño en byte requerido por cada variable del
tipo indicado.
(Node_Type *) es necesario para forzar el tipo del valor
retornado, de otra manera el compilar no permitiría el acceso a los
campos. El compilador diferencia punteros que apuntan a distintos tipos
de datos.
El contenido de la memoria asignada está inicialmente indefinido.
• Para retornar espacio de memoria dinámica no usado por el programa
se usa la función free, como en:
free(p);
Luego de este llamado, el puntero p queda apuntando a un valor
indefinido.
Programación en C
Abstracción de Datos
Abstracción de datos
• La abstracción nos permite simplificar el análisis y resolución
de un problema separando las características que son
relevantes de aquellas que no lo son.
• Una abstracción de datos, también denominada tipo de dato
abstracto, es un nuevo tipo de dato más un conjunto de
operaciones que permiten manipular objetos de dicho tipo.
• La utilización de los TDA da lugar a programas que son:
– más legibles
– más fáciles de mantener
– y más fáciles de modificar.
Utilización de un TDA
• Para poder utilizar un TDA, sin saber como está
representado internamente, es necesario disponer de
su especificación.
• Un TDA se divide en dos partes:
– Especificación.
– Implementación.
Tipos de
estructura de datos lineales
•
•
•
•
Listas
Pilas
Colas
Estas estructuras de datos pueden ser:
– Estáticas (implementado con arreglos) o
– Dinámicas (implementado con listas enlazadas).
Listas
• Son estructuras de datos secuenciales de 0 o más
elementos de un tipo dado almacenados en memoria.
• Son estructuras lineales, donde cada elemento de la
lista, excepto el primero, tiene un único predecesor y
cada elemento de la lista, excepto el último, tiene un
único sucesor.
• El número de elementos de la lista se llama longitud.
Si tiene 0 elementos se llama lista vacía.
Listas
• En una lista podemos añadir nuevos elementos o
suprimirlos en cualquier posición.
• Ejemplo implementado con arreglo:
Tipos de listas
• No enlazada (arreglo)
• Enlazada (objetos)
• Doblemente enlazada (anterior apunta al siguiente y
viceversa)
• Circular (el último apunta al primero)
• A la vez pueden estar:
– Ordenada
– No ordenada
Operaciones sobre Listas
• Algunas operaciones son:
– Inserción
– Supresión
– Recorrido
– Ordenación
– Búsqueda
– Consulta
Operaciones sobre Listas
Aplicación de las listas
• Las listas son comunes en la vida diaria: listas de
alumnos, listas de clientes, listas de espera, listas de
distribución de correo, etc.
• Las listas son estructuras de datos muy útiles para los
casos en los que se quiere almacenar información de la
que no se conoce su tamaño con antelación.
Aplicación de las listas
• También son valiosas para las situaciones en las que
el volumen de datos se puede incrementar o
decrementar dinámicamente durante la ejecución del
programa.
• Cuando aplicamos restricciones de acceso a las listas
tenemos pilas y colas que son listas especiales.
Listas Enlazadas
• Una lista enlazada está formada por una colección de
elementos (denominados nodos) tales que cada uno
de ellos almacena dos valores: un valor de la lista y
un puntero o referencia que indica la posición del
nodo que contiene el siguiente valor de la lista.
• Es necesario almacenar al menos la posición del
primer elemento.
• Es dinámica, su tamaño puede cambiar durante la
ejecución del programa.
Nodos de una lista enlazada
• Una lista enlazada es una sucesión de nodos en la
que a partir de un nodo se puede acceder al que
ocupa la siguiente posición en la lista.
• Esta característica nos indica que el acceso a las
listas es secuencial y no indexado, por lo que para
acceder al último elemento de la lista hay que
recorrer los n-1 elementos previos (n es el tamaño de
la lista).
Nodos de una lista enlazada
• Para que un nodo pueda acceder al siguiente y la
lista no se rompa en varias listas cada nodo tiene que
tener un puntero que guarde la dirección de memoria
que ocupa el siguiente elemento.
Esquema de una lista enlazada
• De esta forma un nodo se podría representar
esquemáticamente así:
Información
Nodo siguiente
• En el campo información se almacena el objeto a
guardar y nodo siguiente mantendrá la conexión con
el siguiente nodo.
Información
Información
Información
Nodo siguiente
Nodo siguiente
Nodo siguiente
Lista doblemente enlazada
• 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 operación
con cada elemento o para insertar, actualizar y
borrar.
• La otra ventaja es que las búsquedas son algo más
rápidas puesto que no hace falta hacer referencia al
elemento anterior.
Lista doblemente enlazada
• Su inconveniente es que ocupan más memoria por
nodo que una lista simple.
Listas circulares (enlazadas)
• Son listas en el que el último elemento tiene una
referencia (enlace) con el primer elemento
(cabecera).
• Pueden ser listas simples o doblemente circulares.
Pilas
• Es un tipo lineal de datos, secuencia de
elementos de un tipo, una estructura tipo LIFO
(Last In First Out) último en entrar primero en
salir.
• Son un subconjunto de las listas, en donde las
eliminaciones e inserciones se dan en un solo
extremo, de manera tal que, el último elemento
es el único accesible.
Pilas
• Operaciones básicas:
–
–
–
–
–
–
Crear la estructura
Insertar
Eliminar
Obtener un elemento
Vacíar
Mostrar los elementos
Aplicaciones de las pilas
• Compiladores (parsers: reconocedores sintácticos de los
compiladores).
• Programación de sistemas (para registrar llamadas a
subprogramas, y recuperar los datos anteriores, o
recuperar los parámetros).
• Recuperación de elementos en orden inverso al que
fueron colocados (en un depósito, una pila de
contenedores, sillas, etc. ).
• Convertir notación infija a posfija o prefija.
• Implementación de recursividad.
Encapsulación en pilas y colas
• Al igual que con las colas, la implementación de las
pilas suele encapsularse, es decir, basta con conocer
las operaciones de manipulación de la pila para poder
usarla, olvidando su implementación interna.
Colas
• Es un tipo de dato lineal con estructura FIFO (First
In, First Out), el primero que entra es el primero que
sale.
• Las colas son un subconjunto de las listas, en
donde las eliminaciones se dan al comienzo de la
lista y las inserciones al final.
• Los elementos se procesan en el orden como se
reciben (similar a la cola de impresión en redes).
Colas
• Operaciones básicas:
–
–
–
–
–
–
Crear la estructura
Insertar
Eliminar
Obtener un elemento
Vacíar
Mostrar los elementos
Aplicaciones de las colas
• Las colas, al igual que las pilas, resultan de
aplicación habitual en muchos problemas
informáticos.
• Su utilización es infinita, desde la simulación de una
cola formada frente a un cajero automático, hasta la
cola de impresión.
• Quizás la aplicación más común de las colas es la
organización de tareas de un ordenador. Los
procesos forman colas para la utilización de los
recursos de un sistema computacional.
Como manipular las estructuras de datos
lineales
• Idealmente una estructura de datos debe ser
manipulada únicamente por procedimientos propios
(encapsulación).
– Ejemplo: Una lista requiere de un top, pop y un push
– La estructura de datos y sus primitivas de manejo
constituyen la estructura abstracta de datos (TDA)
Pilas
• La operación Insert es llamada aquí PUSH.
• La operación Delete es llamada POP.
• Si se hace un POP de una pila vacío, decimos que hay un
underflow, lo cual es un error de programa.
• Si la implementación de la pila posee un límite para el
número de elementos y éste se excede, decimos que hay un
overflow. También es un error.
• Se incorpora la función TOP que retorna el valor más reciente
sin modificar la pila.
• Ejemplos de uso:
– Cuando hacemos undo en editores.
– Cuando hacemos back en un navegador.
Implementación de pila con arreglo
#include <assert.h> /* para eliminar validaciones, agregar #define NDEBUG*/
Const int MAX_ELEMENTS = 100;
typedef struct stack {
int top;
int element [MAX_ELEMENTS];
} STACK;
void MakeNull(STACK *S) {
S->top=-1;
}
int Stack_Empty(STACK * S) {
return (S->top == -1);
}
void Push( STACK *S, int x) {
assert (S->top < MAX_ELEMENTS);
(*S).top++; /* los paréntesis son requeridos por precedencia de operandos */
(*S).element[(*S).top]=x;
/* pudo ser S->element [S->top]=x; */
/* o ambas en (*S).element[++(*S).top]=x; */
}
int Pop (STACK *S) {
assert((*S).top > -1); /* stack no vacío */
return((*S).element[(*S).top--]);
}
int Top(STACK *S) {
assert(S->top > -1);
return (S->element[S->top]);
}
Colas
•
•
•
•
La operación Insert es llamada Enqueue.
La operación Delete es llamada Dequeue.
Cada queue tiene una head (cabeza) y un tail (cola).
También se pueden producir las condiciones de
overflow y underflow cuando la implementación tiene
capacidad limitada.
• Se incorpora la función Head que retorna el valor más
antiguo de la cola.
Implementación de cola con arreglo
circular
Const int MAX_ELEMENTS = 100;
typedef struct stack {
int head; /* apunta al elemento más antiguo de la queue, el
“primero” */
int tail;
/* apunta al elemento más recientemente ingresado a la
cola, el de atrás */
int count; /* cantidad de elemento en la cola. Permite distinguir
cola vacía de llena */
int element [MAX_ELEMENTS];
} QUEUE;
void MakeNull(QUEUE *Q) {
Q->head=0;
Q->tail=MAX_ELEMENTS-1;
Q->count=0;
}
int Queue_Empty(QUEUE * Q) {
return (Q->count == 0);
}
Implementación de cola con arreglo
circular
Const int MAX_ELEMENTS = 100;
typedef struct stack {
int head; /* apunta al elemento más antiguo de la queue, el “primero” */
int tail;
/* apunta al elemento más recientemente ingresado a la cola,
el de atrás */
int count; /* cantidad de elemento en la cola. Permite distinguir cola
vacía de llena */
int element [MAX_ELEMENTS];
} QUEUE;
......
void Enqueue( QUEUE *Q, int x) {
assert (Q->count++ < MAX_ELEMENTS);
Q->tail= ++Q->tail%MAX_ELEMENTS;
Q->element[Q->tail] = x;
}
int Dequeue (QUEUE *Q) {
int aux=Q->head;
assert(Q->count-- > 0); /* Queue no vacío */
Q->head= ++Q->head % MAX_ELEMENTS;
return((*Q).element[aux]);
}
int Head(QUEUE *Q) {
assert(Q->count >0);
return (Q->element[Q->head]);
}
Listas Enlazadas
• Una lista enlazada es una estructura de datos en la
que los objetos están ubicados linealmente.
• En lugar de índices de arreglo aquí se emplean
punteros para agrupar linealmente los elementos.
• La lista enlazada permite implementar todas las
operaciones de un conjunto dinámico.
• En una lista doblemente enlazada cada elemento
contiene dos punteros (next, prev). Next apunta al
elemento sucesor y prev apunta la predecesor.
Listas Enlazadas
• Si el predecesor de un elemento es nil, se trata de la cabeza de la
lista.
• Si el sucesor de un elemento es nil, se trata de la cola de la lista.
• Cuando la cabeza es nil, la lista está vacía.
• Una lista puede ser simplemente enlazada. En este caso se
suprime el campo prev.
• Si la lista está ordenada, el orden lineal de la lista corresponde al
orden lineal de las claves.
• En una lista circular, el prev de la cabeza apunta a la cola y el next
de la cola apunta a la cabeza.
Implementación de Listas doblemente
Enlazadas (Sin Centinela)
typedef struct nodo_lista {
struct nodo_lista * prev;
struct nodo_lista * next;
int elemento;
} NodoLista;
typedef NodoLista * P_NodoLista;
P_NodoLista List_Search(P_NodoLista
P_NodoLista x = L;
while ( x != NULL) {
if ( x->elemento == k)
return x;
x = x->next;
}
return (NULL);
}
L, int k) {
Implementación de Listas doblemente
Enlazadas (Sin Centinela)
typedef struct nodo_lista {
struct nodo_lista * prev;
struct nodo_lista * next;
int elemento;
} NodoLista;
typedef NodoLista * P_NodoLista;
/* Inserción sólo al comienzo de la lista */
void List_Insert (P_NodoLista *pL, P_NodoLista x) {
x->next = *pL;
if (*pL != NULL) /* No es una lista vacía*/
(*pL)->prev = x;
*pL = x;
x->prev = NULL;
}
• Si deseamos insertar elementos en cualquier posición,
podemos hacerlo más fácilmente usando una lista
doblemente enlazada con centinela.
Implementación de Listas doblemente
Enlazadas (Sin Centinela)
Obs: el detalle de los argumentos formales son parte del protocolo para usar la estructura de datos.
Lo mostrado aquí no es la única opción válida. Los argumentos para List_Insert pudieron ser, por
ejemplo,
List_Insert(P_NodoLista *posicion, int elemento)
¿Cuál es el tiempo de ejecución de List_Search?
¿Cuál es el tiempo de ejecución de List_Insert?
Implementación de Listas doblemente
Enlazadas (Sin Centinela)
typedef struct nodo_lista {
struct nodo_lista * prev;
struct nodo_lista * next;
int elemento;
} NodoLista;
typedef NodoLista * P_NodoLista;
/* repetidos aquí por conveniencia para explicar la función de abajo */
void List_Delete(P_NodoLista * pL, P_NodoLista x) {
/* esta función asume que x pertenece a la lista. */
if (x->prev != NULL ) /* No se trata del primer elemento */
x->prev -> next = x->next;
else
*pL = (*pL)->next; /* elimina el primer elemento */
if ( x->next != NULL)
x->next->prev = x->prev;
}
1.- ¿Cuál es el tiempo de ejecución de List_Delete?
2.- Ejercicios: Implementar las operaciones antes descritas para una lista simplemente enlazada.
3.- Otra opción para implementar listas doblemente enlazadas es hacer uso de un “Centinela”.
En este caso la lista es circular y se inicia con un elemento auxiliar o mudo, apuntando a sí
mismo. Así el código no requiere tratar en forma especial las condiciones de borde (eliminación
del último elemento y eliminación del primero). Hacer código como ejercicio.
Implementación de Listas doblemente
Enlazadas Con Centinela
head
Mudo
“Centinela”
Void List_Delete’( P_NodoLista x) {
x->prev->next = x->next;
x->next->prev = x->prev;
}
P_NodoLista List_Search’(P_NodoLista head, int k) {
P_NodoLista x = head->next;
while (x != head && x->elemento != k)
x = x->next;
return x==head?NULL:x;
}
Void List_Insert’ (P_NodoLista pn, P_NodoLista x) {
/* inserta delante de nodo apuntado por pn*/
x->next = pn->next;
pn->next->prev = x;
pn->next = x;
x->prev = pn;
}
Programación en C
Ejercicios
Ejercicios
• Dada una lista circular L doblemente enlazada.
Desarrolle una función que dado dos punteros x, y
a nodos de la lista L, permita intercambiarlos.
typedef struct _nodo {
struct _nodo * next, * prev;
Elemento elemento;
} NODO;
Ejercicios
• Solución 1:
void swap (NODO *x; NODO*y) {
NODO * tmp;
tmp =x->prev;
x->prev = y->prev;
y->prev = tmp;
tmp = x->next;
x->next=y->next;
y->next = tmp;
}
Ejercicios
•
•
Pero ¿qué tal si uno de los nodos es el primero o último?
Solución 2:
void swap_nodos( NODO * * pl, NODO * x, NODO * y) {
NODO * aux = x;
if (x==y) return;
if (*pl==x) *pl = y;
if (*pl==y) *pl = x;
if (x->next !=y && x->prev != y) {
x->prev->next = y;
x->next->prev=y;
y->prev->next = x;
y->next->prev=x;
swap (x,y);
} else if (x->next == y && x->prev == y)
return;
else if (x->next == y) {
y->next->prev = x;
x->prev->next = y;
swap(x,y);
}else {
x->next ->prev =y;
y->prev->next =x;
}
}
Ejercicios
• Codifique en C las operaciones Enqueue y Dequeue
de una cola implementada con la estructura de datos
lista doblemente enlazada con centinela.
• Considere la siguiente estructura para cada nodo:
typedef struct nodo_lista {
struct nodo_lista * prev;
struct nodo_lista * next;
int elemento;
} NODO_LISTA;
Ejercicios
• Solución:
void Enqueue(NODO_LISTA * head,
NODO_LISTA * x)
{
/* head apunta al centinela y x apunta al
nodo que se desea incorporar en la cola*/
x->next = head->next;
head->next->prev = x;
x->prev = head;
head->next=x;
}
Ejercicios
NODO_LISTA * Dequeue(NODO_LISTA * head)
{
/* head apunta al centinela, Dequeue retorna
el más antiguo de la cola*/
NODE_LISTA * x; /* nodo a eliminar y
retornar */
x = head->prev;
head->prev = x->prev;
x->prev->next=head;
return x;
}
• ¿Y qué tal si la cola estaba vacía?
Descargar