1 Capítulo 5. Conjuntos dinámicos. Listas, stacks, colas. Se estudian estructuras abstractas de datos para representar el concepto matemático de conjuntos, considerando que el número de los elementos del conjunto puede variar en el tiempo. 5.1. Nodos. Cada elemento o nodo se representa por una estructura, cuyos campos pueden ser leídos y escritos a través de un puntero a la estructura. Suele existir un campo que se denomina clave, que identifica unívocamente al nodo; otros campos suelen contener punteros a otros nodos de la estructura. La clave puede ser numérica o alfanumérica. 5.2. Operaciones. Las principales operaciones que suelen implementarse pueden clasificarse en consultas, y modificaciones. 5.2.1. Consultas: Buscar un nodo de la estructura que tenga igual valor de clave, que un valor que se pasa como argumento; retornando un puntero al nodo encontrado o NULL si no está presente. Seleccionar un nodo de la estructura que tenga el menor o mayor valor de la clave. Hay otras consultas que pueden hacerse, como buscar el sucesor o antecesor de un nodo. 5.2.2. Modificaciones. Insertar un nodo con determinados valores en la estructura. Debe establecerse la forma en que será insertado, de tal modo de preservar la organización de la estructura. Normalmente esto implica primero conseguir el espacio para el nuevo nodo, y la inicialización de sus campos; también es usual retornar un puntero al nodo recién creado. Profesor Leopoldo Silva Bijit 20-01-2010 2 Estructuras de Datos y Algoritmos Descartar o remover un nodo de la estructura. Asumiendo que se pasa como argumento un puntero al nodo que será descartado, o al nodo anterior. La operación debe mantener la organización de la estructura. Algunos algoritmos no requieren implementar todas las operaciones. Por ejemplo los que tienen sólo las operaciones de buscar, insertar y descartar suelen denominarse diccionarios. Los algoritmos en que sólo se busque e inserte se denominan arreglos asociativos, o tablas de símbolos. En un diccionario puro sólo se implementa buscar. La complejidad de estas operaciones suele cuantificarse de acuerdo al número de nodos de la estructura. Los principales conjuntos dinámicos que estudiaremos son: listas, stacks, colas, árboles binarios de búsqueda, tablas de hash y colas de prioridad. 5.3. Listas. Existe una gran variedad de estructuras denominas listas. 5.3.1. Lista simplemente enlazada. La lista más básica es la simplemente enlazada, la que puede definirse como la secuencia de cero (lista vacía) o más elementos de un determinado tipo. Los elementos quedan ordenados linealmente por su posición en la secuencia. Se requiere sólo un enlace entre un elemento y su sucesor. Los elementos de un arreglo ocupan posiciones contiguas o adyacentes en la memoria. En las listas debe asumirse que el espacio de un nodo no es contiguo con otro; por esta razón, no basta incrementar en uno el puntero a un nodo, para obtener la dirección de inicio del nodo siguiente. Cada nodo está conectado con el siguiente mediante un puntero que es un campo del nodo. Los elementos del arreglo se direccionan en tiempo constante, O(1). Los elementos de las listas tienen un costo de acceso O(n), en peor caso. Las operaciones sobre listas deben considerar que ésta puede estar vacía, lo cual requiere un tratamiento especial; así también los elementos ubicados al inicio y al final de la lista deben considerarse especialmente. Los siguientes diagramas ilustran una lista vacía y una lista con tres elementos. Si los nodos se crean en el heap, la variable lista, de la Figura 5.1, debe estar definida en el stack, o en la zona estática, con el tipo puntero a nodo. Note que el programador no dispone de nombres de variables para los nodos, éstos sólo pueden ser accesados vía puntero (esto debido a que en el momento de la compilación no se conocen las direcciones de los nodos; estas direcciones serán retornadas por malloc en tiempo de ejecución). Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 3 lista lista 1 nodo1 2 3 nodo2 nodo3 Figura 5.1. Lista vacía y con tres nodos. Se denominan listas con cabecera (header) o centinela aquellas que tienen un primer nodo al inicio de la lista. Con esta definición algunas de las operaciones sobre listas resultan más simples, que el caso anterior. lista lista c c 1 2 3 nodo1 nodo2 nodo3 Figura 5.2. Lista con encabezado vacía y con tres nodos. El caso de lista vacía y las acciones con el primer o último elemento de la lista han intentado ser resueltas agregando un nodo de encabezado o un centinela al fin de la lista. Estos elementos facilitan que las funciones diseñadas traten en forma homogénea a todos los elementos de la lista; por ejemplo, la inserción al inicio se trata de igual forma que la inserción en otra posición; el costo del mayor tamaño es despreciable comparado con los beneficios. Se definen los tipos: typedef struct moldenodo { int clave; struct moldenodo *proximo; } nodo, *pnodo; 5.3.1.1. Crea Nodo La siguiente función retorna un puntero al nodo inicializado: pnodo CreaNodo(int dato) { pnodo pn=NULL; if ( (pn= (pnodo) malloc(sizeof(nodo))) ==NULL) exit(1); else { pn->clave=dato; pn->proximo=NULL; } return(pn); } Profesor Leopoldo Silva Bijit 20-01-2010 4 Estructuras de Datos y Algoritmos pn dato Figura 5.3. Espacio antes de salir de CreaNodo. El diagrama de la Figura 5.3, ilustra la situación justo antes de salir de la función. Después de salir no existe la variable pn, ya que es automática. Ejemplos de definición de listas: pnodo lista=NULL; //Creación de lista vacía sin centinela lista Figura 5.4. Creación de lista vacía sin centinela. //Creación de lista vacía con encabezado. pnodo listaC = CreaNodo(0); listaC 0 Figura 5.5. Creación de lista vacía con encabezado. Se ha considerado valor de clave 0 en el encabezado, pero podría ser otro valor; por ejemplo, uno que no sea usado por los valores que se almacenarán en la lista. Debe liberarse, el espacio adquirido mediante malloc, cuando deje de usarse, y dentro del alcance de lista, y siempre que la lista no esté vacía. Esto se logra con: free(lista); Si lista está definida dentro de una función, debe liberarse el espacio, antes de salir de ésta, ya que luego será imposible liberar el espacio, debido a que las variables locales dejan de existir al salir de la función. El ejemplo anterior libera el espacio del nodo que está al inicio de la lista; el borrado de la lista completa requiere liberar el espacio de cada uno de los nodos. 5.3.1.2. Operaciones de consultas en listas. a) Recorrer la lista. Recorrer una lista es un tipo de operación frecuente. Veamos por ejemplo una función que cuente los nodos de la lista. Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 5 /* Dada la dirección de un nodo de la lista Retornar el número de nodos desde el apuntado hasta el final de la lista. */ int LargoLista(pnodo p) { int numeroelementos = 0; while (p != NULL) { numeroelementos ++; p = p ->proximo; //recorre la lista } return (numeroelementos); } lista p->proximo 1 2 p 3 numeroelementos Figura 5.6. Variables en LargoLista. Una alternativa de diseño es empleando un lazo for. int LargoLista(pnodo p) { int numeroelementos = 0; for( ; p != NULL; p=p->proximo) numeroelementos ++; return (numeroelementos); } Otras operaciones que demandan recorrer la lista son el despliegue de los elementos de la lista o buscar un nodo que tenga un determinado valor de clave. b) Buscar elemento. Se da una lista y un valor de la clave: se retorna un puntero al nodo de la lista que tiene igual valor de clave, que el valor pasado como argumento; retorna NULL, si no encuentra dicho valor en la lista. Profesor Leopoldo Silva Bijit 20-01-2010 6 Estructuras de Datos y Algoritmos pnodo Buscar(pnodo p, int valor) { while (p != NULL) { if (p->clave== valor) return (p); //lo encontró else p = p ->proximo; //recorre la lista. O(n) } return (p); //retorna NULL si no lo encontró. } El costo de la operación es O(n). Ejemplo de uso. pnodo q; if ( (q= Buscar(lista, 5)) == NULL) { /* no encontró nodo con clave igual a 5*/ } else { /* lo encontró. …..*/ } Si la lista es con centinela: if ( (q= Buscar(listaC->proximo, 5)) == NULL) { /* no encontró nodo con clave igual a 5*/ } else { /* lo encontró. …..*/ } c) Seleccionar un valor extremo. Se da una lista y se desea encontrar un puntero al nodo que cumple la propiedad de tener el mínimo valor de clave. Si la lista es vacía retorna NULL. Nótese que en seleccionar sólo se dan los datos de la lista; buscar requiere un argumento adicional. Debido a la organización de la estructura las operaciones de consulta tienen costo O(n). Veremos que existen estructuras y algoritmos más eficientes para buscar y seleccionar. pnodo SeleccionarMinimo(pnodo p) { int min; pnodo t; if (p==NULL) return (NULL); else {min=p->clave; //Inicia min t=p; p=p->proximo; } while (p != NULL) { if (p->clave <min ) {min=p->clave; t=p;} p = p ->proximo; //recorre la lista. O(n) } return (t); } Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 7 Si se inicializa la variable min con el mayor valor de su tipo, se simplifica el tratamiento en el borde. pnodo SelMin(pnodo p) { int min= INT_MAX; //requiere incluir limits.h pnodo t=NULL; while (p != NULL) { if (p->clave < min ) {min=p->clave; t=p;} p = p ->proximo; //recorre la lista. O(n) } return (t); } d) Buscar el último nodo. pnodo ApuntarAlFinal(pnodo p) { pnodo t; if (p==NULL) return (NULL); else while (p != NULL) { t=p; p = p ->proximo; //recorre la lista. O(n) } return (t); } 5.3.1.3. Operaciones de modificación de listas. a) Análisis de inserción. Si consideramos pasar como argumentos punteros a nodos, de tal forma de no efectuar copias de los nodos en el stack, en la inserción, se requiere escribir direcciones en los campos próximos de dos nodos, y en determinada secuencia. Esto se requiere para mantener la lista ligada. Supongamos que tenemos dos variables de tipo puntero a nodo: p apunta a un nodo de una lista y n apunta a un nodo correctamente inicializado (por ejemplo, el retorno de CreaNodo). La situación se ilustra en la Figura 5.7 a la izquierda, donde las variables n y p, se han diagramado por pequeños rectángulos. Los nodos se han representado por círculos, con una casilla para la clave, y otra para el puntero al nodo siguiente. El nodo n puede ser insertado después del nodo apuntado por p. La primera escritura en un campo de la estructura puede describirse por: n->proximo = p->proximo; Después de esta acción, la situación puede verse en el diagrama a la derecha de la Figura 5.7. Profesor Leopoldo Silva Bijit 20-01-2010 8 Estructuras de Datos y Algoritmos p->proximo p p->proximo p 2 1 2 1 n n 3 3 n->proximo n->proximo Figura 5.7. Inserción en listas. Primer enlace. La segunda escritura, que termina de encadenar la lista, y que necesariamente debe realizarse después de la primera, puede describirse por: p->proximo = n; La situación y el estado de las variables, después de la asignación, puede describirse según: p->proximo p 2 1 n 3 n->proximo Figura 5.8. Inserción en listas. Segundo enlace. Los valores que toman las variables de tipo puntero son direcciones de memoria, y no son de interés para el programador. Es de fundamental importancia apoyarse en un diagrama para escribir correctamente expresiones en que estén involucrados punteros. Debe considerarse que si en el diseño se elige que las variables n y p sean los argumentos de la función que inserta un nodo, después de ejecutada la función, automáticamente ellas dejan de existir. Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 9 Se puede emplear el siguiente código, si se desea insertar antes de la posición p; se requiere una variable entera, de igual tipo que la clave del nodo, para efectuar el intercambio. Si el nodo tiene más información periférica asociada, también debe ser intercambiada entre los nodos. int temp; n->proximo = p->proximo; p->proximo = n; temp=p->clave; p->clave=n->clave; n->clave=temp; //importa el orden de la secuencia. Después de ejecutado el segmento anterior, se ilustra el estado final de las variables y un esquema de la situación, en el diagrama siguiente. p->proximo p 2 3 temp 1 n 1 n->proximo Figura 5.9. Insertar antes. Si la lista es sin cabecera, la inserción al inicio, debe codificarse en forma especial, ya que no existe en este caso la variable p->proximo. El inicio de la lista sin cabecera es una variable de tipo puntero a nodo, no es de tipo nodo, y por lo tanto no tiene el campo próximo. b) Análisis de la operación descarte. En el descarte de un nodo, si consideramos pasar como argumento un puntero a la posición del nodo anterior al que se desea descartar, se requiere escribir una dirección y mantener una referencia al nodo que se desea liberar a través de free. Entonces la variable p apunta al nodo anterior al que se desea descartar, y t apunta al nodo que se desea desligar de la lista. Se ilustra en la Figura 5.10, la situación de las variables, después de ejecutada la acción: t=p->proximo; Profesor Leopoldo Silva Bijit 20-01-2010 10 Estructuras de Datos y Algoritmos p->proximo p 1 3 2 t t->proximo Figura 5.10. Fijación de t. Fijar la posición de t es necesario, ya que el siguiente paso es escribir en p->proximo, lo cual haría perder la referencia al nodo que se desea liberar. La variable t es necesaria, ya que tampoco se puede efectuar la liberación del nodo mediante: free(p->proximo) ya que esto haría perder la referencia al siguiente nodo de la lista (el nodo con clave 3 en el diagrama). La siguiente acción es la escritura en un campo, para mantener la lista ligada. Esto se logra con: p->proximo = t->proximo; p->proximo p 1 2 t 3 t->proximo Figura 5.11. Mantención de lista ligada. Ahora puede liberarse el espacio, del nodo que será descartado, mediante: free(t); Lo cual se ilustra en la Figura 5.12. También puede descartarse el nodo apuntado por el argumento, pero se requiere copiar los valores del nodo siguiente, enlazar con el subsiguiente y liberar el espacio del nodo siguiente. También debe notarse que descartar el primer nodo requiere un tratamiento especial, ya que se requiere escribir en el puntero a un nodo, que define el inicio, y en éste no existe el campo próximo. Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 11 p->proximo p 3 1 ? t t->proximo Figura 5.12. Espacio después de liberar el nodo. Es un error serio, normalmente fatal, escribir expresiones formadas por: *t, t->clave, o t->proximo, ya que éstas dejaron de existir, después de la ejecución de free(t). Si no se libera el espacio, queda un fragmento de la memoria dinámica inutilizable. No siempre es necesario liberar el espacio, por ejemplo se desea sacar un elemento de una lista e insertarlo en otra, no debe invocarse a free. Aparentemente las operaciones de modificación de listas son sencillas, pero como veremos a continuación aún hay detalles que analizar. c) Análisis adicionales en operación Insertar después. Considerando lo analizado anteriormente un primer diseño de la función es el siguiente: pnodo InsertarDespues( pnodo posición, pnodo nuevo) { nuevo->proximo=posicion->proximo; posicion->proximo=nuevo; return(nuevo); } Se decide retornar la dirección del nodo recién incorporado a la lista. Pero el diseño puede originar problemas, si el nuevo nodo se obtiene invocando a la función CreaNodo2 y éste no pudo ser creado por malloc, ya que en este caso tendrá valor NULL. pnodo CreaNodo2(int dato) { pnodo pn=NULL; if ( (pn= (pnodo) malloc(sizeof(nodo))) !=NULL) ; { pn->clave=dato; pn->proximo=NULL; } return(pn); } Profesor Leopoldo Silva Bijit 20-01-2010 12 Estructuras de Datos y Algoritmos En este caso, en la función InsertarDespues, no existe nuevo->proximo, lo cual produciría un error fatal en ejecución. Una forma de resolver lo anterior es agregando una línea para tratar la excepción. pnodo InsertarDespues( pnodo posición, pnodo nuevo) { if (nuevo == NULL) return (NULL); nuevo->proximo=posicion->proximo; posicion->proximo=nuevo; return(nuevo); } El diseño considera que si la función retorna NULL, implica que la inserción falló. La función funciona bien si la posición apunta al primer nodo, a uno intermedio o al último; ya que todos éstos tienen el campo próximo. Pero si el argumento posición toma valor NULL, se producirá un serio error, ya que posición->proximo apunta a cualquier parte, lo cual podría suceder si se intenta insertar en una lista vacía sin header. Esto lleva a agregar otra alternativa en el cuerpo de la función: pnodo InsertarDespues( pnodo posición, pnodo nuevo) { if (nuevo == NULL) return (NULL); if (posicion != NULL) { nuevo->proximo=posicion->proximo; posicion->proximo=nuevo; } return(nuevo); } Se analiza a continuación la inserción en una lista vacía. pnodo listaS=NULL; //lista sin header pnodo listaC= CreaNodo(0); //lista con header listaS = InsertarDespues(listaS, CreaNodo(1)); Es necesaria la asignación del retorno de la función a la variable listaS, para mantener vinculada la lista. En el caso de lista con header, el argumento listaC, no será NULL, en caso de lista vacía. El llamado: InsertarDespues(listaC, CreaNodo(1)); inserta correctamente el nuevo nodo al inicio de la lista. El valor de retorno apunta al recién agregado a la lista. 5.3.2. Listas doblemente enlazadas. Una definición de tipos: Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 13 typedef struct moldecelda { int clave; struct moldecelda *nx; //next struct modecelda *pr; // previo } nodo, *pnodo; nx pr clave Figura 5.13. Lista doblemente enlazada. Los diagramas describen el estado de las variables, antes y después de la operación de insertar el nodo apuntado por q, después del nodo apuntado por p: p p q q Figura 5.14. Inserción de nodo en lista doblemente enlazada. La secuencia de asignaciones describe la inserción. q->nx = p->nx; q->pr = p; p->nx = q ; q->nx->pr = q ; Descartar el nodo apuntado por q: q->pr->nx = q->nx; q->nx->pr = q->pr ; free(q) ; Las operaciones de insertar, buscar y descartar deben considerar las condiciones en los bordes, y que la lista pueda estar vacía. Profesor Leopoldo Silva Bijit 20-01-2010 14 Estructuras de Datos y Algoritmos Una forma usual de tratar simplificadamente las condiciones de borde, es definir un nodo vacío, denominado cabecera o centinela. La Figura 5.15 superior muestra una lista doblemente enlazada vacía, la inferior una con dos elementos: Las listas circulares doblemente enlazadas con cabecera son más sencillas de implementar y manipular. Las listas circulares simplemente enlazadas ocupan menos espacio pero su codificación debe incluir varios casos especiales, lo cual aumenta el código necesario para implementarlas y el tiempo para ejecutar las acciones. lista h lista h Figura 5.15. Lista doblemente enlazada circular con centinela. Tarea: Desarrollar las operaciones: Insertar, descartar y buscar en una lista doblemente enlazada circular. 5.3.3. Lista circular. En listas simplemente enlazadas, sin o con cabecera, puede escogerse que el último nodo apunte al primero, con esto se logra que el primer nodo pueda ser cualquier nodo de la lista. lista 1 2 3 4 Figura 5.16. Lista simplemente enlazada circular. La inserción al inicio, en el caso de la Figura 5.16, debe tratarse de manera especial, con costo O(n), para que el último nodo apunte al nuevo primero. Si la lista es con cabecera, y si el último apunta a la cabecera, no es necesario introducir código adicional. 5.3.4. Lista auto organizada. La operación buscar mueve a la primera posición el elemento encontrado. De esta manera los elementos más buscados van quedando más cerca del inicio de la lista. Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 15 5.3.5. Lista ordenada. Se mantiene, según el orden de la lista, los valores ordenados de las claves. La inserción requiere primero buscar la posición para intercalar el nuevo nodo. 5.3.6. Listas en base a cursores. En algunas aplicaciones se limita el número de nodos de la estructura por adelantado. En estos casos tiene ventajas tratar listas en base a arreglos. Pudiendo ser éstos: arreglos de nodos, en los cuales se emplean punteros; o bien arreglos que contienen la información de vínculos en base a cursores que almacenan índices. 5.4. Ejemplos de operaciones en listas sin centinela. Ejemplo 5.1 Inserción de un nodo. a) Insertar antes. Para el diseño de la función suponemos que disponemos del valor nuevo, un puntero que apunta a un nodo inicializado. nuevo dato Figura 5.17. Nuevo nodo que será insertado. También disponemos del valor posición, un puntero que apunta al nodo sucesor del que será insertado. Se ilustran dos posibles escenarios, cuando existe lista y el caso de lista vacía. lista posición 1 posición 2 3 lista Figura 5.18. Escenarios para inserción. En el diseño de la función consideramos que se retorne un puntero al nodo recién insertado. Para entender las operaciones sobre listas o estructuras que empleen punteros es recomendable emplear diagramas. Observamos que en caso de lista no vacía, debe escribirse en el campo nuevo->proximo el valor del argumento posición, y retornar el valor de nuevo. Si la lista, estaba originalmente vacía no es preciso escribir el puntero nulo en el campo nuevo->posición, si es que estaba correctamente inicializado. Profesor Leopoldo Silva Bijit 20-01-2010 16 Estructuras de Datos y Algoritmos lista nuevo 1 2 3 dato posición nuevo dato Figura 5.19. Variables en InsertaNodo. pnodo InsertaNodo(pnodo posicion, pnodo nuevo) { if (nuevo == NULL) return (NULL); if (posicion!=NULL) nuevo->proximo=posicion; return nuevo; } //O(1) Para una lista no vacía, un ejemplo de uso, se logra con: lista->proximo=InsertaNodo(lista->proximo, CreaNodo(8)); lista 1 8 2 3 Figura 5.20. Inserta nodo con valor 8 en Figura 5.18. Originalmente el primer argumento de InsertaNodo apuntaba al nodo dos. Dentro de la función se escribe en el campo próximo del nodo recién creado, de este modo se apunta al sucesor. Luego de la asignación, se escribe en el campo de enlace la dirección del nodo agregado. Un ejemplo de inserción al inicio: lista =InsertaNodo(lista, CreaNodo(7)); lista 7 1 2 3 Figura 5.21. Inserción al inicio de nodo con valor 7 en Figura 5.18. La operación diseñada inserta antes de la posición indicada por el argumento. Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 17 b) Insertar después. Una variante es insertar después de la posición. pnodo InsertaNodoDespues(pnodo posicion, pnodo nuevo) { if (nuevo == NULL) return (NULL); if (posicion!=NULL) { nuevo->proximo=posicion->proximo; //enlaza con el resto de la lista posicion->proximo=nuevo; //termina de enlazar el nuevo nodo return (posicion); } return nuevo; } posición lista 1 nuevo 3 2 4 Figura 5.22. Inserción del nodo con valor 4, después del nodo 2 en Figura 5.18. Es importante el orden de las asignaciones. c) Insertar al final. La siguiente función implementa la operación de insertar un nodo, con determinado valor, al final de la lista. pnodo InsertaNodoalFinal(pnodo posicion, int dato) { pnodo temp=posicion; if (temp != NULL) { while (temp->proximo !=NULL) temp=temp->proximo; //O(n) temp->proximo=CreaNodo(dato); return (temp->proximo); //retorna NULL si no se pudo crear el nodo } else return (CreaNodo(dato)); } Si frecuentemente se realizarán las operaciones de insertar al inicio o insertar al final, es preferible modificar la definición de la estructura de datos, agregando otra variable para apuntar al último de la lista, que suele denominarse centinela. Profesor Leopoldo Silva Bijit 20-01-2010 18 Estructuras de Datos y Algoritmos d) Insertar al inicio y al final. Asumiendo variables globales, se simplifica el paso de argumentos. Sin embargo las operaciones sólo son válidas para la lista asociada a dichas variables globales: static pnodo cabeza=NULL; static pnodo cola=NULL; cabeza 1 2 3 4 cola Figura 5.23. Inserciones al inicio y al final. pnodo insertainicio(int clave) { pnodo t=CreaNodo(clave); if(cabeza==NULL) cola=t; t->proximo=cabeza; cabeza=t; //O(1) return(t); } pnodo insertafinal(int clave) { pnodo t =CreaNodo(clave); if(cola==NULL) { cola=cabeza=t;} else { cola->proximo=t; cola=t;} //O(1) return(t); } Tarea: Diseñar descartar al inicio y descartar al final. Cuando sólo se desea insertar y descartar en un extremo la estructura se denomina stack. Cuando se inserta en un extremo y se descarta en el otro se denomina cola (en inglés queue). Cuando la estructura posibilita insertar y descartar en ambos extremos se la denomina doble cola (dequeue o buffer de anillo). e) Procedimiento de inserción. Es posible diseñar una función que no tenga retorno, en este caso uno de los argumentos debe ser pasado por referencia, ya que para mantener la lista ligada debe escribirse en dos campos. La operación puede aplicarse a varias listas, a diferencia del diseño con globales visto anteriormente. Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 19 void insertanodo_ref(pnodo *p, pnodo t) { if (*p==NULL) *p=t; //inserta en lista vacía. else { t->proximo=*p; //lee variable externa. *p=t; //escribe en variable externa. } } Ejemplos de uso. Insertanodo_ref(&lista1, CreaNodo(5)); //Paso por referencia. Aparece &. Insertanodo_ref(&lista2, CreaNodo(3)); // Se inserta en lista2. lista1 p 1 2 3 4 t 5 Figura 5.23a. Espacio luego de ingresar a la función Insertanodo_ref. En el diseño anterior, se pasa como argumento un puntero a un puntero a nodo. Lo cual permite pasar la dirección de la variable que define la lista. En caso de no emplear definición de tipos, en la definición de la función aparece más de un asterisco: void insertanodo_ref(struct moldenodo ** p, pnodo t) Complicando más aún la interpretación del código de la función. f) Error común en pasos por referencia. No es posible escribir fuera de la función sin emplear indirección. void Push(pnodo p, int valor) { pnodo NuevoNodo = malloc(sizeof(struct node)); NuevoNodo->clave = valor; NuevoNodo->proximo = p; p = NuevoNodo; // No escribe en variable externa. } Push(lista, 1); //no se modifica la variable lista p pertenece al frame. Desaparece después de ejecutada la función. Profesor Leopoldo Silva Bijit 20-01-2010 20 Estructuras de Datos y Algoritmos Ejemplo 5.2. Descartar o Borrar nodo. Debido a que descartar un nodo implica mantener la estructura de la lista, resulta sencilla la operación de borrar el siguiente a la posición pasada como argumento. Se tienen tres escenarios posibles: Que la lista esté vacía, que la posición dada apunte al último de la lista, y finalmente, que la posición apunte a un nodo que tiene sucesor. pnodo Descartar(pnodo p) { pnodo t = p; if (p==NULL) return (p); // Lista vacía if ( p->proximo==NULL) { free(p); return(NULL); // Último de la lista } else { t=p->proximo; free(p); return (t); //Retorna enlace si borró el nodo. } } Los diagramas ilustran las variables luego de ingresar a la función. p lista lista p p->proximo lista 1 5 2 3 t t p t Figura 5.24. Tres escenarios en descarte de nodo. Es responsabilidad de la función que llama a Descarte mantener ligada la lista, mediante el retorno. Tarea: Confeccionar ejemplos de invocación a Descartar, manteniendo ligada la lista. Borrar el nodo apuntado por p, requiere recorrer la lista, para encontrar el nodo anterior al que se desea borrar; contemplando el caso que el nodo ha ser borrado sea el primero de la lista. Esta operación es O(n). Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 21 Para lograr un algoritmo de costo constante, debe modificarse la estructura de datos de la lista, por ejemplo agregando un puntero al anterior. Similar situación se tiene si se desea implementar la operación predecesor. 5.5. Stack. Pila. Estructura LIFO (last-in, first-out), 5.5.1. Definición. La utilidad de esta estructura es muy amplia, y se la ha usado tradicionalmente incorporada al hardware de los procesadores: para organizar el retorno desde las subrutinas, para implementar el uso de variables automáticas, permitiendo el diseño de funciones recursivas, para salvar el estado de registros, en el paso de parámetros y argumentos. Generalmente los traductores de lenguajes, ensambladores y compiladores, emplean esta estructura para la evaluación y conversión de expresiones y para la determinación del balance de paréntesis; también existen arquitecturas virtuales denominadas máquinas de stack, para traducir a lenguajes de nivel intermedio las sentencias de lenguajes de alto nivel. Describiremos ahora lo que suele denominarse stack de usuario, como una estructura de datos que permite implementar el proceso de componentes con la política de atención: la última que entró, es la primera en ser atendida. El stack es una lista restringida, en cuanto a operaciones, ya que sólo permite inserciones y descartes en un extremo, el cual se denomina tope del stack. Debido a esta restricción suelen darse nombres especializados a las operaciones. Se denomina push (o empujar en la pila) a la inserción; y pop (o sacar de la pila) al descarte. No suele implementarse la operación buscar, ya que en esta estructura la complejidad de esta operación es O(n); en algunas aplicaciones se dispone de la operación leer el primer elemento del stack, sin extraerlo. En general la implementación de las operaciones generales de inserción y descarte usando arreglos son costosas, en comparación con nodos enlazados vía punteros, debido a que es necesario desplazar el resto de las componentes después de una inserción o descarte; además de que el tamaño del arreglo debe ser declarado en el código, no pudiendo crecer dinámicamente durante la ejecución. Sin embargo la primera dificultad no existe en un stack, la segunda se ve atenuada ya que no se requiere almacenar punteros lo cual disminuye el tamaño del espacio de almacenamiento; la única limitación es la declaración del tamaño del arreglo. Cuando es posible predecir por adelantado la profundidad máxima del stack, se suele implementar mediante arreglos. 5.5.2. Diagrama de un stack. Variables. La representación gráfica siguiente, muestra el arreglo y dos variables para administrar el espacio del stack. La variable stack es un puntero al inicio del arreglo. Profesor Leopoldo Silva Bijit 20-01-2010 22 Estructuras de Datos y Algoritmos stack Base del stack 0 1 Último ocupado 2 3 NumeroDeElementos 4 4 5 … Parte vacía del stack MAXN-1 Figura 5.25. Variables en un stack La variable NumeroDeElementos, contiene el número de elementos almacenados en el stack, el cual en la gráfica crece hacia abajo. Usualmente suele representarse al revés, para mostrar que es una estructura en que se van apilando las componentes; sólo se ve la primera componente, la del tope. El uso de la variable NumeroDeElementos, facilita el diseño de las funciones que prueban si el stack está lleno o vacío. 5.5.3. Archivo de encabezado ( *.h). Si se desea utilizar en alguna implementación la estructura de datos stack, es una práctica usual definir un archivo con extensión h (por header o encabezado), en el que se describen los prototipos de las funciones asociadas al stack. Esto permite conocer las operaciones implementadas y sus argumentos, acompañando a este archivo está el del mismo nombre, pero con extensión .c, que contiene las definiciones de las operaciones; en éste, se suele incluir al principio el archivo con extensión h, de tal modo que si existen funciones que invoquen a otras del mismo paquete, no importe el orden en que son definidas, ya que se conocen los prototipos. En el archivo siguiente, con extensión h, se ha empleado la compilación condicional, mediante la detección de la definición de un identificador. En el caso que se analiza, si no está definido el símbolo __STACK_H__ (note los underscores, para evitar alcances de nombres) se lo define y se compila. En caso contrario, si ya está definido no se compila; esto permite compilar una sola vez este archivo, a pesar de que se lo puede incluir en diferentes archivos que usen el stack. En el texto se incluye un archivo datos.h que permite, usando la misma técnica, definir focalizadamente los tipos de datos que emplee la aplicación que use la herramienta stack. En este caso en particular debe definirse el tipo de datos ElementoStack, que describe la estructura de una componente del arreglo. Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 23 /*stack.h> */ #ifndef __STACK_H__ #define __STACK_H__ #include "datos.h" #define push2(A, B) StackPush((B)); StackPush((A)); void StackInit(int); int StackEmpty(void); int StackFull(void); void StackPush(ElementoStack); ElementoStack StackPop(void); void StackDestroy(void); #endif /* __STACK_H__ */ El ejemplo también ilustra la definición de una macro: push2, que se implementa mediante el reemplazo del macro por dos invocaciones a funciones del paquete. Note que los argumentos se definen entre paréntesis. 5.5.4. Implementación de operaciones. El diseño de las funciones contempla tres variables globales asociadas al stack. Tope y NumeroDeElementos, que ya han sido definidas; además emplea la global MAXN, para almacenar el máximo número de elementos, ya que el tamaño del stack, se solicita dinámicamente, y no está restringido a ser una constante. Las variables globales simplifican el paso de argumentos de las operaciones; sin embargo restringen las operaciones a un solo stack. Si la aplicación empleara varios stacks diferentes, las funciones tendrían que ser redefinidas. /*stack.c Implementación basada en arreglos dinámicos. */ #include <stdlib.h> #include <stdio.h> #include "datos.h" #include "stack.h" static ElementoStack * stack; //puntero al inicio de la zona de la pila static int NumeroDeElementos; //elementos almacenados en el stack static int MAXN; //Máxima capacidad del stack void StackInit(int max) {stack = malloc(max*sizeof(ElementoStack) ); //se solicita el arreglo. if (stack == NULL) exit(1); NumeroDeElementos = 0; MAXN=max; } Profesor Leopoldo Silva Bijit 20-01-2010 24 Estructuras de Datos y Algoritmos int StackEmpty(void) { return(NumeroDeElementos == 0) ; //Retorna verdadero si stack vacío } int StackFull(void) { return(NumeroDeElementos == MAXN) ; //Retorna verdadero si stack lleno } //se puede empujar algo al stack si no está lleno. void StackPush(ElementoStack cursor) { if (!StackFull() ) stack[NumeroDeElementos ++]= cursor; } //se puede sacar algo del stack si no está vacío ElementoStack StackPop(void) { if( StackEmpty() ) {printf("error. Extracción de stack vacio\n"); exit(1); return; } else return ( stack[--NumeroDeElementos] ) ; } void StackDestroy(void) { free(stack); } Es buena práctica que las funciones StackInit y StackDestroy se invoquen en una misma función, para asegurar la liberación del espacio. Los programadores evitan la invocación de funciones innecesariamente, cuando las acciones de éstas sean simples; esto debido al costo de la creación del frame, de la copia de valores de argumentos y de la posterior destrucción del frame. En esta aplicación, podría haberse definido como macros los test de stack vacío o lleno, según: #define StackEmpty( ) (NumeroDeElementos == 0) #define StackFull( ) (NumeroDeElementos == MAXN) Ejemplo 5.3. Uso de stack. Balance de paréntesis. a) Especificación del algoritmo: Se dispone de un archivo de texto, que contiene expresiones que usan paréntesis. Se desea verificar que los paréntesis están balanceados. Es preciso identificar los pares que deben estar balanceados. Ejemplo: “(“, “)”, “[“, “]”, “{“, “}”, etc. Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 25 Se asume que se dispone de funciones para leer caracteres desde un archivo de texto, y para discriminar si el carácter es uno de los símbolos que deben ser balanceados o no. La secuencia siguiente no está balanceada: a+(b-c) * [(d+e])/f, al final están intercambiados dos tipos de paréntesis. b) Descripción inicial. Crear el stack. Mientras no se ha llegado al final del archivo de entrada: Descartar símbolos que no necesiten ser balanceados. Si es un paréntesis de apertura: empujar al stack. Si es un paréntesis de cierre, efectuar un pop y comparar. Si son de igual tipo continuar Si son de diferente tipo: avisar el error. Si se llega al fin de archivo, y el stack no esta vacío: avisar el error. Destruir el stack. El siguiente paso en el desarrollo es la descripción por seudo código, en la cual se establecen las variables y el nombre de las funciones. Ejemplo 5.4. Evaluación de expresiones en notación polaca inversa. Las expresiones aritméticas que generalmente escribimos están en notación “in situ” o fija. En esta notación los operadores se presentan entre dos operandos; por ejemplo: 2 + 3 * 4. Esta notación no explica el orden de precedencia de los operadores; debido a esto los lenguajes de programación tienen reglas de que establecen cuales operadores reciben primero sus operandos. En el lenguaje C, la multiplicación tiene mayor precedencia que el operador suma; entonces, en el caso del ejemplo, se realizará primero la multiplicación y luego la suma. La relación entre operadores y operandos puede hacerse explícita mediante el uso de paréntesis. La escritura de ( 2 + 3) *4 y 2 + (3 * 4) asocia operadores y operandos mediante paréntesis. En C, además existen reglas de asociatividad para especificar los operandos de un operador, en caso de que existan varios de igual precedencia, por ejemplo: 3*4*5. Si la asociatividad es de izquierda a derecha: se interpreta: ((3 * 4) * 5); si es de derecha a izquierda: (3* (4*5)) La notación inversa desarrollada por Jan Lukasiewicz (1878 - 1956) y empleada por los ingenieros de Hewlett-Packard para simplificar el diseño electrónico de las primeras calculadoras, permite escribir expresiones sin emplear paréntesis y definiendo prioridades para los operadores. En esta notación el operador sigue a los operandos. La expresión infija 3 + 4 tiene su equivalente en notación inversa como: 3 4 +. Y el ejemplo inicial: 2 + 3 * 4, se representa, en notación inversa, según: 2 3 4 * +. Una generalización es agregar el nombre de funciones a los operadores. Normalmente las funciones son operadores monádicos: sin[123 + 45 ln(27 - 6)] a) Ejemplo de evaluación. La expresión: (3 + 5) * (7 - 2) puede escribirse: 3 5 + 7 2 - * Profesor Leopoldo Silva Bijit 20-01-2010 26 Estructuras de Datos y Algoritmos Leyendo la expresión en notación inversa, de izquierda a derecha, se realizan las siguientes operaciones: Push 3 en el stack. Push 5 en el stack. Éste contiene ahora (3, 5). El 5 está en el tope, el último en entrar. Se aplica la operación + : la cual saca los dos números en el tope del stack, los suma y coloca el resultado en el tope del stack. Ahora el stack contiene el número 8. Push 7 en el stack. Push 2 en el stack. Éste contiene ahora (8, 7, 2). El 2 está en el tope. Se efectúa la operación – con los dos números ubicados en el tope. Éste contiene ahora (8, 5) Se efectúa la operación * con los dos números ubicados en el tope. Éste contiene ahora (40) La clave es entender que las operaciones se realizan sobre los dos primeros números almacenados en el stack, y que se empujan los operandos. b) Especificación. Se dispone de un archivo de texto que contiene expresiones aritméticas en notación inversa. Se dispone de funciones que permiten: leer un número como una secuencia de dígitos; reconocer los siguientes símbolos como operadores: +, -, * y /. descartar separadores, que pueden ser los símbolos: espacio, tab, nueva línea. reconocer el símbolo fin de archivo. c) Seudo código. While ( no se haya leído el símbolo fin de archivo EOF) { leer un símbolo; Si es número: empujar el valor del símbolo en el stack Si es un operador: { Efectuar dos pop en el stack; Operar los números, de acuerdo al operador; Empujar el resultado en el stack; } } Retornar el contenido del tope del stack, mediante pop. Ejemplo 5.5. Conversión de notación in situ a inversa. Se emplea para convertir las expresiones infijas y evaluarlas en un stack. Para especificar el algoritmo es preciso establecer las reglas de precedencia de operadores. La más alta prioridad está asociada a los paréntesis, los cuales se tratan como símbolos; prioridad media tienen la operaciones de multiplicación y división; la más baja la suma y resta. Se asume solamente la presencia de paréntesis redondos en expresiones. Como la notación polaca inversa no requiere de paréntesis, éstos no se sacarán hacia la salida. Notar que el orden en que aparecen los números son iguales en ambas representaciones, sólo difieren en el orden y el lugar en que aparecen los operadores. Se empleará el stack para almacenar los operadores y el símbolo de apertura de paréntesis. Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 27 Seudo código. While ( no se haya leído el símbolo fin de archivo EOF) { leer un símbolo; Si es número: enviar hacia la salida; Si es el símbolo „)‟: sacar del stack hacia la salida, hasta encontrar „(„, el cual no debe copiarse hacia la salida. Si es operador o el símbolo „(„: Si la prioridad del recién leído es menor o igual que la prioridad del operado ubicado en el tope del stack: { if( tope==‟(„ ) empujar el operador recién leído; else { efectuar pop del operador y sacarlo hacia la salida hasta que la prioridad del operador recién leído sea mayor que la prioridad del operador del tope. Empujar el recién leído en el tope del stack. } } } Si se llega a fin de archivo: vaciar el stack, hacia la salida. Se trata un stack con el símbolo „(„ en el tope como un stack vacío. 5.6. Cola. Buffer circular. Estructura FIFO (first-in, first-out). 5.6.1. Definición de estructura. Una cola es una lista con restricciones. En ésta las inserciones ocurren en un extremo y los descartes en el otro. La atención a los clientes en un banco, el pago de peaje en autopistas, son ejemplos cotidianos de filas o colas de atención. Si se conoce el máximo número de componentes que tendrán que esperar en la cola, se suele implementar en base a arreglos. Se requieren ahora dos variables para administrar los índices de la posición del elemento que será insertado o encolado (cola, tail en inglés); y también el índice de la posición de la componente que será descartada o desencolada en la parte frontal (cabeza. head). - 1 2 3 4 - out in cabeza cabeza cola Figura 5.26. Diagrama de una cola. Profesor Leopoldo Silva Bijit 20-01-2010 cola 28 Estructuras de Datos y Algoritmos El diagrama ilustra la situación luego: de la inserción de los elementos: 0, 1, 2, 3, y 4 y del descarte del electo 0. La cabeza (head) apunta al elemento a desencolar. La cola (tail) apunta a la posición para encolar. Apunta a un elemento disponible. Se observa que a medida que se consumen o desencolan componentes, van quedando espacios disponibles en las primeras posiciones del arreglo. También a medida que se encolan elementos va disminuyendo el espacio para agregar nuevos elementos, en la zona alta del arreglo. Una mejor utilización del espacio se logra con un buffer circular, en el cual la posición siguiente a la última del arreglo es la primera del arreglo. 5.6.2. Buffer circular. Esto es sencillo de implementar aplicando aritmética modular, si el anillo tiene N posiciones, la operación: cola = (cola+1) % N, mantiene el valor de la variable cola entre 0 y N-1. Operación similar puede efectuarse para la variable cabeza cuando deba ser incrementada en uno. La variable cola puede variar entre 0 y N-1. Si cola tiene valor N-1, al ser incrementada en uno (módulo N), tomará valor cero. cabeza N-1 0 1 2 3 4 5 cola Figura 5.27. Buffer circular. Los números, del diagrama, muestran los valores del índice de cada casilla del arreglo circular. La gráfica anterior ilustra la misma situación planteada con un arreglo lineal. 5.6.3. Cola vacía y llena. El diagrama a la izquierda ilustra una cola vacía; la de la derecha una cola con un espacio disponible. En esta última situación, el cursor cola (tail) dio la vuelta completa y está marcando como posición disponible para encolar la posición anterior a la que tocaría consumir. Si se encola un nuevo elemento, se producirá la condición de cola llena; pero esta situación es indistinguible de la de cola vacía. Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 29 cabeza cola N -1 N -1 0 0 1 1 2 2 3 3 cola 4 4 5 5 cabeza Figura 5.28. Cola vacía y casi llena. De esta forma no es posible distinguir entre las dos situaciones: cola llena o vacía. Una de las múltiples soluciones a este problema, es registrar en una variable adicional la cuenta de los elementos encolados; esto además facilita el diseño de las funciones que determinan cola vacía o llena. Si la variable la denominamos encolados. Entonces con cola vacía, encolados toma valor cero. La cola llena se detecta cuando encolados toma valor N. El algoritmo se basa en las funciones que operan sobre una cola circular basada en arreglos. Con operaciones de colocar en la cola (put), sacar de la cola (get) y verificar si la cola está vacía o llena. 5.6.4. Operaciones en colas. /* QUEUE.c en base a arreglo circular dinámico */ #include <stdlib.h> #include "QUEUE.h" static Item *q; // Puntero al arreglo de Items static int N, cabeza, cola, encolados; //Administran el anillo Debe estar definido el tipo de datos Item. void QUEUEinit(int maxN) //maxN es el valor N-1 de la Figura 5.27. { q = malloc((maxN+1)*sizeof(Item)); //Se pide espacio para N celdas. N = maxN+1; cabeza = 0; cola = 0; encolados=0; } La detección de cola vacía se logra con: int QUEUEempty() { return encolados == 0; } Profesor Leopoldo Silva Bijit 20-01-2010 30 Estructuras de Datos y Algoritmos Si la cola no está vacía se puede consumir un elemento: Item QUEUEget() { Item consumido= q[cabeza]; cabeza = (cabeza + 1) % N ; encolados--; return (consumido); } Se emplea aritmética módulo N. La detección de cola llena se logra con: int QUEUEfull() {return( encolados == N); } Si la cola no está llena se puede encolar un elemento: void QUEUEput(Item item) { q[cola] = item; cola = (cola +1) % N; encolados++;} Para recuperar el espacio: void QUEUEdestroy(void) { free ( q ); } En un caso práctico las funciones cola llena y vacía se implementan con macros. #define QUEUEempty() (encolados == 0) #define QUEUEfull() (encolados == N) Las dos aplicaciones, el stack de usuario y la cola, se emplearán en algoritmos para construir árboles en grafos. Ejemplo 5.6. Diseño de buffer circular estático de caracteres. Para insensibilizarse de las diferentes velocidades que pueden tener un consumidor y un productor de caracteres, se suele emplear un buffer. En el caso de un computador alimentando a una impresora, la velocidad de producción de caracteres del procesador es mucho mayor que la que tiene la impresora para liberar los caracteres hacia el medio de impresión; el disponer de un buffer de impresora, permite al procesador escribir en el buffer y no tener que esperar que la impresora escriba un carácter. Lo mismo ocurre cuando un usuario escribe caracteres desde un teclado; su velocidad de digitación es bastante menor que la velocidad con que el procesador utiliza los caracteres. Se emplea la variable cnt para llevar la cuenta de los elementos almacenados en el buffer. #define SIZE 16 #define LLENO (cnt==SIZE) #define VACIO (cnt==0) unsigned char Buffer[SIZE]; int rd=0, wr=0, cnt=0; Profesor Leopoldo Silva Bijit //buffer estático //administran el espacio 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 31 El cursor rd apunta al elemento a leer. El cursor wr al elemento que está disponible para ser escrito. La rutina put, coloca elementos en el buffer. void put(unsigned char c) { Buffer[wr]=c; wr=(wr+1)%SIZE; cnt++; } La rutina get consume elementos del buffer. unsigned char get(void) { unsigned char ch; ch=Buffer[rd]; rd=(rd+1)%SIZE; cnt--; return(ch); } SIZE-1 0 1 cnt 2 2 rd wr Figura 5.29. Buffer de caracteres. Las siguientes sentencias ilustran el uso de las funciones: if ( !VACIO ) ch=get(); else printf("vacío\n"); while( !LLENO ) put('1'); //lo llena if ( !LLENO ) put('2'); else printf("lleno\n"); while( !VACIO ) putchar(get()); //lo vacia if ( !VACIO ) putchar(get()); else printf("\nvacio\n"); Usualmente una de las rutinas opera por interrupciones. La rutina que no es de interrupción debe modificar la variable común cnt deshabilitando el tipo de interrupción. Profesor Leopoldo Silva Bijit 20-01-2010 32 Estructuras de Datos y Algoritmos Problemas resueltos. P5.1 Se tienen los diagramas de una lista circular vacía, y luego de haber insertado uno, dos y tres elementos. Notar que el puntero a la lista referencia el último nodo insertado en la estructura. 1 1 2 1 2 3 Figura P5.1. Buffer de caracteres. Definir tipos de datos: nodo es el tipo de datos del nodo, y pnodo es el nombre del tipo puntero a nodo. El valor almacenado en el nodo es de tipo entero. En cada caso ilustrar un ejemplo de uso, mostrando las variables que sean necesarias, con diagramas que ilustren la relación entre los datos. a) Diseñar función insertar con prototipo: pnodo insertar(int); El argumento es el valor que debe almacenarse en el nodo que se inserta. Retorna puntero al recién insertado, nulo en caso que no se haya podido crear el nodo. Asumir que se tiene variable global de nombre lista, de tipo pnodo. b) Diseñar función sumar con prototipo: int sumar(pnodo); El argumento es un puntero a un nodo cualquiera de la lista. Retorna la suma de los valores almacenados en todos los nodos de la lista; 0 en caso de lista vacía. c) Asumir que se tienen varias listas circulares, cada una de ellas referenciadas por un puntero almacenado en una variable de tipo pnodo. Se tiene la siguiente función, en la cual el argumento sirve para referenciar a una de las listas. pnodo funcion(pnodo *p) { pnodo t=*p; if(*p==NULL) return (NULL); *p = (*p)->proximo; return (t); } Determinar que realiza la función. Solución. typedef struct moldenodo { int clave; struct moldenodo *proximo; Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 33 } nodo, *pnodo; a) pnodo Insertar(int valor) { pnodo pn=NULL; if ( (pn = (pnodo) malloc(sizeof(nodo))) == NULL) return NULL; pn->clave = valor; if (listac == NULL){pn->proximo = pn;} else {pn->proximo = listac->proximo; listac->proximo = pn;} listac = pn; return (pn); } La siguiente definición, debe estar fuera de las funciones, y ubicada antes de la definición de la función Insertar: pnodo listac=NULL; La sentencia siguiente forma la lista cuyo diagrama se muestra más a la izquierda, en la definición del problema. for(i=1; i<4; i++) if ( Insertar( i ) == NULL) break; b) int Sumar(pnodo p) { pnodo t = p; int sum = 0; if(p == NULL) {printf(" Lista vacía. "); return(0);} sum += t->clave; for(t = p->proximo; t != p; t = t->proximo) sum += t->clave; return (sum); } printf("La suma de los elementos de la lista circular es %d\n", Sumar(listac)); c) La acción que realiza funcion(&Lista1), es apuntar al siguiente de la lista referenciada por la variable Lista1. Retorna puntero al que antes era el primero de la lista, nulo en caso de lista vacía. En el caso de la lista con tres elementos, dada al inicio, después de invocar a la función, en esa lista, debe retornar un puntero al nodo con valor 3, y la lista apunta al elemento con valor 1, según se ilustra en el siguiente diagrama. Profesor Leopoldo Silva Bijit 20-01-2010 34 Estructuras de Datos y Algoritmos Lista1 1 2 3 Figura P5.2. Si antes de invocar se tiene la situación dada al inicio, el siguiente segmento: pnodo t=NULL; if( (t=avanzar(&Lista1))!=NULL) printf("el anterior era %d\n", t->clave); Imprime el valor 3. Ejercicios propuestos. E5.1. Verificar que para la siguiente entrada: a+b*c+(d*e+f)*g La salida, en notación polaca inversa, se genera en el siguiente orden: abc abc*+ abc*+ abc*+d abc*+de abc*+de* abc*+de*f a b c * + d e* f + a b c * + d e* f + a b c * + d e* f + g a b c * + d e* f + g * + Efectuar una traza del contenido del stack, a medida que se van procesando los símbolos de entrada. E5.2. Se tienen los siguientes tipos de datos: typedef struct moldenodo { int clave; struct moldenodo *proximo; } nodo, *pnodo; Para la estructura de la Figura E5.1: a) Declarar las variables inicial y final. Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 35 b) Diseñar función que inserte nodo, con un valor pasado como argumento, al inicio. c) Diseñar función que inserte nodo, con valor pasado como argumento, al final. d) Diseñar función que intercambie el nodo inicial con el nodo final. Las funciones de inserción deben considerar la posibilidad de insertar en una cola vacía. inicial final Figura E5.1. Cola. E5.3. Búsqueda autoorganizada en listas. El proceso de reorganizar una lista por transposición, tiene por objetivo mejorar el tiempo promedio de acceso para futuras búsquedas, moviendo los nodos más accesados hacia el comienzo de la lista. Diseñar una rutina, en C, que busque un elemento en una lista en base a punteros. Y tal que cuando encuentre un elemento lo trasponga con el anterior, excepto cuando lo encuentre en la primera posición. E5.4. Insertar en lista ordenada. Comparar las dos funciones para insertar un nodo en una lista ordenada. pnodo inserteenorden (pnodo p, int k ) { pnodo p1, p2, p3; for( p2 = NULL, p1 = p; p1 != NULL && p1->clave < k; p2 = p1, p1 = p1->proximo ); if (p1 != NULL && p1->clave == k) return p; //no acepta claves repetidas p3= (pnodo) malloc (sizeof (nodo)) ; if(p3!=NULL) { p3->clave = k; if (p2 == NULL) { /* inserta al inicio */ p3->proximo = p1; return p3 ; } Profesor Leopoldo Silva Bijit 20-01-2010 36 Estructuras de Datos y Algoritmos p3->proximo = p2->proximo; p2->proximo = p3; } return p ; } pnodo inserteenordenHeader( pnodo p, int k ) { nodo header; pnodo p1,p2; header.proximo = p; for(p2 = &header; p != NULL && p->clave< k; p2 = p, p = p->proximo); if (p == NULL || p->clave !=k ){ p1 = (pnodo) malloc(sizeof(nodo)); if( p1!=NULL){ p1->clave = k; p1->proximo = p; p2->proximo = p1; } } return header.proximo ; } Notar que se trata el encabezado como una variable local. Referencias. En el apéndice: Assemblers, Linkers, and the SPIM Simulator de James R. Larus, del libro de Patterson A. David y Hennessy L. John, Computer Organization and Design: The Hardware/software Interface, Morgan Kaufmann 2004, aparece una excelente descripción del proceso de compilación, de la creación de archivos objetos, del proceso de ligado y carga de un programa. Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 37 Índice general. CAPÍTULO 5. ............................................................................................................................................ 1 CONJUNTOS DINÁMICOS. ................................................................................................................... 1 LISTAS, STACKS, COLAS. ..................................................................................................................... 1 5.1. NODOS. .............................................................................................................................................. 1 5.2. OPERACIONES. ................................................................................................................................... 1 5.2.1. Consultas:.................................................................................................................................. 1 5.2.2. Modificaciones. ......................................................................................................................... 1 5.3. LISTAS. .............................................................................................................................................. 2 5.3.1. Lista simplemente enlazada. ...................................................................................................... 2 5.3.1.1. Crea Nodo ........................................................................................................................................... 3 5.3.1.2. Operaciones de consultas en listas. ..................................................................................................... 4 a) Recorrer la lista. ...................................................................................................................................... 4 b) Buscar elemento. .................................................................................................................................... 5 c) Seleccionar un valor extremo. ................................................................................................................. 6 d) Buscar el último nodo. ............................................................................................................................ 7 5.3.1.3. Operaciones de modificación de listas. ............................................................................................... 7 a) Análisis de inserción. .............................................................................................................................. 7 b) Análisis de la operación descarte. ........................................................................................................... 9 c) Análisis adicionales en operación Insertar después............................................................................... 11 5.3.2. Listas doblemente enlazadas. .................................................................................................. 12 5.3.3. Lista circular. .......................................................................................................................... 14 5.3.4. Lista auto organizada. ............................................................................................................. 14 5.3.5. Lista ordenada. ........................................................................................................................ 15 5.3.6. Listas en base a cursores. ........................................................................................................ 15 5.4. EJEMPLOS DE OPERACIONES EN LISTAS SIN CENTINELA. .................................................................. 15 Ejemplo 5.1 Inserción de un nodo. .................................................................................................... 15 a) Insertar antes. ........................................................................................................................................ 15 b) Insertar después. ........................................................................................................................................ 17 c) Insertar al final. .......................................................................................................................................... 17 d) Insertar al inicio y al final. ......................................................................................................................... 18 e) Procedimiento de inserción. ....................................................................................................................... 18 f) Error común en pasos por referencia. ......................................................................................................... 19 Ejemplo 5.2. Descartar o Borrar nodo.............................................................................................. 20 5.5. STACK. PILA. ESTRUCTURA LIFO (LAST-IN, FIRST-OUT), ................................................................ 21 5.5.1. Definición. ............................................................................................................................... 21 5.5.2. Diagrama de un stack. Variables. ........................................................................................... 21 5.5.3. Archivo de encabezado ( *.h). ................................................................................................. 22 5.5.4. Implementación de operaciones. ............................................................................................. 23 Ejemplo 5.3. Uso de stack. Balance de paréntesis. ........................................................................... 24 a) Especificación del algoritmo: .................................................................................................................... 24 b) Descripción inicial. .................................................................................................................................... 25 Ejemplo 5.4. Evaluación de expresiones en notación polaca inversa. .............................................. 25 a) Ejemplo de evaluación. .............................................................................................................................. 25 Profesor Leopoldo Silva Bijit 20-01-2010 38 Estructuras de Datos y Algoritmos b) Especificación. ........................................................................................................................................... 26 c) Seudo código.............................................................................................................................................. 26 Ejemplo 5.5. Conversión de notación in situ a inversa. .....................................................................26 Seudo código.................................................................................................................................................. 27 5.6. COLA. BUFFER CIRCULAR. ESTRUCTURA FIFO (FIRST-IN, FIRST-OUT). ............................................27 5.6.1. Definición de estructura. ..........................................................................................................27 5.6.2. Buffer circular. .........................................................................................................................28 5.6.3. Cola vacía y llena. ...................................................................................................................28 5.6.4. Operaciones en colas. ..............................................................................................................29 Ejemplo 5.6. Diseño de buffer circular estático de caracteres. ....................................................................... 30 PROBLEMAS RESUELTOS. ........................................................................................................................32 EJERCICIOS PROPUESTOS. ........................................................................................................................34 E5.1. Verificar que para la siguiente entrada: ..................................................................................34 E5.2. Se tienen los siguientes tipos de datos: .....................................................................................34 E5.3. Búsqueda autoorganizada en listas. .........................................................................................35 E5.4. Insertar en lista ordenada. .......................................................................................................35 REFERENCIAS. .........................................................................................................................................36 ÍNDICE GENERAL. ....................................................................................................................................37 ÍNDICE DE FIGURAS. ................................................................................................................................39 Profesor Leopoldo Silva Bijit 20-01-2010 Conjuntos dinámicos. Listas, stacks, colas. 39 Índice de figuras. FIGURA 5.1. LISTA VACÍA Y CON TRES NODOS. ............................................................................................. 3 FIGURA 5.2. LISTA CON ENCABEZADO VACÍA Y CON TRES NODOS................................................................. 3 FIGURA 5.3. ESPACIO ANTES DE SALIR DE CREANODO. ................................................................................ 4 FIGURA 5.4. CREACIÓN DE LISTA VACÍA SIN CENTINELA............................................................................... 4 FIGURA 5.5. CREACIÓN DE LISTA VACÍA CON ENCABEZADO. ........................................................................ 4 FIGURA 5.6. VARIABLES EN LARGOLISTA. ................................................................................................... 5 FIGURA 5.7. INSERCIÓN EN LISTAS. PRIMER ENLACE. ................................................................................... 8 FIGURA 5.8. INSERCIÓN EN LISTAS. SEGUNDO ENLACE. ................................................................................ 8 FIGURA 5.9. INSERTAR ANTES. ...................................................................................................................... 9 FIGURA 5.10. FIJACIÓN DE T. ...................................................................................................................... 10 FIGURA 5.11. MANTENCIÓN DE LISTA LIGADA. ........................................................................................... 10 FIGURA 5.12. ESPACIO DESPUÉS DE LIBERAR EL NODO. .............................................................................. 11 FIGURA 5.13. LISTA DOBLEMENTE ENLAZADA. ........................................................................................... 13 FIGURA 5.14. INSERCIÓN DE NODO EN LISTA DOBLEMENTE ENLAZADA. ..................................................... 13 FIGURA 5.15. LISTA DOBLEMENTE ENLAZADA CIRCULAR CON CENTINELA. ................................................ 14 FIGURA 5.16. LISTA SIMPLEMENTE ENLAZADA CIRCULAR. ......................................................................... 14 FIGURA 5.17. NUEVO NODO QUE SERÁ INSERTADO. .................................................................................... 15 FIGURA 5.18. ESCENARIOS PARA INSERCIÓN............................................................................................... 15 FIGURA 5.19. VARIABLES EN INSERTANODO. ............................................................................................. 16 FIGURA 5.20. INSERTA NODO CON VALOR 8 EN FIGURA 5.18. ..................................................................... 16 FIGURA 5.21. INSERCIÓN AL INICIO DE NODO CON VALOR 7 EN FIGURA 5.18. ............................................. 16 FIGURA 5.22. INSERCIÓN DEL NODO CON VALOR 4, DESPUÉS DEL NODO 2 EN FIGURA 5.18. ....................... 17 FIGURA 5.23. INSERCIONES AL INICIO Y AL FINAL. ...................................................................................... 18 FIGURA 5.23A. ESPACIO LUEGO DE INGRESAR A LA FUNCIÓN INSERTANODO_REF. ..................................... 19 FIGURA 5.24. TRES ESCENARIOS EN DESCARTE DE NODO. ........................................................................... 20 FIGURA 5.25. VARIABLES EN UN STACK...................................................................................................... 22 FIGURA 5.26. DIAGRAMA DE UNA COLA. .................................................................................................... 27 FIGURA 5.27. BUFFER CIRCULAR. ............................................................................................................... 28 FIGURA 5.28. COLA VACÍA Y CASI LLENA. .................................................................................................. 29 FIGURA 5.29. BUFFER DE CARACTERES....................................................................................................... 31 FIGURA P5.1. BUFFER DE CARACTERES. ..................................................................................................... 32 FIGURA P5.2. .............................................................................................................................................. 34 FIGURA E5.1. COLA. ................................................................................................................................... 35 Profesor Leopoldo Silva Bijit 20-01-2010