Listas, stacks, colas. - Departamento de Electrónica

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