Cap. 5 Conjuntos Dinámicos. Listas, stacks, colas.lsb/elo320/clases/c5.pdf · Con esta definición...

39
1 Profesor Leopoldo Silva Bijit 20-01-2010 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.

Transcript of Cap. 5 Conjuntos Dinámicos. Listas, stacks, colas.lsb/elo320/clases/c5.pdf · Con esta definición...

1

Profesor Leopoldo Silva Bijit 20-01-2010

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.

2 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

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).

Conjuntos dinámicos. Listas, stacks, colas. 3

Profesor Leopoldo Silva Bijit 20-01-2010

nodo1

1 2 3

lista

nodo2 nodo3

lista

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.

nodo1

1 2 3

lista

nodo2 nodo3

c

c

lista

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);

}

4 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

dato

pn

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);

0

listaC

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.

Conjuntos dinámicos. Listas, stacks, colas. 5

Profesor Leopoldo Silva Bijit 20-01-2010

/*

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);

}

1 2 3

lista

p

p->proximo

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.

6 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

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);

}

Conjuntos dinámicos. Listas, stacks, colas. 7

Profesor Leopoldo Silva Bijit 20-01-2010

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.

8 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

p

3

2 1

n

p->proximo

n->proximo

p

3

2 1

n

p->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

3

2 1

n

p->proximo

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.

Conjuntos dinámicos. Listas, stacks, colas. 9

Profesor Leopoldo Silva Bijit 20-01-2010

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

1

2 3

n

p->proximo

n->proximo

temp

1

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;

10 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

p

2 3 1

t

p->proximo

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

2 3 1

t

p->proximo

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.

Conjuntos dinámicos. Listas, stacks, colas. 11

Profesor Leopoldo Silva Bijit 20-01-2010

p

3 1

t

p->proximo

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);

}

12 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

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:

Conjuntos dinámicos. Listas, stacks, colas. 13

Profesor Leopoldo Silva Bijit 20-01-2010

typedef struct moldecelda

{

int clave;

struct moldecelda *nx; //next

struct modecelda *pr; // previo

} nodo, *pnodo;

pr nx

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:

q

p

q

p

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.

14 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

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.

1 3 4

lista

2

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.

Conjuntos dinámicos. Listas, stacks, colas. 15

Profesor Leopoldo Silva Bijit 20-01-2010

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.

dato nuevo

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.

1 2 3

lista posición

lista

posición

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.

16 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

dato nuevo

1 2 3

lista

dato

nuevo

posición

Figura 5.19. Variables en InsertaNodo.

pnodo InsertaNodo(pnodo posicion, pnodo nuevo)

{

if (nuevo == NULL) return (NULL);

if (posicion!=NULL) nuevo->proximo=posicion; //O(1)

return nuevo;

}

Para una lista no vacía, un ejemplo de uso, se logra con:

lista->proximo=InsertaNodo(lista->proximo, CreaNodo(8));

1 2 3

lista

8

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));

7 2 3

lista

1

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.

Conjuntos dinámicos. Listas, stacks, colas. 17

Profesor Leopoldo Silva Bijit 20-01-2010

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;

}

1 2 3

lista

4 nuevo

posición

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.

18 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

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;

1 3 4

cabeza

2

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.

Conjuntos dinámicos. Listas, stacks, colas. 19

Profesor Leopoldo Silva Bijit 20-01-2010

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.

1 3 4 lista1

2 p

5

t

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.

20 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

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.

lista

p

t

5

lista

p

t

1 2 3

lista

p

p->proximo

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).

Conjuntos dinámicos. Listas, stacks, colas. 21

Profesor Leopoldo Silva Bijit 20-01-2010

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.

22 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

NumeroDeElementos

4

5

1

2

3

4

0

MAXN-1

Último ocupado

Parte vacía del stack

Base del stack stack

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.

Conjuntos dinámicos. Listas, stacks, colas. 23

Profesor Leopoldo Silva Bijit 20-01-2010

/*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;

}

24 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

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.

Conjuntos dinámicos. Listas, stacks, colas. 25

Profesor Leopoldo Silva Bijit 20-01-2010

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 - *

26 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

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.

Conjuntos dinámicos. Listas, stacks, colas. 27

Profesor Leopoldo Silva Bijit 20-01-2010

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 - -

cabeza cola

out in

cabeza cola

Figura 5.26. Diagrama de una cola.

28 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

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.

0 1

N-1

2

cola

3

4

5

cabeza

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.

Conjuntos dinámicos. Listas, stacks, colas. 29

Profesor Leopoldo Silva Bijit 20-01-2010

0 1

N - 1

2

3

4

5

cabeza

cola

0 1

N - 1

2

cola 3

4

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; }

30 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

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]; //buffer estático

int rd=0, wr=0, cnt=0; //administran el espacio

Conjuntos dinámicos. Listas, stacks, colas. 31

Profesor Leopoldo Silva Bijit 20-01-2010

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);

}

0 1

SIZE-1

2

rd wr

cnt

2

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.

32 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

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;

Conjuntos dinámicos. Listas, stacks, colas. 33

Profesor Leopoldo Silva Bijit 20-01-2010

} 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.

34 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

1 2 3

Lista1

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:

a b c

a b c * +

a b c * +

a b c * + d

a b c * + d e

a b c * + d e *

a b c * + d e * 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.

Conjuntos dinámicos. Listas, stacks, colas. 35

Profesor Leopoldo Silva Bijit 20-01-2010

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.

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 ;

}

final

inicial

36 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

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.

Conjuntos dinámicos. Listas, stacks, colas. 37

Profesor Leopoldo Silva Bijit 20-01-2010

Í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

38 Estructuras de Datos y Algoritmos

Profesor Leopoldo Silva Bijit 20-01-2010

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

Conjuntos dinámicos. Listas, stacks, colas. 39

Profesor Leopoldo Silva Bijit 20-01-2010

Í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