Profesor: José Miguel Rubio L. Magíster © en Ingeniería Informática Ingeniero Civil en Informática Licenciado en Ciencias de la Ingeniería Técnico en Programación Oficina: 3-20 e-mail 1: [email protected] e-mail 2: [email protected] ICI 142 Fundamentos de Programación Programación en C (Segunda Parte) Índice • Implementación de Listas Archivos. Doblemenente Enlazadas. Variables dinámicas. • 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 Variables Dinámicas 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();} 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?