• No se han encontrado resultados

INSTITUTO SUPERIOR TECNOLÓGICO NORBERT WIENER

N/A
N/A
Protected

Academic year: 2022

Share "INSTITUTO SUPERIOR TECNOLÓGICO NORBERT WIENER"

Copied!
148
0
0

Texto completo

(1)

INSTITUTO SUPERIOR TECNOLÓGICO NORBERT WIENER

Manual del Alumno

ASIGNATURA: Estructura de la Información

PROGRAMA: S3C

Lima-Perú

(2)

Manual del Alumno

LISTAS

1. INTRODUCCIÓN.

Dado un dominio D, una lista de elementos de dicho conjunto es una sucesión finita de

elementos del mismo.En lenguaje matemático, una lista es una aplicación de un conjunto de la forma {1,2, ... ,n} en un dominio D:

R:{1,2, ... ,n} ---> D Una lista se suele representar de la forma:

<a1,a2, ... ,an> con ai = a(i)

A n se le llama longitud de la lista.

A 1,2,...,n se les llama posiciones de la lista. El elemento a(i)=ai, se dice que ocupa la posicion i. Si la lista tiene n elementos, no existe ningún elemento que ocupe la posición n+1. Sin embargo, conviene tener en cuenta dicha posición, a la que se llama posición detras de la última, ya que esta posición indicará el final de la lista. A a1 se le llama primer elemento de la lista y a an último elemento de la lista. Si n=0 diremos que la lista está vacía y lo

representaremos como <>. Los elementos de una lista estan ordenados por su posición. Así, se dice que ai precede a ai+1 y que ai sigue a ai-1. A continuación vamos a especificar un ejemplo de posibles operaciones primitivas entre listas. Al conjunto de las listas (es decir, al tipo lista) lo llamaremos tLista. Al conjunto de los elementos básicos(es decir, al tipo que se almacenará en la lista) tElemento. También vamos a considerar el tipo posición como

tPosicion. Esto lo haremos así, ya que no siempre las posiciones las vamos a representar por números naturales del lenguaje que se utilice. Lo único importante de la representación que se utilice es que sea un conjunto finito y totalmente ordenado: hay un primer y un último elemento y dado un elemento se puede determinar el siguiente (si no es último) y el anterior (si no es el primero).

Hay que tener en cuenta que si una lista tiene longitud n y se elimina el elemento que ocupa una determinada posición intermedia i, entonces la longitud pasa a ser n-1 y el elemento que estaba en la posición i+1 pasará a ocupar la posición i, el de la posición i+2 pasará a ocupar la posición i+1 y así sucesivamente.

2. OPERACIONES PRIMITIVAS DE LAS LISTAS.

Dentro del tipo abstracto de listas podemos proponer las siguientes primitivas:

void anula(tLista *l) tPosicion primero(tLista l) tPosicion fin(tLista l)

tPosicion siguiente(tPosicion p, tLista l) tPosicion anterior(tPosicion p, tLista l) tPosicion posicion(tElemento x, tLista l) tElemento elemento(tPosicion p, tLista l) void insertar(tElemento x, tPosicion p, tLista t) void borrar(tPosicion p, tLista l)

(3)

Manual del Alumno

ESPECIFICACIÓN SEMANTICA Y SINTACTICA.

void anula(tLista *l) PRE: l = <a1,a2, ... ,an>

POST: (*l) = <>

{vacía la lista}

tPosicion primero(tLista l) PRE: l está inicializada.

POST: RESULTADO = (1)

{devuelve la primera posición de la lista. Si la lista es <> coincide con fin(l)}

tPosicion fin(tLista l) PRE: l está inicializada.

POST: RESULTADO = (n + 1) {posición detrás de la última}

tPosicion siguiente(tPosicion p, tLista l) PRE: l = <a1,a2, ... ,an>, 1 <= p <= n POST: RESULTADO = p + 1

{devuelve la posición siguiente a la posición p}

tPosicion anterior(tPosicion p, tLista l) PRE: l = <a1,a2, ... ,an>, 2 <= p <= n+1 POST: RESULTADO = p - 1

{devuelve la posición anterior a la posición p}

tPosicion posicion(tElemento x, tLista l) PRE: l está inicializada.

POST: Si existe j perteneciente a {1,2, ... ,n} tal que aj = x entonces RESULTADO = i, donde i verifica: ai=x y si aj=x entonces j>=i.

{da la posición de la primera aparición de x en la lista l}

tElemento elemento(tPosicion p, tLista l) PRE: l = <a1,a2, ... ,an>, 1 <= p <= n POST: RESULTADO = ap

{devuelve el elemento situado en la posición p}

void insertar(tElemento t, tPosicion p, tLista l) PRE: l = <a1,a2, ... ,an>, 1 <= p <= n+1

POST: l = <a1, ... ,ap-1, x, ap, ... ,an >

{Resulta una lista de longitud n+1, en la que x ocupa la posicion p. Si p=n entonces la lista resultante es l=< a1,...,an-1,x,an>}

void borrar(tPosicion p, tLista l) PRE: l = <a1,a2, ... ,an>, 1 <= p <= n POST: l = <a1, ... ,ap-1, ap+1, ... ,an >

{elimina el elemento que ocupa la posición p, de forma que ahora la posición p la ocupa el elemento que se encontraba en la posición p+1}

Respecto al conjunto de primitivas que hemos presentado, no son más que un ejemplo representativo de las primitivas más importantes que nos sirve para ilustrar la forma en que se debe construir el tipo de dato abstracto Lista. Obviamente, en una implementacion real es posible optar por un conjunto distinto de primitivas teniendo en cuenta varios puntos:

El conjunto de primitivas tiene que ser completo en el sentido de que tiene que ser posible construir cualquier algoritmo que use listas utilizando únicamente las primitivas que se incluyen.

(4)

Manual del Alumno

También debe ser suficiente pero no obligatoriamente mínimo. Aunque no sea necesario incluir nuevas primitivas, puede ser conveniente añadir nuevas funciones si existen motivos como:

a.

La función va a ser probablemente muy usada. Es el caso de primitivas como la de posición o anterior, las cuales pueden ser perfectamente programadas en base a las demás. Es fácil imaginar nuevas primitivas como pueden ser una primitiva de copia de una lista en otra, una primitiva de ordenación eficiente de los elementos de la lista,etc.

b.

La función va a ser usada con cierta asiduidad y por otra parte la

implementación haciendo uso de las demás funciones primitivas empeora sustancialmente la velocidad de operación. Es el caso de primitivas como anterior que pueden ser programadas en base a primero y siguiente pero que en determinadas implementaciones pasaría esta operación de tener orden constante a tener orden de la longitud de la lista.

Es posible tener que rehacer el conjunto de primitivas atendiendo a razones referentes a una eficiente utilización de los recursos hardware. Es el caso por ejemplo de la función anula, la cual en ciertas implementaciones es altamente probable se a transformada en una función de creación que se ve completada con otra nueva de destrucción:

.

CrearVacia: Devuelve una lista que está vacía.

i.

CrearCopia: Toma como argumento una lista ya creada y devuelve una lista distinta que es copia de la primera.

Las cabeceras de las funciones pueden necesitar ser modificadas para hacer viable su implementación. Es el caso por ejemplo de que una función no pueda devolver un tipo de dato o que el tipo de dato sea muy complejo y que pasarlo por valor o devolverlo como salida de una función pueda convertirse en algo ineficente dado su tamaño. En muchos casos, por tanto, será aconsejable no pasar estructuras directamente sino un puntero a ellas, que una función no devuelva un valor sino que se devuelva mediante un puntero en uno de sus parámetros,etc.

Un tipo de dato abstracto es un producto software y como tal es algo dinámico que está sujeto a un mantenimiento. De esta forma tendremos que tener en cuenta que el conjunto de primitivas de un TDA es algo extensible. En este sentido, el conjunto de funciones que incorporamos a un TDA no debe ser diseñado considerando que debemos añadir todas y cada una de primitvas que creemos que se necesitarán, es decir, puede ser más conveniente retrasar la incorporación de ciertas primitavas en caso de que dudemos de su utilidad. Téngase en cuenta que, desde el punto de vista del mantenimiento del software que usa el TDA, es mucho menos costosa la adicíon de nuevas primitivas que la supresion de algunas ya existentes.

EJEMPLOS DE USO.

Es importante aprender a usar las listas basándonos en estas especificaciones, aunque este tipo no venga en el lenguaje en el que estemos trabajando y no conozcamos la implementación que se va a usar.

Por ejemplo, vamos a escribir un porcedimiento que escriba todos los elementos de una lista.

Suponiendo que para tElemento existe un procedimiento, ,escribe(x), que escribe un elemento de dicho tipo.

void salida (tLista t) {

(5)

Manual del Alumno

tposicion p;

telemento x;

for (p = primero(l); p != fin(l); p = siguiente(p, l)){

x = elemento(p, l);

escribe(x);

} }

Veamos otro ejemplo de copia de una lista en otra:

tLista copia (tLista l) {

tLista l2;

tPosicion p;

anula(&l2);

for (p=primero(l);p!=fin(l);p=siguiente(p,l)) insertar(elemento(p,l),fin(l2),l2);

return l2;

}

En el siguiente ejemplo, vamos a ver un porcedimiento para eliminar todos los elementos repetidos de la lista. Suponemos que el tipo básico es tElemento y que existe una función lógica, igual(x,y), que nos dice cuando son iguales dos elementos de este tipo. Se podría pensar en que bastaría considerar la igualdad del C(==), pero es posible que no coincidad con la igualdad de tElemento. Por ejemplo, consideremos los numeros racionales definidos como:

typedef struct { int num;

int den;

}racional;

(6)

Manual del Alumno

Entonces si x,y son de tipo racional, entonces pueden representar el mismo racional, ser iguales, aunque no se verifique x.num==y.num && x.den==y.den .La función igual sería en este caso:

int igual (int x;int y) {

return (x.num*y.den == y.num*x.den);

}

Con estas consideraciones, el procedimiento para eliminar las repeticiones de una lista sería como sigue:

void elimina (tLista l, int (*es_igual)(tElemento, tElemento)) {

tposicion p, q;

for (p = primero(l); p != fin(l); p = siguiente(p, l)){

q = siguiente(p ,l);

while (q != fin(l))

if ((*es_igual)(elemento(p, l), elemento(q, l)))

borrar(q, l);

else

q = siguiente(q, l);

} }

Unos comentarios respecto a esto ejemplo:

La forma de usar esta función con nuestro ejemplo sobre una lista l es mediante la llamada elimina(l,igual).

La variable l es un parámetro que se pasa por valor. Nótese que su valor no cambia a lo largo de la función dado que en las especificaciones siempre se pasa este parámetro de esta forma. La única función de las especificadas que se puede usar para cambiar el valor de una variable de tipo tLista es anula.

(7)

Manual del Alumno

Se puede observar que tan solo se pasa a la posición siguiente cuando no se borra el elemento que se encuentra en la posición p ya que en el caso de que sea borrado ese elemento habrá que analizar el elemento que se encuentra en esa posición p.

3. IMPLEMENTACIÓN DE LAS LISTAS.

IMPLEMENTACIÓN DE LISTAS MEDIANTE VECTORES.

Las listas se pueden implementar usando las posiciones consecutivas de un vector.Como las listas tienen longitud variable y los vectores longitud fija, esto se resuelve considerando vectores de tamaño igual a la longitud maxima de la lista y un entero donde se indica la posición donde se encuentra el último elemento de la lista.

Así podriamos tener:

#define LMAX = 100; /* Una constante adecuada. */

typedef int tElemento; /* Por ejemplo. */

typedef struct{

tElemento elementos[LMAX];

int n;

} Lista;

typedef Lista *tLista;

typedef int tPosicion;

FUNCIÓN DE ABSTRACCIÓN:

Dado el objeto del tipo rep r = {elementos, n}, el objeto abstracto que representa es:

<r.elementos[0], r.elementos[1], ... , r.elementos[n-1]>

INVARIANTE DE LA REPRESENTACIÓN:

VERDAD.

(8)

Manual del Alumno

La implementación de la mayoria de las operaciones es prácticamente inmediata. Por ejemplo, las mas simples son:

static void error (char *mensaje) {

fprintf(stderr, "%s\n", mensaje);

exit(-1);

}

void anula (tLista *l) {

*l = (tLista) malloc (sizeof(Lista));

if (*l==NULL) {

error("No hay memoria.");

}

(*l)->n=-1;

}

tPosicion primero (tLista l) {

return 0;

}

tPosicion fin (tLista l) {

return(l->n+1);

}

tPosicion siguiente (tPosicion p, tLista l) {

if ((p < 0) || (p > l->n))

(9)

Manual del Alumno

error("Posición no válida.");

return (p + 1);

}

tPosicion anterior (tPosicion p, tLista l) {

if ((p <= 0) || (p > l->n+1)) error("Posición no válida.");

return (p - 1);

}

tElemento elemento (tPosicion p, tLista l) {

if ((p < 0) || (p > l->n))

error("Posición no válida.");

return (l->elementos[p]);

}

Las únicas operaciones que pueden presentar un poco de dificultad son las de insertar,borrar y posicion. La función posición tiene que realizar una búsqueda lineal en un vector. En caso de que el elemento considerado no esté en el vector, esta función debe devolver lo mismo que fin(l).

tPosicion posicion (tElemento x, tLista l) {

tPosicion q;

int encontrado;

q = encontrado = 0;

while ((q <= l->n) && (!encontrado)) { if (l->elementos[q] == x)

encontrado=1;

else q++;

};

(10)

Manual del Alumno

return q;

}

Para la operación de inserción hay que hacer previamente un hueco donde realizar dicha inserción. Para el borrado, hay que "rellenar" el hueco dejado por el elemento borrado. En la figura podemos observar en las flechas superiores los movimientos de los elementos que se han tenido que realizar para insertar en la posición p (coinciden con los movimientos en sentido contrario que se deben realizar para borrar el elemento que se encuentra en dicha posición).

Como consecuencia de ello, habrá que mover, en ambos casos, todos los elementos que ocupen una posición superior a la considerada para realizar la inserción o borrado. Esto tiene como consecuencia que la eficiencia de las operaciones no es muy buena, del orden del tamaño de la lista.

void insertar (tElemento x, tPosicion p, tLista l) {

tPosicion q;

if ((p > l->n+1) || (p < 0))

error("Error p incorrecta.");

else if (l->n >= LMAX-1)

error("Lista llena");

else{

for (q = l->n; q >= p; q--)

l->elementos[q+1] = l->elementos[q];

l->n++;

l->elementos[p] = x;

} }

(11)

Manual del Alumno

void borrar (tPosicion p,tLista l) {

if ((p > l->n) || (p < 0)) error("p incorrecta.");

else { l->n--;

for (; p <= l->n; p++)

l->elementos[p]=l->elementos[p+1];

} }

Aparte de la mala eficiencia de estas dos operaciones, que veremos como se mejorará en otras implementaciones, otro inconveniente de esta implementación es que las listas tienen un tamaño máximo del que no se puede pasar. Es decir, no corresponden exactamente a las especificaciones consideradas en un principio. Por otra parte, siempre hay una porción de espacio reservada para los elementos de la lista, y que no se utiliza al ser el tamaño de la lista, en un momento dado menor que el tamaño máximo. Esto se hace más grave si las distintas listas que se representen son de un tamaño muy distinto.

Otro detalle importante de esta implementación es, cómo hemos mencionado anteriormente, la necesidad de una función de destrucción ya que ahora mismo la memoria que se requiere cada vez que se hace una llamada a la función anula no es recuperada en ningún momento.Sería interesante añadir una nueva función tal como la siguiente (Nótese que si la constante LMAX es grande y se hace uso de un número alto de listas esta función no sólo se hace interesante sino que necesaria:

void destruye (tLista l) {

free(l);

}

Teniendo en cuenta los problemas que presenta la implementación que hemos presentado mediante vectores y considerando las posibilidades que nos brinda el lenguaje C, podemos proponer una versión mas optimizada:

typedef int tElemento /* Por ejemplo. */

(12)

Manual del Alumno

typedef struct{

tElemento *elementos;

int Lmax;

int n;

}Lista;

typedef Lista *tLista;

typedef int tPosicion;

tLista crear (int tamanoMax) {

tLista l;

l = (tLista) malloc(sizeof(Lista));

if (l == NULL)

error("Memoria Insuficiente");

l->Lmax = tamanoMax;

l->n = -1;

l->elementos = (tElemento *)malloc(tamanoMax*sizeof(tElemento));

if (l->elementos == NULL)

error("Memoria Insuficiente.");

}

void destruir (tLista l) {

free(l->elementos);

free(l);

}

(13)

Manual del Alumno

Donde las demás primitivas quedarían de la misma forma sustituyendo LMAX por l->LMAX En esta nueva implementación conseguimos resolver con exito dos cosas:

1.

Tamaños variables: Ahora la primitiva anula ha sido sustituida por la primitiva crear a la que se pasa un parámetro indicando el tamaño maximo que tendra la lista.La mejora, por lo tanto, ha sido sustancial teniendo en cuenta que el tamaño máximo que es necesario para la versión anterior debe ser superior a la más grande de las listas que se manejan y por consiguiente para pequeñas listas habría una gran cantidad de memoria desperdiciada.

2.

Creación y Destrucción: Aunque en la versión anterior se solucionó el problema al proponer la función destruye, es importante destacar que en esta versión también se ofrece el constructor y destructor del tipo de dato permitiendo de esta forma recuperar los recursos ocupados por las listas que no se volverán a usar.

Es importante destacar la forma en que se deben usar las funciones de un tipo de dato abstracto (normalmente en la especificación junto con algún ejemplo si es necesario). Así destacaremos que que en este nuevo conjunto de primitivas incluyendo crear y destruir el uso del TDA debe ser:

1.

Declaración de la variable de tipo tLista.

2.

Creación de la lista mediante la primitva crear.

3.

Uso de la lista mediante primitivas distintas a la de creación y destrucción.

4.

Destrucción de la lista mediante la primitiva destruir.

Teniendo en cuenta:

1.

El uso de la primitiva crear sobre una lista ya creada provocará una pérdida de los recursos de memoria ocupados por esta lista y la actualización de su valor a la lista vacía.

2.

El uso de la primitiva destruir a una lista no creada o a una lista que aunque se creó ha sido destruida es erróneo y provocará resultados imprevisibles.

3.

Obviamente, después de la destrucción de una lista, se podrá usar de nuevo la misma variable en la creación, uso y destrucción de una nueva lista.

Como ejemplo mostramos una función que guarda en una lista los números enteros del 0 al 9, despues la recorre eliminando los impares y por último escribe el resultado dos veces, desde el primer elemento al último y desde el último al primero:

void EJEMPLO () {

int a;

tLista l;

tPosicion p;

l = crear(10);

for (a=0; a<10; a++)

insertar(a, primero(l), l);

(14)

Manual del Alumno

for (p=primero(l); p!=fin(l); ) { a = elemento(p,l);

if (a%2)

borrar(p,l);

else

p = siguiente(p,l);

}

for (p=primero(l); p!=fin(l); p=siguiente(p,l)) { a = elemento(p,l);

printf("Elemento: %d \n",a);

}

printf(" \n \n ");

for (p=fin(l); p!=primero(l); p=anterior(p,l)) { a = elemento(anterior(p,l), l);

printf("Elemento: %d \n",a);

}

destruir(l);

}

IMPLEMENTACIÓN DE LISTAS MEDIANTE CELDAS ENLAZADAS POR PUNTEROS.

Una implementación de las listas que evita los problemas anteriormente mencionados para los vectores, es la que está basada en el uso de punteros. Esta implementación se basa en representar cada elemento, ai, de una lista <a1,a2, ...,an> como una celda dividida en dos partes: un primer campo donde se almacena el elemento en cuestión; y un segundo campo donde se almacena un puntero, que nos indica donde está el siguiente elemento de la lista, tal como se muestra en la parte (a) de la figura.

La celda que contiene el último elemento de la lista tiene un puntero donde se almacena NULL.

Así, la lista quedaria como se muestra en la parte (b) de la figura.

(15)

Manual del Alumno

Para realizar más facilmente las operaciones es conveniente considerar una celda inicial, llamada de cabecera y donde no se almacena ningún elemento de la lista. De esta forma la lista propiamente diche vendrá representada por un puntero que indique la dirección de la cabecera y que permite obtener los distintos elementos de la misma como se muestra finalmente en la parte (c) de la figura.

Para estas listas es conveniente representar la posición mediante un puntero que acceda al elemento correspondiente. Sin embargo, no se va a consider un puntero con la dirección de la celda donde está el elemento considerado, sino la dirección de la celda donde está el elemento anterior. Con esto se puede acceder a dicho elemento (mediante el puntero correspondiente), y también será más útil para las operaciones de inserción y borrado. La posición del primer elemento, vendrá representada entonces por un puntero apuntando a la celda de cabecera, es decir, idéntico a la lista, l. La posición del elemento ai se representará mediante un puntero, indicando la celda del elemento ai-1. La posición detrás del último elemento será un puntero apuntando a an.

Debido a que la posición lógica de un elemento viene determinada por la posición física del anterior puede dar lugar a un error de programación si se trabaja con varias posiciones a la vez y se realizan borrados. Por ejemplo, consideremos una lista con 3 elementos y 2 punteros indicando la posición del segundo (puntero p) y tercer (puntero q) elemento (ver figura).

Si no atendemos a la implementación, el borrar el elemento de la posición p (elemento a2) podemos considerar dos resultados:

Dado que q apunta al tercer elemento y quedan dos, q resulta apuntando a fin(l).

Dado que a3 pasa a ser el segundo elemento y q apuntaba a a3, ahora q apunta al elemento segundo de la lista.

En general, el primer caso corresponde a la implementación realizada mediante vectores y el segundo a la realizada mediante celdas enlazadas teniendo en cuenta:

(16)

Manual del Alumno

En el caso de las listas mediante celdas enlazadas, el comportamiento es válido excepto para el caso de dos posiciones consecutivas. Si en el ejemplo que nos ocupa borramos el segundo elemento, la zona a la que apunta q es liberada y por tanto es incorrecto usar su contenido además de haber quedado fuera de la lista y por tanto es una posición no válida.

En el caso de las matrices,ocurre de forma paralela que el comportamiento es válido excepto si una posición indicaba el final de la lista. Al eliminar un elemento, el final de la lista se ve modificado y por tanto si una posición indicaba el final, queda apuntando a una zona fuera de la lista.

Es por ello que el uso simultáneo de varias posiciones conviene que sea manejado con cuidado. Obviamente, el que el acceso a un elemento se produzca por medio del elemento anterior conviene que sea indicado en la especificación del TDA mediante el correspondiente aviso de que el borrado de un elemento invalidad los valores de posición del inmediatamente posterior (por ejemplo, se puede indicar el comportamiento de los valores posición cuando se usan las funciones de inserción y de borrado).

En C, la definición de tipos correspondiente a la implementación por punteros sería:

typedef struct Celda{

tElemento elemento;

struct Celda *siguiente;

}celda;

typedef celda *tPosicion;

typedef celda *tLista;

FUNCIÓN DE ABSTRACCIÓN

Dado el objeto del tipo rep l={elemento, siguiente}, el objeto abstracto que representa es:

<l->siguiente->elemento, l->siguiente->siguiente->elemento, ... ,l->siguiente-> (n) ->siguiente-

>elemento>

Donde r->siguiente-> (n+1) ->siguiente == NULL.

INVARIANTE DE REPRESENTACIÓN

Todas las direcciones de los campos siguiente proceden de llamadas (tposicion) malloc(sizeof(celda)) o son NULL.

Y las operaciones se pueden implementar como sigue:

tLista crear () {

tLista l;

l = (tLista)malloc(sizeof(celda));

if (l == NULL)

(17)

Manual del Alumno

error("Memoria Insuficiente.");

l->siguiente = NULL;

return l;

}

void destruir (tLista l) {

tPosicion p;

for (p = l; l != NULL; p = l){

l = l->siguiente;

free(p);

} }

tPosicion fin (tLista l) {

tPosicion p;

p=l;

While (p->siguiente != NULL) { p = p->siguiente;

}

return p;

}

Repecto a esta función es importante senñalar , que siempre tiene que recorrer toda la lista para devolver el puntero que se muestra en la figura. Por lo que su eficiencia es del orden de la longitud de la lista. Habría que procurar no utilizarla demasiado si se usa esta implementación.

(18)

Manual del Alumno

Por ejemplo, un ciclo while con una condición p!=fin(l) se debe sustituir por:

q=fin(l);

while (p!=q)...

void insertar (tElemento x, tPosicion p, tLista l) {

tPosicion q;

q = (tPosicion)malloc(sizeof(celda));

if (q == NULL)

error("Memoria Insuficiente.");

q->elemento = x;

q->siguiente = p->siguiente;

p->siguiente = q;

}

La forma en la que se realiza la inserción puede observarse en la figura.

(19)

Manual del Alumno

Es importante señalar varias cosas de este procedimiento:

Tarda siempre un tiempo constante. No como en la implementación vectorial en que tardaba un tiempo proporcional a la longitud de la lista.

No comprueba la precondición. Se podría hacer, pero entonces se perdería mucho tiempo en la comprobación. Es responsabilidad del programador utilizarlo siempre con posiciones de esta lista. Si no se hace así, puede dar lugar a graves errores.

El procedimiento funciona bien en los casos extremos de la primera posición y la posición fin(l). En las listas sin cabecera estos casos habría que haberlos considerado aparte.

tPosicion siguiente (tPosicion p, tLista l) {

if (p->siguiente==NULL) {

error("No hay siguiente de fin.");

}

return p->siguiente;

}

tPosicion primero (tLista l)

(20)

Manual del Alumno

{

return l;

}

tPosicion posicion (tElemento x, tLista l) {

tPosicion p;

int encontrado;

p = primero(l);

encontrado = 0;

while ((p->siguiente != NULL) && (!encontrado)) { if (p->siguiente->elemento == x)

encontrado=1;

else p = p->siguiente;

}

return p;

}

Notas referentes a la función posicion:

Es importante comprobar que la función verifica las postcondiciones en los dos casos posibles: cuando esté y cuando no esté el elemento buscado en la lista.

La complejidad es igual al caso de la implementación mediante vectores. En término medio hay que recorrer la mitad de la lista.

En la condición del bucle aparece la comparación (p->siguente != NULL). Esta es equivalente a (p !=fin(l)), pero entonces aumentaria mucho la complejidad debido a la poca eficiencia de la función fin(l). Se podría pensar en sustituir en cualquier programa, esta condición por la que hemos usado aquí. Pero esto lo hemos podido hacer porque ésta es una operación primitva y se puede hacer referencia a la implementación. En un programa que use las listas no se debe hacer.

tElemento elemento (tPosicion p, tLista l) {

(21)

Manual del Alumno

if (p->siguiente == NULL) {

error("Error: posicion fin(l).");

}

return p->siguiente->elemento;

}

void borrar (tPosicion p, tLista l) {

tPosicion q;

if (p->siguiente == NULL)

error("Error: posicion fin(l).");

q = p->siguiente;

p->siguiente = q->siguiente;

free(q);

}

Respecto a esta implementación son válidos los mismos comentarios que para la función insercion.

4. COMPARACIÓN DE MÉTODOS.

Resulta de interés saber si es mejor usar una implementación de listas basada celdas

enlazadas o en matrices en una circunstancia dada. Frecuentemente la contestación depende de las operaciones uqe queramos llevar a cabo, o de cuales son llevadas a cabo con mayor asiduiadad. Otras veces, la decisión es en base a la longitud de la lista.

Los puntos principales a considerar son los siguentes:

1.

La implementación matricial nos obliga a especificar el tamaño máximo de una lista en tiempo de compilación. Si no podemos poner una cota a la longitud de la lista,

posiblemente deberíamos coger una implementación basada en punteros.

Lógicamente, este problema ha sido parcialmente solucionado con la parametrización del tamaño máximo de la lista, pero aún así hay que delimitar el tamaño máximo para cada una de las listas.

2.

Ciertas operaciones requieren más tiempo en unas implementaciones que en otras.

Por ejemplo insertar y borrar realizan un número constante de pasos para una lista enlazada, pero necesitan tiempo proporcional al número de elementos siguientes cuando usamos la representación matricial.Inversamente, ejecutar fin requiere tiempo constante con la implementación matricial, pero tiempo proporcional a la lista si usamos la implementación por punteros simplemente-enlazadas (aunque recordemos que el problema es solucionable añadiendo un puntero). Por otro lado, en las listas

(22)

Manual del Alumno

doblemente-enlazadas se requiere tiempo constante para todas las operaciones (excepto la de posición que requiere un tiempo proporcional a la longitud de la lista).

3.

La implementación matricial puede derrochar espacio, ya que usa la cantidad máxima de espacio independientemente del número de elementos presentes en la lista en un momento dado. La implementación por punteros usa tanto espacio como necesita para los elementos que hay en la lista, pero necesita espacio adicional para los punteros de cada celda.Por último, las listas doblemente-enlazadas aunque son las más eficientes requieren dos punteros para cada elemento.

4.

En las listas enlazadas la posición de un elemento se determina con un puntero a la celda del elemento anterior por lo que hay que tener cuidado con la operación de borrado si se trabaja con varias posiciones tal y como vimos anteriormente. En el caso de la implementación matricial, si borramos un elemento, todas las posiciones

posteriores a ese elemento apuntarán al siguiente al que apuntaban y si existe una posición apuntando al final de la lista, ésta queda invalidada. (El comportamiento tambien es distinto para la inserción). En el caso de las listas doblemente-enlazadas, su comportamiento es el mas cómodo siempre que la implementación realizada no provoque que la posición usada en el borrado quede invalidada.

PILAS

1. INTRODUCCIÓN.

Una Pila es una clase especial de lista en la cual todas las inserciones y borrados tienen lugar en un extremo denominado extremo, cabeza o tope. otro nombre para las pilas son listas FIFO (último en entrar, primero en salir) o listas pushdown (empujdas hacia abajo). El modelo intuitivo de una pila es un conjunto de objetos apilados de forma que al añadir un objeto se coloca encima del ultimo añadido y para quitar un objeto del montón hay que quitar antes los que están por encima de él.Un tipo de dato abstracto PILA incluye las siguientes operaciones.

2. OPERACIONES PRIMITIVAS DE LAS PILAS.

Dentro del tipo abstracto de pila podemos proponer las siguientes primitivas:

CREAR() DESTRUIR(P) TOPE(P) POP(P) PUSH(x,P) VACIA(P)

ESPECIFICACIÓN SEMANTICA Y SINTACTICA pila crear ()

Efecto: Devuelve un valor del tipo pila preparado para ser usado y que contiene un valor de pila vacia.Esta operación es la misma que la de las listas generales.

void destruir (pila *P) Argumentos: Una pila P.

Efecto: Libera los recursos que mantienen la lista P de forma que para volver a usarla se debe asignar una nueva pila con la operación de creación. Esta operación es la misma que la de las listas generales.

(23)

Manual del Alumno

telemento tope (pila P)

Argumentos: Una pila P que debe ser no vacía.

Efecto: Devuelve el elemento en la cabeza de la pila P. Si, como es lógico, identificamos la cabeza de una pila con la posición 1, entonces TOPE(P) puede escribirse en términos de operaciones de listas como ELEMENTO (PRIMERO(P),P).

void pop (pila P)

Argumentos: Una pila P que debe ser no vacía. Es modificada.

Efecto: Borra el elemento del tope de la pila P, esto es, BORRA (PRIMERO(P),P).

Algunas veces es conveniente implementar POP como una función que devuelve el elemento que acaba de borrar.

void push (telemento x, pila P) Argumentos:

x: Un elemento que deseamos poner en la pila.

p: Una pila P valí donde deseamos poner el elemento x.

Efecto:Inserta el elemento x en el tope de la pila P. El elemento tope antiguo se convierte en el siguiente al tope y asi sucesivamente. En términos de primitivas de listas esta operación es INSERTA (x,PRIMERO(P),P).

int vacia (pila P) Argumentos: Una pila P.

Efecto: Devuelve si P es una pila vacía.

EQUIVALENCIA CON LAS LISTAS

3. IMPLEMENTACIÓN DE LAS PILAS.

Todas las implementaciones de las listas que hemos descrito son validas para las pilas ya que una pila junto con sus operaciones es un caso especial de una lista con sus operaciones. Aún asi conviene destacar que las operaciones de las pilas son más específicas y que por lo tanto la implementación puede ser mejorada especialmente en el caso de la implementación matricial.

IMPLEMENTACIÓN MATRICIAL DE LAS PILAS.

La implementacion basada en matrices para las listas que dimos anteriormente, no es

particularmente buena para las pilas, porque cada PUSH o POP requiere mover la lista entera hacia arriba o hacia abajo y por tanto, requiere un tiempo proporcional al número de elementos en la pila. Una forma mejor de usar matrices toma en cuenta el hecho de que inserciones y borrados ocurren solamente en el tope y por lo tanto dichas operaciones sólo se efectuarán en un extremo de la estructura. Obsérvese que la mejora puede ser introducida haciendo las inserciones y borrados al final de la lista dentro de la implementación matricial de las listas.

Podemos situar el fondo de la pila en el primer elemento de la matriz y hacer crecer la pila

(24)

Manual del Alumno

hacia el ultimo elemento de la matriz. Un cursor llamado tope indica la posición actual del primer elemento de la pila.

Para esta implementacion basada en matrices de pilas definimos el tipo de dato abstracto Pila por

typedef int tElemento /* Por ejemplo */

typedef struct {

tElemento *elementos;

int Lmax;

int tope;

} tipoPila;

typedef tipoPila *pila;

FUNCIÓN DE ABSTRACCIÓN.

Dado el objeto del tipo rep p, *p = (elemento, Lmax, tope), el objeto abstracto que representa es:

<p->elemento[p->tope], p->elemento[p->tope - 1],..., p->elemento[0]>.

INVARIANTE DE LA REPRESENTACIÓN.

Dado el objeto del tipo rep p, *p = (elemento, Lmax, tope) debe cumplir:

a.

p tiene valores obtenidos de llamadas (pila) malloc(sizeof(tipopila));

b.

p->elemento tiene una dirección válida de tipo telemento*.

c.

p->Lmax > 0.

d.

-1 <= p->tope <= p->Lmax - 1.

Las operaciones tipicas sobre las pilas están implementadas en las siguientes funciones y procedimientos.

pila CREAR (int tamanoMax) {

pila P;

P = (pila) malloc(sizeof(tipoPila));

if (P == NULL)

(25)

Manual del Alumno

error("No hay memoria suficiente");

P->Lmax = tamanoMax;

P->tope = -1;

P->elementos = (tElemento *) malloc(tamanoMax, sizeof(tElemento));

if (P->elementos == NULL)

error("No hay memoria suficiente.");

return P;

}

void DESTRUIR (pila *P) {

free((*P)->elementos);

free(*P);

*P = NULL;

}

int VACIA (pila P) {

return(P->tope == -1);

}

tElemento TOPE (pila P) {

if (VACIA(P)) {

error("No hay elementos en la pila.");

return(P->elementos[P->tope]);

}

void POP (pila P) {

if (VACIA(P)) {

error("No hay elementos en la pila.");

P->tope--;

}

void PUSH (tElemento x, pila P) {

if (P->tope==P->Lmax-1) { error("Pila llena");

p->tope++;

p->elementos[p->tope] = x;

}

Como puede observar el lector, esta implementación es justamente la realizada sobre las listas mediante vectores pero simplificada de una forma considerable.

IMPLEMENTACIÓN DE LAS PILAS MEDIANTE CELDAS ENLAZADAS.

La representación por celdas enlazadas de una pila es facil, porque PUSH y POP operan sólamente sobre la celda de cabecera. De hecho, las cabeceras pueden ser punteros mejor que celdas completas, ya que no hay noción de posición para las pilas y por tanto no necesitamos representar la posición 1 en una forma análoga a otras posiciones tal y como muestra figura.

(26)

Manual del Alumno

Obviamente, el que las funciones sobre pilas sean más especificas que sobre listas implica que en general se simplificará la implementación (que responde a la estructura de la figura

anterior).

FUNCIÓN DE ABSTRACCIÓN.

Dado el objeto del tipo rep p, el objeto abstracto que representa es:

<(*p)->elemento, (*p)->siguiente->elemento, ... , (*p)->siguiente-> (n) ->siguiente->elemento>.

con (*p)->siguiente-> (n+1) ->siguiente = NULL.

INVARIANTE DE LA REPRESENTACIÓN.

Dado un objeto del tipo rep p, debe cumplir:

a.

p tiene valores obtenidos de llamadas (tiponodo **) malloc(sizeof(tiponodo *));

b.

Los campos siguiente de los nodos tienen direcciones válidas, obtenidas de llamadas a (tiponodo *) malloc(sizeof(tiponodo)). Sólo es NULL el último.

typedef struct pnodo { tElemento elemento;

struct pnodo *siguiente;

} tipopnodo;

typedef tipopnodo **pila;

pila CREAR () {

pila P;

P = (tipopnodo **) malloc(sizeof(tipopnodo *));

if (P == NULL) {

error("Memoria insuficiente.");

*P = NULL;

return P;

}

void DESTRUIR (pila P) {

while (!VACIA(P)) POP(P);

free(P);

}

tElemento TOPE (pila P) {

if (VACIA(P))

error("No existe tope.");

return((*P)->elemento);

(27)

Manual del Alumno

}

void POP (pila P) {

tipopnodo *q;

if (VACIA(P))

error("No existe tope.");

q = (*P);

(*P) = q->siguiente;

free(q);

}

void PUSH (tElemento x,pila P) {

tipopnodo *q;

q = (tipopnodo *) malloc(sizeof(tipopnodo));

if (q == NULL) {

error("No hay memoria.");

q->elemento = x;

q->siguiente = (*P);

(*P) = q;

}

int VACIA (Pila P) {

return (*P == NULL);

}

4. EJEMPLO DE APLICACIÓN.

Editor de líneas.

#: carácter de borrado

@: carácter de cancelación de línea IDEA: procesar una línea de texto usando una pila.

Leer un carácter

Si el carácter no es '#' ni '@' meterlo en la pila

Si el carácter es '#' sacar de la pila

Si el carácter es '@' vacia la pila

El código podría ser:

editar (void) { pila p, q;

char c;

p = crear();

while ((c = (char)getchar()) != EOF) { if (c == '#')

quitar(p);

else

if (c == '@') {

destruir(&p);

p = crear();

} else

(28)

Manual del Alumno

poner(c, p);

};

q = crear();

while (!vacia(p)) {

poner(tope(p), q);

quitar(p);

};

while (!vacia(q)) {

printf("%c", tope(q));

quitar(q);

};

destruir(&q);

destruir(&p);

}

COLAS

1. INTRODUCCIÓN.

Una Cola es otro tipo especial de lista en el cual los elementos se insertan por un extremo (el posterior) y se suprimen por el otro (el anterior o frente). Las colas se conocen tambien como listas FIFO (primero en entrar,primero en salir). Las operaciones para las colas son análogas a las de las pilas. Las diferencias sustanciales consisten en que las inserciones se hacen al final de la lista, y no al principio, y en que la terminología tradicional para colas y listas no es la misma. Las primitivas que vamos a considerar para las colas son las siguientes.

2. OPERACIONES PRIMITIVAS DE LAS COLAS.

Dentro del tipo abstracto de cola podemos proponer las siguientes primitivas:

CREAR() DESTRUIR(C) FRENTE(C)

PONER_EN_COLA(x,C) QUITAR_DE_COLA(C) VACIA(C)

ESPECIFICACIÓN SEMANTICA Y SINTACTICA.

cola crear ()

Argumentos: Ninguno.

Efecto: Devuelve una cola vacia preparada para ser usada.

void destruir(cola C) Argumentos: Una cola C.

Efecto: Destruye el objeto C liberando los recursos que mantiene que empleaba.Para volver a usarlo habrá que crearlo de nuevo.

(29)

Manual del Alumno

tElemento frente (cola C)

Argumentos: Recibe una cola C no vacía.

Efecto: Devuelve el valor del primer elemento de la cloa C. Se puede escribir en función de las operaciones primitivas de las listas como: ELEMENTO(PRIMERO(C),C).

void poner_en_cola (tElemento x, cola C) Argumentos:

x: Elemento que queremos insertar en la cola.

C: Cola en la que insertamos el elemento x.

Efecto: Inserta el elemento x al final de la cola C. En función de las operaciones de las listas seria: INSERTA(x,FIN(C),C).

void quitar_de_cola (cola C)

Argumentos: Una cola C que debe ser no vacía.

Efecto: Suprime el primer elemento de la cola C. En función de las operaciones de listas seria: BORRA(PRIMERO(C),C).

int vacia (cola C) Argumentos: Una cola C.

Efecto: Devuelve si la cola C es una cola vacía.

EQUIVALENCIA CON LAS LISTAS

3. IMPLEMENTACIÓN DE LAS COLAS. IMPLEMENTACIÓN DE COLAS BASADA EN CELDAS ENLAZADAS.

Igual que en el caso de las pilas, cualquier implementación de listas es válida para las colas.

No obstante, para aumentar la eficiencia de PONER_EN_COLA es posible aprovechar el hecho de que las inserciones se efectúan sólo en el extremo posterior de forma que en lugar de recorrer la lista de principio a fin cada vez que desea hacer una inserción se puede mantener un apuntador al último elemento. Como en las listas de cualquier clase, tambien se mantiene un puntero al frente de la lista. En las colas ese puntero es útil para ejecutar mandatos del tipo FRENTE o QUITA_DE_COLA. Utilizaremos al igual que para las listas, una celda cabecera con el puntero frontal apuntándola con lo que nos permitirá un manejo más cómodo.

Gráficamente, la estructura de la cola es tal y como muestra la figura:

Una cola es pues un puntero a una estructura compuesta por dos punteros, uno al extremo anterior de la cola y otro al extremo posterior. La primera celda es una celda cabecera cuyo campo elemento se ignora.

La definición de tipos es la siguiente:

typedef struct Celda{

(30)

Manual del Alumno

tElemento elemento;

struct Celda *siguiente;

} celda;

typedef struct {

celda *ant,*post;

} tcola;

typedef tcola *cola;

FUNCIÓN DE ABSTRACCIÓN.

Dado el objeto del tipo rep c, *c = (ant, post), el objeto abstracto que representa es:

<c->ant->siguiente->elemento, c->ant->siguiente->siguiente->elemento, ..., c-

>ant->siguiente-> (n) ->siguiente->elemento>, tal que c->siguiente->siguiente->

(n) ->siguiente = c->post.

INVARIANTE DE LA REPRESENTACIÓN.

Dado un objeto del tipo rep c, *c = (ant, post), debe cumplir:

a.

c tiene valores obtenidos de llamadas (tcola **) malloc(sizeof(tcola));

b.

Los campos siguiente de los nodos, c->ant y c->post tienen direcciones válidas, obtenidas de llamadas a (celda) malloc(sizeof(celda)). Sólo es NULL el últimode los campos siguiente.

Con estas definiciones, la implementación de las primitivas es la siguiente:

cola CREAR () {

cola C;

C = (tcola *) malloc(sizeof(tcola));

if (C == NULL)

error("Memoria insuficiente.");

C->ant = C->post = (celda *)malloc(sizeof(celda));

if (C->ant == NULL)

error("Memoria insuficiente.");

C->ant->siguiente = NULL;

return C;

}

void DESTRUIR (cola C) {

while (!VACIA(C))

QUITAR_DE_COLA(C);

free(C->ant);

free(C);

}

int VACIA (cola C) {

return(C->ant == C->post);

}

tElemento FRENTE (cola C) {

(31)

Manual del Alumno

if (VACIA(C)) {

error("Error: Cola Vacia.");

}

return(C->ant->siguiente->elemento);

}

void PONER_EN_COLA (tElemento x,cola C) {

C->post->siguiente = (celda *) malloc(sizeof(celda));

if (C->post->siguiente == NULL) error("Memoria insuficiente.");

C->post = C->post->siguiente;

C->post->elemento = x;

C->post->siguiente = NULL;

}

void QUITAR_DE_COLA (cola C) {

celda *aux;

if (VACIA(C))

error("Cola vacia.");

aux = C->ant;

C->ant = C->ant->siguiente;

free(aux);

}

Este procedimiento QUITAR_DE_COLA suprime el primer elemento de C desconectando el encabezado antiguo de la cola,de forma que el primer elemento de la cola se convierte en la nueva cabecera.

En la figura siguiente puede verse esquematicamente el resultado de hacer consecutivamente las siguientes operaciones:

C=CREAR(C);

PONER_EN_COLA(x,C);PONER_EN_COLA(y,C);

QUITAR_DE_COLA(C);

DESTRUIR(C);

(32)

Manual del Alumno

Se puede observar que en el primer caso, la memoria que se obtiene del sistema es la de la estructura de tipo celda que hace de cabecera y la memoria para ubicar los dos punteros anterior y posterior. En los dos últimos casos, la línea punteada indica la memoria que es liberada.

IMPLEMENTACIÓN DE LAS COLAS USANDO MATRICES CIRCULARES.

La implementación matrical de las listas no es muy eficiente para las colas, puesto que si bien con el uso de un apuntador al último elemento es posible ejecutar PONER_EN_COLA en un tiempo constante, QUITAR_DE_COLA, que suprime le primer elemento, requiere que la cola completa ascienda una posición en la matriz con lo que tiene un orden de eficiencia lineal proporcional al tamaño de la cola. Para evitarlo se puede adoptar un criterio diferente.

Imaginemos a la matriz como un circulo en el que la primera posición sigue a la última, en la forma en la que se ve en la figura siguiente. La cola se encuentra en alguna parte de ese círculo ocupando posiciones consecutivas. Para insertar un elemento en la cola se mueve el apuntador post una posición en el sentido de las agujas del reloj y se escribe el elemento en esa posición. Para suprimir un elemento simplemente se mueve ant una posición en el sentido de las agujas del reloj. De esta forma, la cola se mueve en ese sentido conforme se insertan y suprimen elementos. Obsérvese que utilizando este modelo los procedimientos

PONER_EN_COLA y QUITAR_DE_COLA se pueden implementar de manera que su ejecución se realice en tiempo constante.

(33)

Manual del Alumno

Existe un probelma que aparece en la representación de la figura anterior y en cualquier variación menor de esta estrategia (p.e. que post apunte a la última posición en el sentido de las agujas del reloj). El problema es que no hay forma de distinguir una cola vacia de una que llene el círculo completo salvo que mantengamos un bit que sea verdad si y solo si la cola está vacia. Si no deseamos mantener este bit debemos prevenir que la cola llene alguna vez la matriz. Para ver por qué puede pasar esto, supongamos que la cola de la figura anterior tuviera MAX_LONG elementos. Entonces, post apuntaría a la posición anterior en el sentido de las agujas del reloj de ant. ¿Qué pasaria si la cola estuviese vacia?. Para ver como se representa una cola vacia, consideramos primero una cola de un elemento. Entonces post y ant apuntarian a la misma posición. Si extraemos un elemento, ant se mueve una posición en el sentido de las agujas del reloj, formando una cola vacia. Por tanto una cola vacia tiene post a una posición de ant en el sentido de las agujas del reloj, que es exactamente la misma posición relativa que cuando la cola tenia MAX_LONG elementos. Por tanto vemos que aún cuando la matriz tenga MAX_LONG casillas, no podemos hacer crecer la cola más allá de MAX_LONG-1 casillas, a menos que introduzcamos un mecanismo para distinguir si la cola está vacía o llena.

Ahora escribimos las primitivas de las colas usando esta representación para una cola:

typedef struct {

tElemento *elementos;

int Lmax;

int ant,post;

} tipocola;

typedef tipocola *cola;

cola CREAR (int tamanoMax) {

cola C;

C = (cola) malloc(sizeof(tipocola));

if (C == NULL)

error("No hay memoria.");

C->Lmax = tamanoMax+1;

C->ant = 0;

C->post = C->Lmax-1;

C->elementos = (tElemento *) calloc((tamanoMax+1), sizeof(tElemento));

(34)

Manual del Alumno

if (C->elementos == NULL) error("No hay memoria.");

return C;

}

void DESTRUIR (cola *C) {

free(*C->elementos);

free(*C);

*C == NULL;

}

int VACIA (cola C) {

return((C->post+1)%(C->Lmax) == C->ant) }

tElemento FRENTE (cola C) {

if (VACIA(C))

error("Cola vacia.");

return(C->elementos[C->ant]);

}

void PONER_EN_COLA (tElemento x,cola C) {

if ((C->post+2) % (C->Lmax) == C->ant) error("Cola llena.");

C->post = (C->post+1) % (C->Lmax);

C->elementos[C->post] = x;

}

(35)

Manual del Alumno

void QUITAR_DE_COLA (cola C) {

if (VACIA(C))

error("Cola vacia.");

C->ant = (C->ant+1) % (C->Lmax);

}

En esta implementación podemos observar que se reserva una posicón más que la

especificada en el parametro de la función CREAR. La razón de hacerlo es que no se podrán ocupar todos los elementos de la matriz ya que debemos distinguir la cola llena de la cola vacía. Estas dos situaciones por lo tanto vienen representadas tal y como se muestra en la figura siguiente.

Se puede observar en el caso de la cola llena en la figura como la posición siguiente a post no es usada y por lo tanto es necesario crear una matriz de un tamaño N+1 para tener una capacidad para almacenar N elementos en cola.

(36)

Manual del Alumno

LISTAS DOBLEMENTE ENLAZADAS

1. INTRODUCCIÓN.

En algunas aplicaciones podemos desear recorrer la lista hacia adelante y hacia atrás, o dado un elemento, podemos desear conocer rápidamente los elementos anterior y siguiente. En tales situaciones podríamos desear darle a cada celda sobre una lista un puntero a las celdas siguiente y anterior en la lista tal y como se muestra en la figura.

Otra ventaja de las listas doblemente enlazadas es que podemos usar un puntero a la celda que contiene el i-ésimo elemento de una lista para representar la posición i, mejor que usar el puntero a la celda anterior aunque lógicamente, también es posible la implementación similar a la expuesta en las listas simples haciendo uso de la cabecera. El único precio que pagamos por estas características es la presencia de un puntero adicional en cada celda y

consecuentemente procedimientos algo más largos para algunas de las operaciones básicas de listas. Si usamos punteros (mejor que cursores) podemos declarar celdas que consisten en un elemento y dos punteros a través de:

typedef struct celda{

tipoelemento elemento;

struct celda *siguiente,*anterior;

}tipocelda;

typedef tipocelda *posicion;

Un procedimiento para borrar un elemento en la posición p en una lista doblemente enlazada es:

void borrar (posicion p)

(37)

Manual del Alumno

{

if (p->anterior != NULL)

p->anterior->siguiente = p->siguiente;

if (p->siguiente != NULL)

p->siguiente->anterior = p->anterior;

free(p);

}

El procedimiento anterior se expresa de forma gráfica en como muestra la figura:

Donde los trazos contínuos denotan la situación inicial y los punteados la final. El ejemplo visto se ajusta a la supresión de un elemento o celda de una lista situada en medio de la misma.

Para obviar los problemas derivados de los elementos extremos (primero y último) es práctica común hacer que la cabecera de la lista doblemente enlazada sea una celda que efectivamente complete el círculo, es decir, el anterior a la celda de cabecera sea la última celda de la lista y la siguiente la primera. De esta manera no necesitamos chequear para NULL en el anterior procedimiento borrar.

Por consiguiente, podemos realizar una implementación de listas doblemente enlazadas con cabecera tal que tenga una estructura circular en el sentido de que dado un nodo y por medio de los punteros siguiente podemos volver hasta él como se puede observar en la figura (de forma analoga para anterior).

Es importante notar que aunque la estructura física de la lista puede hacer pensar que mediante la operación siguiente podemos alcanzar de nuevo un nodo de la lista, la estructura lógica es la de una lista y por lo tanto habrá una posición primero y una posición fin de forma que al aplicar una operación anterior o siguiente respectivamente sobre estas posiciones el resultado será un error.

Respecto a la forma en que trabajarán las funciones de la implementación que proponemos hay que hacer constar los siguientes puntos:

La función de creación debe alojar memoria para la cabecera y hacer que los punteros siguiente y anterior apunten a ella, devolviendo un puntero a dicha cabecera.

(38)

Manual del Alumno

La función primero(l) devolverá un puntero al nodo siguiente a la cabecera.

La función fin(l) devolvera un puntero al nodo cabecera.

Trabajar con varias posiciones simultáneamente tendrá un comportamiento idéntico al de las listas enlazadas excepto respecto al problema referente al borrado cuando se utilizan posiciones consecutivas. Es posible implementar la función de borrado de tal forma que borrar un elemento de una posición p invalida el valor de dicha posición p y no afecta a ninguna otra posición. Nosotros en nuestra implementación final optaremos por pasar un puntero a la posición para el borrado de forma que la posición usada quede apuntando al elemento siguente que se va a borrar al igual que ocurría en el caso de las listas simples. Otra posible solución puede ser que la función devuelva la posición del elemento siguiente a ser borrado.

La inserción se debe hacer a la izquierda del nodo apuntado por la posición ofrecida a la función insertar. Esto implica que al contrario que en las listas simples, al insertar un nodo, el puntero utilizado sigue apuntando al mismo elemento al que apuntaba y no al nuevo elemento insertado. Si se desea, es posible modificar la función de forma que se pase un puntero a la posición de inserción para poder modificarla y hacer que apunte al nuevo elemento insertado. En cualquier caso, el comportamiento final de la función deberá quedar reflejado en el conjunto de especificaciones del TDA.

2. OPERACIONES PRIMITIVAS DE LISTAS DOBLES.

Dentro del tipo abstracto de listas doblemente enlazadas podemos proponer las siguientes primitivas:

tLista crear ()

void destruir (tLista l) tPosicion primero (tLista l) tPosicion fin (tLista l)

void insertar (tElemento x, tPosicion p, tLista l) void borrar (tPosicion *p, tLista l)

tElemento elemento(tPosicion p, tLista l) tPosicion siguiente (tPosicion p, tLista l) tPosicion anterior (tPosicion p, tLista l) tPosicion posicion (tElemento x, tLista l)

ESPECIFICACIÓN SEMANTICA Y SINTACTICA.

tLista crear ()

Argumentos: Ninguno.

Efecto: (Constructor primitivo). Crea un objeto del tipo tLista.

void destruir (tLista l) Argumentos: Una lista.

Efecto: Destruye el objeto l liberando los recursos que empleaba. Para volver a usarlo habrá que crearlo de nuevo.

tPosicion primero (tLista l) Argumentos: Una lista.

Efecto: Devuelve la posición del primer elemento de la lista.

tPosicion fin (tLista l) Argumentos: Una lista.

Efecto: Devuelve la posición posterior al último elemento de la lista.

(39)

Manual del Alumno

void insertar (tElemento x, tPosicion p, tLista l) Argumentos:

l: Es modificada.

p: Es una posición válida para la lista l.

x: Dirección válida de un elemento del tipo T con que se instancia la lista, distinta de NULL.

Efecto: Inserta elemento x en la posición p de la lista l desplazando todos los demás elementos en una posición.

void borrar (tPosicion *p, tLista l) Argumentos:

l: Es modificada.

p: Es una posición válida para la lista l.

Efecto: Elimina el elemento de la posición p de la lista l desplazando todos los demás elementos un una posición.

tElemento elemento(tPosicion p, tLista l) Argumentos:

l: Una lista.

p: Es una posción válida de la lista l.

Efecto: Devuelve el elemento que se encuentra en la posición p de la lista l.

tPosicion siguiente (tPosicion p, tLista l) Argumentos:

l: Una lista.

p: Es una posición válida para la lista l, distinta de fin(l).

Efecto: Devuelve la posición siguiente a p en l.

tPosicion anterior (tPosicion p, tLista l) Argumentos:

l: Una lista.

p: Es una posición válida para la lista l, distinta de primero(l).

Efecto: Devuelve la posición que precede a p en l.

tPosicion posicion (tElemento x, tLista l) Argumentos:

l: Una lista.

x: Dirección válida de un elemento del tipo T con que se instancia la lista, distinta de NULL.

Efecto: Si x se encuentra entre los elementos de la lista l, devuelve la posición de su primera ocurrencia. En otro caso, devuelve la posición fin(l).

(40)

Manual del Alumno

3. EFICIENCIA.

Comparación de la eficiencia para las distintas implementaciones de las listas:

4. IMPLEMENTACIÓN DE LISTAS DOB. ENLAZADAS.

Una vez aclaradas las posibles ambigüedades y dudas que se pueden plantear, la implementación de las listas doblemente enlazadas quedaría como sigue:

typedef struct celda { tElemento elemento;

struct celda *siguiente,*anterior;

} tipocelda;

typedef tipocelda *tPosicion;

typedef tipocelda *tLista;

static void error(char *cad) {

fprintf(stderr, "ERROR: %s\n", cad);

exit(1);

}

tLista Crear() {

tLista l;

l = (tLista)malloc(sizeof(tipocelda));

if (l == NULL)

Error("Memoria insuficiente.");

l->siguiente = l->anterior = l;

return l;

}

(41)

Manual del Alumno

void Destruir (tLista l) {

tPosicion p;

for (p=l, l->anterior->siguiente=NULL; l!=NULL; p=l) { l = l->siguiente;

free(p);

} }

tPosicion Primero (tLista l) {

return l->siguiente;

}

tPosicion Fin (tLista l) {

return l;

}

void Insertar (tElemento x, tPosicion p, tLista l) {

tPosicion nuevo;

nuevo = (tPosicion)malloc(sizeof(tipocelda));

if (nuevo == NULL)

Error("Memoria insuficiente.");

nuevo->elemento = x;

nuevo->siguiente = p;

nuevo->anterior = p->anterior;

p->anterior->siguiente = nuevo;

p->anterior = nuevo;

}

void Borrar (tPosicion *p, tLista l) {

tPosicion q;

if (*p == l){

Error("Posicion fin(l)");

}

q = (*p)->siguiente;

(*p)->anterior->siguiente = q;

q->anterior = (*p)->anterior;

free(*p);

(*p) = q;

}

tElemento elemento(tPosicion p, tLista l) {

if (p == l){

Error("Posicion fin(l)");

}

return p->elemento;

}

(42)

Manual del Alumno

tPosicion siguiente (tPosicion p, tLista l) {

if (p == l){

Error("Posicion fin(l)");

}

return p->siguiente;

}

tPosicion anterior( tPosicion p, tLista l) {

if (p == l->siguiente){

Error("Posicion primero(l)");

}

return p->anterior;

}

tPosicion posicion (tElemento x, tLista l) {

tPosicion p;

int encontrado;

p = primero(l);

encontrado = 0;

while ((p != fin(l)) && (!encontrado)) if (p->elemento == x)

encontrado = 1;

else

p = p->siguiente;

return p;

}

MULTILISTAS

1. TDA FRENTE A ESTRUCTURA DE DATOS.

Tipo de Dato Abstracto (TDA): Modelo formal de un ente junto con un conjunto de operaciones definidas sobre el modelo que nos permite procesarlo.

Estructuras de Datos: Organización lógica de la información con que representamos los Datos.

(43)

Manual del Alumno

2. ENTIDADES Y RELACIONES.

Tipos de Relación:

Uno a uno (Ejemplo: Nombre <--> D.N.I.).

Uno a muchos (Ejemplo: Equipo <-->> Jugador).

Muchos a muchos (Ejemplo: Alumno <<-->> Asignatura).

Representación de relaciones muchos a muchos.

Matriz.

Listas.

Multilistas.

3. ESTRUCTURA DE DATOS MULTILISTA

Conjunto de nodos en que algunos tienen más de un puntero y pueden estar en más de una lista simultáneamente.

Para cada tipo de nodo es importante distinguir los distintos campos puntero para realizar los recorridos adecuados y evitar confusiones.

Estructura básica para Sistemas de Bases de Datos en Red.

4. IMPLEMENTACIÓN DE MULTILISTAS

Dados dos tipos de entidades, TipoA y TipoB, se necesitan:

Dos nuevos tipos correspondientes a los nodos para cada clase de entidad, que junto con la información propia de la entidad incluye los punteros necesarios para mantener la estructura.

typedef struct NodoTipoA { TipoA Info;

NodoRelacion *PrimerB;

} NodoTipoA;

typedef struct NodoTipoB{

TipoB Info;

NodoRelacion *PrimerA;

} NodoTipoB;

Una estructura para agrupar los objetos de cada tipo de entidad (Array, Lista,Árbol, Tabla Hash, ...).

Un TDA Nodo Relacion que incluye un puntero por cada lista así como información propia de la relación.

typedef struct NodoRelacion { NodoTipoA *SiguienteA;

NodoTipoB *SiguienteB;

<tipo1> campo1;

...

<tipon> campo_n;

Referencias

Documento similar

Inscripciones masivas: Tener en cuenta que el sistema le permitirá inscribir a varios alumnos del mismo grado en un solo proceso, quienes deberán presentarse el día del concurso,

ACTIVO, PASIVO y CAPITAL. Está constituido por los recursos de la empresa: el dinero, sus mercaderías para la venta, sus muebles y equipos, créditos a su

Se entiende por evaluación académica el conjunto de instrumentos que dan cuenta del grado o nivel que el estudiante logra de los objetivos del curso. Es importante

[r]

La administración de los recursos humanos es relevante en las organizaciones competitivas siguen un proceso analítico; y también, admitir personal nuevo, como la

Se entiende por evaluación académica el conjunto de instrumentos que dan cuenta del grado o nivel que el estudiante logra de los objetivos del curso. Es importante tener en cuenta

Se entiende por evaluación académica el conjunto de instrumentos que dan cuenta del grado o nivel que el estudiante logra de los objetivos del curso. Es importante

Fuente: Internet.. Para cargar los datos en la aplicación debemos tomar en cuenta el lugar donde se encontraba la información antes de implementar el sistema, sea el caso