Beruflich Dokumente
Kultur Dokumente
NORBERT WIENER
PROGRAMA: S3C
Lima-Per
2
Manual del Alumno
LISTAS
1. INTRODUCCIN.
Dado un dominio D, una lista de elementos de dicho conjunto es una sucesin finita de
elementos del mismo.En lenguaje matemtico, una lista es una aplicacin de un conjunto de la
forma {1,2, ... ,n} en un dominio D:
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 ningn elemento que ocupe la posicin n+1. Sin
embargo, conviene tener en cuenta dicha posicin, a la que se llama posicin detras de la
ltima, ya que esta posicin indicar el final de la lista. A a 1 se le llama primer elemento de la
lista y a an ltimo elemento de la lista. Si n=0 diremos que la lista est vaca y lo
representaremos como <>. Los elementos de una lista estan ordenados por su posicin. As,
se dice que ai precede a ai+1 y que ai sigue a ai-1. A continuacin 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 bsicos(es decir, al tipo que se
almacenar en la lista) tElemento. Tambin vamos a considerar el tipo posicin como
tPosicion. Esto lo haremos as, ya que no siempre las posiciones las vamos a representar por
nmeros naturales del lenguaje que se utilice. Lo nico importante de la representacin 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 posicin intermedia i, entonces la longitud pasa a ser n-1 y el elemento que
estaba en la posicin i+1 pasar a ocupar la posicin i, el de la posicin i+2 pasar a ocupar la
posicin i+1 y as sucesivamente.
Dentro del tipo abstracto de listas podemos proponer las siguientes primitivas:
tPosicion primero(tLista l)
PRE: l est inicializada.
POST: RESULTADO = (1)
{devuelve la primera posicin de la lista. Si la lista es <> coincide con fin(l)}
tPosicion fin(tLista l)
PRE: l est inicializada.
POST: RESULTADO = (n + 1)
{posicin detrs de la ltima}
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
Tambin debe ser suficiente pero no obligatoriamente mnimo. Aunque no sea
necesario incluir nuevas primitivas, puede ser conveniente aadir nuevas funciones si
existen motivos como:
Las cabeceras de las funciones pueden necesitar ser modificadas para hacer viable su
implementacin. Es el caso por ejemplo de que una funcin 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 funcin pueda convertirse en algo ineficente dado su tamao. En
muchos casos, por tanto, ser aconsejable no pasar estructuras directamente sino un
puntero a ellas, que una funcin no devuelva un valor sino que se devuelva mediante
un puntero en uno de sus parmetros,etc.
Un tipo de dato abstracto es un producto software y como tal es algo dinmico 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 diseado considerando que
debemos aadir todas y cada una de primitvas que creemos que se necesitarn, es
decir, puede ser ms conveniente retrasar la incorporacin de ciertas primitavas en
caso de que dudemos de su utilidad. Tngase en cuenta que, desde el punto de vista
del mantenimiento del software que usa el TDA, es mucho menos costosa la adicon de
nuevas primitivas que la supresion de algunas ya existentes.
EJEMPLOS DE USO.
Es importante aprender a usar las listas basndonos en estas especificaciones, aunque este
tipo no venga en el lenguaje en el que estemos trabajando y no conozcamos la implementacin
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.
{
5
Manual del Alumno
tposicion p;
telemento x;
x = elemento(p, l);
escribe(x);
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 bsico es tElemento y que existe una funcin
lgica, igual(x,y), que nos dice cuando son iguales dos elementos de este tipo. Se podra
pensar en que bastara 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 funcin igual sera en este
caso:
Con estas consideraciones, el procedimiento para eliminar las repeticiones de una lista sera
como sigue:
tposicion p, q;
q = siguiente(p ,l);
while (q != fin(l))
borrar(q, l);
else
q = siguiente(q, l);
La forma de usar esta funcin con nuestro ejemplo sobre una lista l es mediante la
llamada elimina(l,igual).
La variable l es un parmetro que se pasa por valor. Ntese que su valor no cambia a
lo largo de la funcin dado que en las especificaciones siempre se pasa este parmetro
de esta forma. La nica funcin 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 posicin siguiente cuando no se borra el
elemento que se encuentra en la posicin p ya que en el caso de que sea borrado ese
elemento habr que analizar el elemento que se encuentra en esa posicin p.
typedef struct{
tElemento elementos[LMAX];
int n;
} Lista;
FUNCIN DE ABSTRACCIN:
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 REPRESENTACIN:
VERDAD.
8
Manual del Alumno
La implementacin de la mayoria de las operaciones es prcticamente inmediata. Por ejemplo,
las mas simples son:
exit(-1);
if (*l==NULL) {
(*l)->n=-1;
return 0;
return(l->n+1);
return (p + 1);
error("Posicin no vlida.");
return (p - 1);
error("Posicin no vlida.");
return (l->elementos[p]);
Las nicas operaciones que pueden presentar un poco de dificultad son las de insertar,borrar y
posicion. La funcin posicin tiene que realizar una bsqueda lineal en un vector. En caso de
que el elemento considerado no est en el vector, esta funcin debe devolver lo mismo que
fin(l).
tPosicion q;
int encontrado;
q = encontrado = 0;
if (l->elementos[q] == x)
encontrado=1;
else q++;
};
10
Manual del Alumno
return q;
Para la operacin de insercin hay que hacer previamente un hueco donde realizar dicha
insercin. 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 posicin p (coinciden con los movimientos en sentido
contrario que se deben realizar para borrar el elemento que se encuentra en dicha posicin).
Como consecuencia de ello, habr que mover, en ambos casos, todos los elementos que
ocupen una posicin superior a la considerada para realizar la insercin o borrado. Esto tiene
como consecuencia que la eficiencia de las operaciones no es muy buena, del orden del
tamao de la lista.
tPosicion q;
error("Error p incorrecta.");
error("Lista llena");
else{
l->elementos[q+1] = l->elementos[q];
l->n++;
l->elementos[p] = x;
}
11
Manual del Alumno
error("p incorrecta.");
else {
l->n--;
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 implementacin es que las listas tienen un
tamao mximo del que no se puede pasar. Es decir, no corresponden exactamente a las
especificaciones consideradas en un principio. Por otra parte, siempre hay una porcin de
espacio reservada para los elementos de la lista, y que no se utiliza al ser el tamao de la lista,
en un momento dado menor que el tamao mximo. Esto se hace ms grave si las distintas
listas que se representen son de un tamao muy distinto.
Otro detalle importante de esta implementacin es, cmo hemos mencionado anteriormente, la
necesidad de una funcin de destruccin ya que ahora mismo la memoria que se requiere cada
vez que se hace una llamada a la funcin anula no es recuperada en ningn momento.Sera
interesante aadir una nueva funcin tal como la siguiente (Ntese que si la constante LMAX
es grande y se hace uso de un nmero alto de listas esta funcin no slo se hace interesante
sino que necesaria:
free(l);
Teniendo en cuenta los problemas que presenta la implementacin que hemos presentado
mediante vectores y considerando las posibilidades que nos brinda el lenguaje C, podemos
proponer una versin mas optimizada:
typedef struct{
tElemento *elementos;
int Lmax;
int n;
}Lista;
tLista l;
l = (tLista) malloc(sizeof(Lista));
if (l == NULL)
error("Memoria Insuficiente");
l->Lmax = tamanoMax;
l->n = -1;
if (l->elementos == NULL)
error("Memoria Insuficiente.");
free(l->elementos);
free(l);
}
13
Manual del Alumno
Donde las dems primitivas quedaran de la misma forma sustituyendo LMAX por l->LMAX
En esta nueva implementacin conseguimos resolver con exito dos cosas:
1. Tamaos variables: Ahora la primitiva anula ha sido sustituida por la primitiva crear a
la que se pasa un parmetro indicando el tamao maximo que tendra la lista.La mejora,
por lo tanto, ha sido sustancial teniendo en cuenta que el tamao mximo que es
necesario para la versin anterior debe ser superior a la ms grande de las listas que
se manejan y por consiguiente para pequeas listas habra una gran cantidad de
memoria desperdiciada.
Es importante destacar la forma en que se deben usar las funciones de un tipo de dato
abstracto (normalmente en la especificacin junto con algn ejemplo si es necesario). As
destacaremos que que en este nuevo conjunto de primitivas incluyendo crear y destruir el uso
del TDA debe ser:
Teniendo en cuenta:
1. El uso de la primitiva crear sobre una lista ya creada provocar una prdida de los
recursos de memoria ocupados por esta lista y la actualizacin de su valor a la lista
vaca.
2. El uso de la primitiva destruir a una lista no creada o a una lista que aunque se cre ha
sido destruida es errneo y provocar resultados imprevisibles.
3. Obviamente, despus de la destruccin de una lista, se podr usar de nuevo la misma
variable en la creacin, uso y destruccin de una nueva lista.
Como ejemplo mostramos una funcin que guarda en una lista los nmeros 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);
a = elemento(p,l);
if (a%2)
borrar(p,l);
else
p = siguiente(p,l);
a = elemento(p,l);
printf("Elemento: %d \n",a);
printf(" \n \n ");
a = elemento(anterior(p,l), l);
printf("Elemento: %d \n",a);
destruir(l);
Una implementacin de las listas que evita los problemas anteriormente mencionados para los
vectores, es la que est basada en el uso de punteros. Esta implementacin 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 cuestin; 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 ms facilmente las operaciones es conveniente considerar una celda inicial,
llamada de cabecera y donde no se almacena ningn elemento de la lista. De esta forma la
lista propiamente diche vendr representada por un puntero que indique la direccin 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 posicin mediante un puntero que acceda al
elemento correspondiente. Sin embargo, no se va a consider un puntero con la direccin de la
celda donde est el elemento considerado, sino la direccin de la celda donde est el elemento
anterior. Con esto se puede acceder a dicho elemento (mediante el puntero correspondiente), y
tambin ser ms til para las operaciones de insercin y borrado. La posicin del primer
elemento, vendr representada entonces por un puntero apuntando a la celda de cabecera, es
decir, idntico a la lista, l. La posicin del elemento ai se representar mediante un puntero,
indicando la celda del elemento ai-1. La posicin detrs del ltimo elemento ser un puntero
apuntando a an.
Debido a que la posicin lgica de un elemento viene determinada por la posicin fsica del
anterior puede dar lugar a un error de programacin 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 posicin del segundo (puntero p) y tercer (puntero q) elemento (ver figura).
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.
Es por ello que el uso simultneo 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 especificacin del TDA mediante el correspondiente
aviso de que el borrado de un elemento invalidad los valores de posicin del inmediatamente
posterior (por ejemplo, se puede indicar el comportamiento de los valores posicin cuando se
usan las funciones de insercin y de borrado).
En C, la definicin de tipos correspondiente a la implementacin por punteros sera:
tElemento elemento;
}celda;
FUNCIN DE ABSTRACCIN
Dado el objeto del tipo rep l={elemento, siguiente}, el objeto abstracto que representa es:
(n)
<l->siguiente->elemento, l->siguiente->siguiente->elemento, ... ,l->siguiente-> ->siguiente-
>elemento>
(n+1)
Donde r->siguiente-> ->siguiente == NULL.
INVARIANTE DE REPRESENTACIN
Todas las direcciones de los campos siguiente proceden de llamadas (tposicion)
malloc(sizeof(celda)) o son NULL.
tLista crear ()
tLista l;
l = (tLista)malloc(sizeof(celda));
if (l == NULL)
17
Manual del Alumno
error("Memoria Insuficiente.");
l->siguiente = NULL;
return l;
tPosicion p;
l = l->siguiente;
free(p);
tPosicion p;
p=l;
p = p->siguiente;
return p;
Repecto a esta funcin es importante senalar , 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. Habra que procurar no utilizarla demasiado si se usa esta implementacin.
18
Manual del Alumno
Por ejemplo, un ciclo while con una condicin p!=fin(l) se debe sustituir por:
q=fin(l);
while (p!=q)...
tPosicion q;
q = (tPosicion)malloc(sizeof(celda));
if (q == NULL)
error("Memoria Insuficiente.");
q->elemento = x;
q->siguiente = p->siguiente;
p->siguiente = q;
if (p->siguiente==NULL) {
return p->siguiente;
return l;
tPosicion p;
int encontrado;
p = primero(l);
encontrado = 0;
if (p->siguiente->elemento == x)
encontrado=1;
else p = p->siguiente;
return p;
Es importante comprobar que la funcin 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 implementacin mediante vectores. En trmino
medio hay que recorrer la mitad de la lista.
En la condicin del bucle aparece la comparacin (p->siguente != NULL). Esta es
equivalente a (p !=fin(l)), pero entonces aumentaria mucho la complejidad debido a la
poca eficiencia de la funcin fin(l). Se podra pensar en sustituir en cualquier programa,
esta condicin por la que hemos usado aqu. Pero esto lo hemos podido hacer porque
sta es una operacin primitva y se puede hacer referencia a la implementacin. En un
programa que use las listas no se debe hacer.
{
21
Manual del Alumno
if (p->siguiente == NULL) {
return p->siguiente->elemento;
tPosicion q;
if (p->siguiente == NULL)
q = p->siguiente;
p->siguiente = q->siguiente;
free(q);
Respecto a esta implementacin son vlidos los mismos comentarios que para la funcin
insercion.
4. COMPARACIN DE MTODOS.
Resulta de inters saber si es mejor usar una implementacin de listas basada celdas
enlazadas o en matrices en una circunstancia dada. Frecuentemente la contestacin depende
de las operaciones uqe queramos llevar a cabo, o de cuales son llevadas a cabo con mayor
asiduiadad. Otras veces, la decisin es en base a la longitud de la lista.
Los puntos principales a considerar son los siguentes:
PILAS
1. INTRODUCCIN.
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 aadir un objeto se
coloca encima del ultimo aadido y para quitar un objeto del montn hay que quitar antes los
que estn por encima de l.Un tipo de dato abstracto PILA incluye las siguientes operaciones.
Dentro del tipo abstracto de pila podemos proponer las siguientes primitivas:
CREAR()
DESTRUIR(P)
TOPE(P)
POP(P)
PUSH(x,P)
VACIA(P)
pila crear ()
Efecto: Devuelve un valor del tipo pila preparado para ser usado y que contiene un
valor de pila vacia.Esta operacin es la misma que la de las listas generales.
Para esta implementacion basada en matrices de pilas definimos el tipo de dato abstracto Pila
por
typedef struct {
tElemento *elementos;
int Lmax;
int tope;
} tipoPila;
FUNCIN DE ABSTRACCIN.
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 REPRESENTACIN.
Dado el objeto del tipo rep p, *p = (elemento, Lmax, tope) debe cumplir:
Las operaciones tipicas sobre las pilas estn implementadas en las siguientes funciones y
procedimientos.
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;
}
Como puede observar el lector, esta implementacin es justamente la realizada sobre las listas
mediante vectores pero simplificada de una forma considerable.
Obviamente, el que las funciones sobre pilas sean ms especificas que sobre listas implica que
en general se simplificar la implementacin (que responde a la estructura de la figura
anterior).
FUNCIN DE ABSTRACCIN.
Dado el objeto del tipo rep p, el objeto abstracto que representa es:
(n)
<(*p)->elemento, (*p)->siguiente->elemento, ... , (*p)->siguiente-> ->siguiente->elemento>.
(n+1)
con (*p)->siguiente-> ->siguiente = NULL.
INVARIANTE DE LA REPRESENTACIN.
Dado un objeto del tipo rep p, debe cumplir:
pila CREAR ()
{
pila P;
if (VACIA(P))
error("No existe tope.");
q = (*P);
(*P) = q->siguiente;
free(q);
}
q = (tipopnodo *) malloc(sizeof(tipopnodo));
if (q == NULL) {
error("No hay memoria.");
q->elemento = x;
q->siguiente = (*P);
(*P) = q;
}
4. EJEMPLO DE APLICACIN.
Editor de lneas.
#: carcter de borrado
@: carcter de cancelacin de lnea
Leer un carcter
Si el carcter no es '#' ni '@' meterlo en la pila
Si el carcter es '#' sacar de la pila
Si el carcter es '@' vacia la pila
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. INTRODUCCIN.
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 anlogas 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 terminologa tradicional para colas y listas no es la
misma. Las primitivas que vamos a considerar para las colas son las siguientes.
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)
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 vaca.
Efecto: Devuelve el valor del primer elemento de la cloa C. Se puede escribir en
funcin de las operaciones primitivas de las listas como: ELEMENTO(PRIMERO(C),C).
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 definicin de tipos es la siguiente:
typedef struct {
celda *ant,*post;
} tcola;
FUNCIN DE ABSTRACCIN.
Dado el objeto del tipo rep c, *c = (ant, post), el objeto abstracto que representa es:
INVARIANTE DE LA REPRESENTACIN.
Dado un objeto del tipo rep c, *c = (ant, post), debe cumplir:
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;
}
if (VACIA(C))
error("Cola vacia.");
aux = C->ant;
C->ant = C->ant->siguiente;
free(aux);
}
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 lnea punteada indica la memoria que es
liberada.
typedef struct {
tElemento *elementos;
int Lmax;
int ant,post;
} tipocola;
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;
}
Se puede observar en el caso de la cola llena en la figura como la posicin siguiente a post no
es usada y por lo tanto es necesario crear una matriz de un tamao N+1 para tener una
capacidad para almacenar N elementos en cola.
36
Manual del Alumno
1. INTRODUCCIN.
En algunas aplicaciones podemos desear recorrer la lista hacia adelante y hacia atrs, o dado
un elemento, podemos desear conocer rpidamente los elementos anterior y siguiente. En
tales situaciones podramos 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 posicin i, mejor que usar el
puntero a la celda anterior aunque lgicamente, tambin es posible la implementacin similar a
la expuesta en las listas simples haciendo uso de la cabecera. El nico precio que pagamos
por estas caractersticas es la presencia de un puntero adicional en cada celda y
consecuentemente procedimientos algo ms largos para algunas de las operaciones bsicas
de listas. Si usamos punteros (mejor que cursores) podemos declarar celdas que consisten en
un elemento y dos punteros a travs de:
Donde los trazos contnuos denotan la situacin inicial y los punteados la final. El ejemplo visto
se ajusta a la supresin 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 prctica
comn hacer que la cabecera de la lista doblemente enlazada sea una celda que efectivamente
complete el crculo, 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 implementacin 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 fsica de la lista puede hacer pensar que
mediante la operacin siguiente podemos alcanzar de nuevo un nodo de la lista, la estructura
lgica es la de una lista y por lo tanto habr una posicin primero y una posicin fin de forma
que al aplicar una operacin anterior o siguiente respectivamente sobre estas posiciones el
resultado ser un error.
Respecto a la forma en que trabajarn las funciones de la implementacin que proponemos
hay que hacer constar los siguientes puntos:
La funcin de creacin 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 funcin primero(l) devolver un puntero al nodo siguiente a la cabecera.
La insercin se debe hacer a la izquierda del nodo apuntado por la posicin ofrecida a
la funcin 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 funcin de forma que se
pase un puntero a la posicin de insercin para poder modificarla y hacer que apunte al
nuevo elemento insertado. En cualquier caso, el comportamiento final de la funcin
deber quedar reflejado en el conjunto de especificaciones del TDA.
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)
tLista crear ()
Argumentos: Ninguno.
Efecto: (Constructor primitivo). Crea un objeto del tipo tLista.
l: Es modificada.
p: Es una posicin vlida para la lista l.
x: Direccin vlida de un elemento del tipo T con que se instancia la
lista, distinta de NULL.
l: Es modificada.
p: Es una posicin vlida para la lista l.
l: Una lista.
p: Es una poscin vlida de la lista l.
l: Una lista.
p: Es una posicin vlida para la lista l, distinta de fin(l).
l: Una lista.
p: Es una posicin vlida para la lista l, distinta de primero(l).
l: Una lista.
x: Direccin vlida de un elemento del tipo T con que se instancia la
lista, distinta de NULL.
3. EFICIENCIA.
Comparacin de la eficiencia para las distintas implementaciones de las listas:
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;
return l->siguiente;
}
return l;
}
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;
}
if (*p == l){
Error("Posicion fin(l)");
}
q = (*p)->siguiente;
(*p)->anterior->siguiente = q;
q->anterior = (*p)->anterior;
free(*p);
(*p) = q;
}
if (p == l){
Error("Posicion fin(l)");
}
return p->elemento;
}
42
Manual del Alumno
if (p == l){
Error("Posicion fin(l)");
}
return p->siguiente;
}
if (p == l->siguiente){
Error("Posicion primero(l)");
}
return p->anterior;
}
p = primero(l);
encontrado = 0;
while ((p != fin(l)) && (!encontrado))
if (p->elemento == x)
encontrado = 1;
else
p = p->siguiente;
return p;
}
MULTILISTAS
Tipo de Dato Abstracto (TDA): Modelo formal de un ente junto con un conjunto de
operaciones definidas sobre el modelo que nos permite procesarlo.
2. ENTIDADES Y RELACIONES.
Tipos de Relacin:
Matriz.
Listas.
Multilistas.
4. IMPLEMENTACIN DE MULTILISTAS
Dados dos tipos de entidades, TipoA y TipoB, se necesitan:
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
informacin propia de la relacin.
NodoMultilista a, b, r;
b = Direccion(B);
/* Depende de como se agrupen los NodoTipoB. */
r = b.cont.b.PrimerA;
/* Mediante r se recorre el conjunto de entidades TipoA para B. */
while (r.tipo == NODO_ML) {
a = r;
do
a = a.cont.nr.SiguienteB;
while (a.tipo == NODO_ML)
Escribe(a.cont.a.Info);
r = r.cont.nr.SiguienteA;
};
};
45
Manual del Alumno
6. TDA RELACIN.
TDA Relacion: crear, aadeAlum, aadeAsig, borrarAlum, borrarAsig, aadir, borrar, existe,
escribeAsig, escribeAlum, destruir.
Definicin: Dados los TDAs Alumno y Asignatura, los objetos Relacion representan las
relaciones (matrcula, calificacin) entre un conjunto de Alumnos y un conjunto de Asignaturas.
OPERACIONES:
Efecto: (Constructor Primitivo): Crea un objeto del tipo Relacion, que representa las
matrculas de hasta un mximo de NumAlum alumnos en un mximo de NumAsig
asignaturas.Devuelve un objeto vaco, sin alumnos, asignaturas, ni vnculos entre
stos.
al: Un alumno.
r: El nmero de alumnos debe ser menor del mximo. Es modificada.
Efecto: Aade el alumno al a la relacin r, sin establecer ningn vnculo con las
asignaturas.
Efecto: Aade la asignatura as a la relacin r, sin establecer ningn vnculo con los
alumnos.
Efecto: Elimina todos los vnculos del alumno al con asignaturas en r. Despus elimina
el alumno de r.
void destruir(Relacion r)
Argumentos: r: Es modificada.
Efecto Destruye el objeto r liberando los recursos que empleaba.Para volver a usarlo
habr que crearlo de nuevo.
TABLAS HASH
1. INTRODUCCIN.
Las funciones que evitan valores duplicados son sorprendentemente dificiles de encontrar,
incluso para tablas pequeas. Por ejemplo, la famosa "paradoja del cumpleaos" asegura que
si en una reunin estn presentes 23 ms presonas, hay bastante probabilidad de que dos de
ellas hayan nacido el mismo dia del mismo mes. En otras palabras, si seleccionamos una
funcin aleatoria que aplique 23 claves a una tabla de tamao 365 la probabilidad de que dos
claves no caigan en la misma localizacin es de slo 0.4927.
En consecuencia, las aplicaciones h(k), a las que desde ahora llamaremos funciones hash,
tienen la particularidad de que podemos esperar que h( ki ) = h( kj ) para bastantes pares
distintos ( ki,kj ). El objetivo ser pues encontrar una funcin hash que provoque el menor
nmero posible de colisiones (ocurrencias de sinnimos), aunque esto es solo un aspecto del
problema, el otro ser el de disear mtodos de resolucin de colisiones cuando stas se
produzcan.
2. FUNCIONES HASH.
El primer problema que hemos de abordar es el clculo de la funcin hash que transforma
claves en localizaciones de la tabla. Ms concretamente, necesitamos una funcin que
transforme claves(normalmente enteros o cadenas de caracteres) en enteros en un rango
[0..M-1], donde M es el nmero de registros que podemos manejar con la memoria de que
dispongamos.como factores a tener en cuenta para la eleccin de la funcin h(k) estn que
minimice las colisiones y que sea relativamente rpida y fcil de calcular, aunque la situacin
ideal sera encontrar una funcin h que generara valores aleatorios uniformemente sobre el
intervalo [0..M-1]. Las dos aproximaciones que veremos estn encaminadas hacia este objetivo
y ambas estn basadas en generadores de nmeros aleatorios.
Hasing Multiplicativo.
Esta tcnica trabaja multiplicando la clave k por s misma o por una constante, usando despus
alguna porcin de los bits del producto como una localizacin de la tabla hash.
Cuando la eleccin es multiplicar k por s misma y quedarse con alguno de los bits centrales, el
mtodo se denomina el cuadrado medio. Este metodo an siendo simple y pudiendo cumplir el
criterio de que los bits elegidos para marcar la localizacin son funcin de todos los bits
originales de k, tiene como principales inconvenientes el que las claves con muchos ceros se
reflejarn en valores hash tambin con muchos ceros, y el que el tamao de la tabla est
restringido a ser una potencia de 2.
Otro mtodo multiplicativo, que evita las restricciones anteriores consiste en calcular h(k) =
Int[M * Frac(C*k)] donde M es el tamao de la tabla y 0 <= C <= 1, siendo importante elegir C
con cuidado para evitar efectos negativos como que una clave alfabtica K sea sinnima a
otras claves obtenidas permutando los caracteres de k. Knuth (ver bibliografa) prueba que un
valor recomendable es:
49
Manual del Alumno
En este caso la funcin se calcula simplemente como h(k) = k mod M usando el 0 como el
primer ndice de la tabla hash de tamao M.
3. RESOLUCIN DE COLISIONES.
Para todos los ejemplos el tamao de la tabla ser M=13 y la funcin hash h1(k) que
utilizaremos ser:
y los valores de la clave k que consideraremos son los expuestos en la siguiente tabla:
Suponiendo que k=0 no ocurre de forma natural, podemos marcar todas las localizaciones de
la tabla, inicialmente vacas, dndoles el valor 0. Finalmente y puesto que las operaciones de
bsqueda e insercin estn muy relacionadas, se presentaran algoritmos para buscar un item
insertndolo si es necesario (salvo que esta operacin provoque un desbordamiento de la
tabla) devolviendo la localizacin del item o un -1 (NULL) en caso de desbordamiento.
La manera ms simple de resolver una colisin es construir, para cada localizacin de la tabla,
una lista enlazada de registros cuyas claves caigan en esa direccin. Este mtodo se conoce
normalmente con el nombre de encadenamiento separado y obviamente la cantidad de tiempo
requerido para una bsqueda depender de la longitud de las listas y de las posiciones
relativas de las claves en ellas. Existen variantes dependiendo del mantenimiento que
50
Manual del Alumno
hagamos de las listas de sinnimos (FIFO, LIFO, por valor Clave, etc), aunque en la mayora
de los casos, y dado que las listas individuales no han de tener un tamao excesivo, se suele
optar por la alternativa ms simple, la LIFO.
En cualquier caso, si las listas se mantienen en orden esto puede verse como una
generalizacin del mtodo de bsqueda secuencial en listas. La diferencia es que en lugar de
mantener una sola lista con un solo nodo cabecera se mantienen M listas con M nodos
cabecera de forma que se reduce el nmero de comparaciones de la bsqueda secuencial en
un factor de M (en media) usando espacio extra para M punteros. Para nuestro ejemplo y con
la alternativa LIFO, la tabla quedara como se muestra en la siguiente figura:
Otra posibilidad consiste en utilizar un vector en el que se pone una clave en cada una de sus
casillas. En este caso nos encontramos con el problema de que en el caso de que se produzca
una colisin no se pueden tener ambos elementos formando parte de una lista paraesa casilla.
Para solucionar ese problema se usa lo que se llama rehashing. El rehashing consiste en que
una vez producida una colisin al insertar un elemento se utiliza una funcin adicional para
determinar cual ser la casilla que le corresponde dentro de la tabla, aesta funcin la
llamaremos funcin de rehashing,rehi(k).
Despues de llevar a cabo la insercin de las claves consideradas en nuestro ejemplo, el estado
de la tabla hash ser el que se puede observar en la tabla (C) en la que admas aparece el
nmero de intentos que han sido necesarios para insertar cada una de las claves.
Para intentar evitar el problema de las agrupaciones que acabamos de ver podramos utilizar la
siguiente funcin de rehashing:
4. BORRADOS Y REHASING.
Cuando intentamos borrar un valor k de una tabla que ha sido generada por direccionamiento
abierto, nos encontramos con un problema. Si k precede a cualquier otro valor k en una
secuencia de pruebas, no podemos eliminarlo sin ms, ya que si lo hiciramos, las pruebas
siguientes para k se encontrarian el "agujero" dejado por k por lo que podramos concluir que k
no est en la tabla, hecho que puede ser falso.Podemos comprobarlo en nuestro ejemplo en
cualquiera de las tablas. La solucin es que necesitamos mirar cada localizacin de la tabla
hash como inmersa en uno de los tres posibles estados: vacia, ocupada o borrada, de forma
que en lo que concierne a la busqueda, una celda borrada se trata exectamente igual que una
ocupada.En caso de inserciones, podemos usar la primera localizacin vacia o borrada que se
encuentre en la secuencia de pruebas para realizar la operacin. Observemos que este
problema no afecta a los borrado de las listas en el encadenamiento separado. Para la
implementacin de la idea anterior podria pensarse en la introduccin en los algortmos de un
valor etiqueta para marcar las casillas borradas, pero esto sera solo una solucin parcial ya
que quedara el problema de que si los borrados son frecuentes, las bsquedas sin
xitopodran requerir O(M) pruebas para detectar que un valor no est presente.
Cuando una tabla llega a un desbordamiento o cuando su eficiencia baja demasiado debido a
los borrados, el nico recurso es llevarla a otra tabla de un tamao ms apropiado, no
necesariamente mayor, puesto que como las localizaciones borradas no tienen que
reasignarse, la nueva tabla podra ser mayor, menor o incluso del mismo tamao que la
original. Este proceso se suele denominar rehashing y es muy simple de implementar si el arca
de la nueva tabla es distinta al de la primitiva, pero puede complicarse bastante si deseamos
hacer un rehashing en la propia tabla.
Encadenamiento separado.
54
Manual del Alumno
Aunque puede resultar engaoso comparar este mtodo con los otros dos, puesto que en este
caso puede ocurrir que >1, las frmulas paroximadas son:
Estas expresiones se aplican incluso cuando >>1, por lo que para n>>M, la longitud media de
cada lista ser , y deberia esperarse en media rastrear la mitad de la lista, antes de encontrar
un determinado elemento.
Hasing Lineal.
Las frmulas aproximadas son:
Como puede verse, este mtodo, aun siendo satisfactorio para pequeos, es muy pobre
cuando -> 1, ya que el lmite de los valores medios de BE y BF son respectivamente:
Hasing Doble.
Las frmulas son ahora:
BE=-(1/1-) * ln(1-)
BF=1/(1-)
con valores medios cuando -> 1 de M y M/2, respectivamente.
Para facilitar la comprensin de las frmulas podemos construir una tabla en la que las
evaluemos para distintos valores de :
La eleccin del mejor mtodo hash para una aplicacin particular puede no ser fcil. Los
distintos mtodos dan unas caractersticas de eficiencia similares. Generalmente, lo mejor es
usar el encadenamiento separado para reducir los tiempos de bsqueda cuando el nmero de
registros a procesar no se conoce de antemano y el hash doble para buscar claves cuyo
nmero pueda, de alguna manera, predecirse de antemano.
En comparacin con otras tcnicas de bsqueda, el hashing tiene ventajas y desventajas. En
general, para valores grandes de n (y razonables valores de ) un buen esquema de hashing
requiere normalmente menos pruebas (del orden 1.5 - 2) que cualquier otro mtodo de
bsqueda, incluyendo la bsqueda en rboles binarios. Por otra parte, en el caso peor, puede
comportarse muy mal al requerir O(n) pruebas. Tambin puede considerarse como una ventaja
el hecho de que debemos tener alguna estimacin a priori de nmero mximo de items que
vamos a colocar en la tabla aunque si no disponemos de tal estimacin siempre nos quedara
la opcin de usar el metodo de encadenamiento separado en donde el desbordamiento de la
tabla no constituye ningn problema.
Otro problema relativo es que en una tabla hash no tenemos ninguna de las ventajas que
tenemos cuando manejamos relaciones ordenadas, y as p.e. no podemos procesar los items
en la tabla secuencialmente, ni concluir tras una bsqueda sin xito nada sobre los items que
55
Manual del Alumno
tienen un valor cercano al que buscamos, pero en cualquier caso el mayor problema que tener
el hashing cerrado es el de los borrados dentro de la tabla.
TablaHash CrearTablaHash ()
{
tLista *t;
register i;
t=(tLista *)malloc(NCASILLAS*sizeof(tLista));
if (t==NULL)
error("Memoria insuficiente.");
for (i=0;i<NCASILLAS;i++)
t[i]=crear();
return t;
}
for (i=0;i<NCASILLAS;i++)
destruir(t[i]);
free(t);
}
Como fue mencionado anteriormente la funcin hash que ser usada es:
for (c=cad,valor=O;*c;c++)
56
Manual del Alumno
valor+=(int)(*c);
return(valor%NCASILLAS);
}
p=primero(t[pos]);
enc=O;
while (p!=fin(t[pos]) && !enc) {
if (strcmp(cad,elemento(p,t[pos]))==O)
enc=1;
else
p=siguiente(p,t[pos]);
}
return enc;
}
if (MiembroHash(cad,t))
return;
pos=Hash(cad);
insertar(cad,primero(t[pos]),t[pos]);
}
p=primero(t[pos]);
while (p!=fin(t[pos]) && !strcmp(cad,elemento(p,t[pos])))
p=siguiente(p,t[pos]));
if (p!=fin(t[pos]))
borrar(p,t[pos]);
}
Como se puede observar esta implementacin es bastante simple de forma que puede sufrir
bastantes mejoras. Se propone como ejercicio el realizar esta labor dotando al tipo de dato de
posibilidades como:
TablaHash CrearTablaHash ()
{
TablaHash t;
register i;
t=(TablaHash)malloc(NCASILLAS*sizeof(char *));
if (t==NULL)
error("Memoria Insuficiente.");
for (i=0;i<NCASILLAS;i++)
t[i]=VACIO;
return t;
}
for (i=O;i<NCASILLAS;i++)
if (t[i]!=VACIO && t[i]!=BORRADO)
free(t[i]);
free t;
}
La funcin hash que ser usada es igual a la que ya hemos usado para la implementacin del
Hasing Abierto. Y funciones del tipo MiembroHash, InsertarHash, BorrarHash pueden ser
programadas tal como sigue, teniendo en cuenta que en esta implementacin haremos uso de
un rehashing lineal.
return (valor%NCASILLAS);
}
{
int ini,i,aux;
ini=Hash(x);
for (i=O;i<NCASILLAS;i++) {
aux=(ini+i)%NCASILLAS;
if (t[aux]==VACIO)
return aux;
if (!strcmp(t[aux],x))
return aux;
}
return ini;
}
{
int ini,i,aux;
ini=Hash(x);
for (i=O;i<NCASILLAS;i++) {
aux=(ini+i)%NCASILLAS;
if (t[aux]==VACIO || t[aux]==BORRADO)
return aux;
if (!strcmp(t[aux],x))
return aux;
}
return ini;
}
if (t[pos]==VACIO)
return 0;
else
return(!strcomp(t[pos],cad));
}
if (!MiembroHash(cad,t)) {
pos=Localizar1(cad,t);
if (t[pos]==VACIO || t[pos]==BORRADO) {
t[pos]=(char *)malloc((strlen(cad)+1)*sizeof(char));
strcpy(t[pos],cad);
} else {
error("Tabla Llena. \n");
}
}
}
Obviamente, esta implementacin al igual que la del hasing abierto es tambin mejorable de
forma que se propone el ejercicio de disear e implementar una versin mejorada con
posibilidades similares a las enumeradas en el apartado anterior.
ARBOLES GENERALES
1. INTRODUCCIN.
Hasta ahora las estructuras de datos que hemos estudiado eran de tipo lineal, o sea,exista una
relacin de anterior y siguiente entre los elementos que la componan(cada elemento tendr
uno anterior y otro posterior , salvo los casos de primero y ltimo).Pues bien, aqu se va a
estudiar una estructuracin de los datos ms compleja: los rboles.
Este tipo de estructura es usual incluso fuera del campo de la informtica.El lector seguramente
conoce casos como los rboles gramaticales para analizar oraciones,los rboles genealgicos
,representacin de jerarquas,etc...La estructuracin en rbol de los elementos es fundamental
dentro del campo de la informtica aplicndose en una amplia variedad de problemas como
veremos ms adelante.
En principio podemos considerar la estructura de rbol de manera intuitiva como una estructura
jerrquica.Por tanto,para estructurar un conjunto de elementos ei en rbol, deberemos escoger
uno de ellos e1 al que llamaremos raz del rbol.Del resto de los elementos se selecciona un
subconjunto e2,...,ek estableciendo una relacin padre-hijo entre la raz y cada uno de dichos
60
Manual del Alumno
elementos de manera que e1 es llamado el padre de e2,de e3,...ek y cada uno de ellos es
llamado un hijo de e1.Iterativamente podemos realizar la misma operacin para cada uno de
estos elementos asignando a cada uno de ellos un nmero de 0 o ms hijos hasta que no
tengamos ms elementos que insertar.El nico elemento que no tiene padre es e1,la raz del
rbol.Por otro lado hay un conjunto de elementos que no tienen hijos aunque s padre que son
llamados hojas.Como hemos visto la relacin de paternidad es una relacin uno a muchos.
Usando esta notacin,un rbol tiene uno y slo un nodo raz y uno o ms nodos hoja.
Desde un punto de vista formal la estructura de datos rbol es un caso particular de grafo, ms
concretamente,en la teora de grafos se denota de forma similar como rbol dirigido. A pesar
de ello,la definicin formal ms usual de rbol en ciencias de la computacin es la recursiva:
El caso bsico es un rbol con un nico nodo.Lgicamente este nodo es a la vez raz y
hoja del rbol.
Para construir un nuevo rbol a partir de un nodo nr y k rboles A1 ,A2,...,Ak de races
n1,n2,...,nk con N1,N2,...,Nk elementos cada uno establecemos una relacin padre-hijo
entre nr y cada una de las races de los k rboles.El rbol resultante de N=1 + N1 + ... +
Nk nodos tiene como raz el nodo nr, los nodos n1,n2,...,nk son los hijos de nr y el
conjunto de nodos hoja est formado por la unin de los k conjuntos hojas iniciales.
Adems a cada uno de los Ai se les denota subrboles de la raz.
Podemos observar que cada uno de los identificadores representa un nodo y la relacin padre-
hijo se seala con una lnea.Los rboles normalmente se presentan en forma descendente y se
interpretan de la siguiente forma:
RECORRIDOS DE UN RBOL.
En una estructura lineal resulta trivial establecer un criterio de movimiento por la misma
para acceder a los elementos, pero en un rbol esa tarea no resulta tan simple.No
obstante, existen distintos mtodos tiles en que podemos sistemticamente recorrer
todos los nodos de un rbol.Los tres recorridos ms importantes se denominan
preorden,inorden y postorden aunque hay otros recorridos como es el recorrido por
niveles.
11. El listado por niveles es: desde i=0 hasta la altura h del rbol,listar de izquierda
a derecha los elementos de profundidad i.Como podemos observar,un nodo n1
aparece antes que n2 en el listado por niveles si la profundidad de n1 es menor
que la profundidad de n2 usando el orden de los nodos definido anteriormente
para el caso en que tengan la misma profundidad.
Para que un rbol represente una expresin,hay que tener en cuenta que:
En los rboles de expresin,la sucesin del preorden de etiquetas nos da lo que se conoce
como la forma prefijo de una expresin, en la que el operador precede a su operando izquierdo
y su operando derecho.En el ejemplo de la figura 5,el preorden de etiquetas del rbol es *-xy/zt
.
Anlogamente,la sucesin postorden de las etiquetas de un rbol expresin nos da lo que se
conoce como la representacin postfijo de una expresin.As en el ejemplo,la expresin postfijo
del rbol es xy-zt/*.
Finalmente,el inorden de una expresin en un rbol de expresin da la expresin infijo en s
misma,pero sin parntesis.En el ejemplo,la sucesin inorden del rbol anterior es x-y*z/t.
IMPLEMENTACIN DE RBOLES.
UNA IMPLEMENTACIN MATRICIAL
Sea A un rbol en el cual los nodos se etiquetan 0,1,2,...,n-1,es decir,cada nodo contiene un
campo de informacin que contendr estos valores.La representacin ms simple de A que
soporta la operacin PADRE es una matriz lineal P en la cual el valor de P[i] es un valor o un
cursor al padre del nodo i.La raz de A puede distinguirse dndole un valor nulo o un valor a l
mismo como padre.Por ejemplo.,podemos usar un esquema de cursores donde P[i]=j si el nodo
j es el padre del nodo i,y P[i]=-1 (suponemos que NODO_NULO=-1) si el nodo i es la raz.La
definicin del tipo sera:
Esta representacin usa la propiedad de los rboles de que cada nodo tiene un nico
padre.Con esta representacin el padre de un nodo puede encontrarse en tiempo constante.Un
camino hacia arriba en el rbol puede seguirse atravesando el rbol en tiempo proporcional al
nmero de nodos en el camino.Podemos soportar tambin el operador ETIQUETA aadiendo
otra matriz L ,tal que L[i] es la etiqueta del nodo i ,o haciendo que los elementos de la matriz A
sean registros consistiendo en un entero(cursor)y una etiqueta.EJEMPLO:Vase el rbol de la
figura 7:
67
Manual del Alumno
La representacin de padre por cursores no facilita las operaciones que requieren informacin
de hijos.Dado un nodo n ,es costoso determinar los hijos de n o la altura de n.Adems,la
representacin por cursores del padre no especifica el orden de los hijos de un nodo.Por
tanto,operaciones como HIJO_IZQDA y HERMANO_DRCHA no estn bien
definidas.Podramos imponer un orden artificial,por ejemplo,numerando los hijos de cada nodo
despus de numerar el padre,y numerar los hijos en orden creciente de izquierda a derecha.
Nota:Tngase en cuenta que aunque esta implementacin no parece muy adecuada, es
posible ampliarla con la utilizacin de nuevos campos de cursores.Por ejemplo:Podemos aadir
dos matrices adicionales para almacenar para cada nodo tanto el hijo a la izquierda como el
hermano a la derecha.
IMPLEMENTACIN DE RBOLES POR LISTAS DE HIJOS
Una forma til e importante de representar rboles es formar para cada nodo una lista de sus
hijos.Las listas pueden representarse por cualquier mtodo,pero como el nmero de hijos que
cada nodo puede tener puede ser variable,las representaciones por listas enlazadas son las
ms apropiadas.La figura 8 sugiere como puede representarse el rbol del ejemplo de la figura
7:
68
Manual del Alumno
Hay una matriz de celdas de cabecera indexadas por nodos ,que suponemos numerados
0,1,2,...,n-1. Cada punto de cabecera apunta a una lista enlazada de elementos que son
nodos.Los elementos sobre una lista encabezada por cabecera[i] son los hijos de i(por ejemplo,
9 y 4 son los hijos de 8).Si desarrollamos la estructura de datos que necesitamos en trminos
de un tipo de dato abstracto tLista (de nodos) y damos una implementacin particular de
listas,puede verse como las abstracciones encajan.
Suponemos que la raz de cada rbol est almacenada explcitamente en el campo raz.El -1
en el campo raz se usa para representar el rbol nulo o vaco.La siguiente funcin muestra el
cdigo para la operacin HIJO_IZQDA:
L=T.cabecera[n];
if(PRIMERO(L)==FIN(L))
return NODO_NULO; /*No tiene hijos*/
else
return RECUPERA(PRIMERO(L),L); /*Recupera el primero(izqda)*/
69
Manual del Alumno
}
Las dems operaciones son tambin fciles de implementar utilizando la anterior estructura
para el tipo de dato y usando las primitivas del TDA Lista.
Nota:Las funciones PRIMERO,FIN y RECUPERA usadas en el ejemplo anterior pertenecen al
TDA Lista anteriormente estudiado.
IMPLEMENTACIN DE RBOLES BASADA EN CELDAS ENLAZADAS
Al igual que ocurre en los TDA estudiados (Listas,Pilas o Colas), un nodo puede ser declarado
de forma que la estructura del rbol pueda ir en aumento mediante la obtencin de memoria de
forma dinmica,haciendo una peticin de memoria adicional cada vez que se quiere crear un
nuevo nodo.
Observemos que bajo esta implementacin cada nodo de un rbol contiene 3 punteros: padre
que apunta al padre,hizqda que apunta al hijo izquierdo y herdrcha que apunta al hermano a la
derecha del nodo.Para esta implementacin de rbol vamos a presentar las funciones
primitivas de las que hablbamos al principio.Suponemos que para referenciar el nodo i la
variable puntero apuntar a ese nodo.Suponemos tambin unas variables de tipo nodo y que la
variable T de tipo rbol apunta a la raz del rbol.
nodo RaizAr(tArbol T)
70
Manual del Alumno
{
return T;
}
return raiz;
}
void Destruir(tArbol T)
{
if(T){
destruir(T->hizqda);
destruir(T->herdrcha);
free(T);
}
}
Ti->herdrcha=n->hizqda;
Ti->padre=n;
n->hizqda=Ti;
}
if(n==raizAr(T)){
error("Memoria Insuficiente.");
}
Td->herdrcha=n->herdrcha;
Td->padre=n->padre;
n->herdrcha=Td;
}
Taux=n->hizqda;
if(Taux!=ARBOL_VACIO){
n->hizqda=Taux->herdrcha;
Taux->padre=NODO_NULO;
Taux->herdrcha=NODO_NULO;
}
71
Manual del Alumno
return Taux;
}
Taux=n->herdrcha;
if(Taux!=ARBOL_VACIO){
n->herdrcha=Taux->herdrcha;
Taux->padre=NODO_NULO;
Taux->herdrcha=NODO_NULO;
}
return Taux;
}
Como vemos hemos implementado creaRaiz de manera que el rbol devuelto es un nico
nodo.Es posible construir en C un procedimiento con un nmero variable de parmetros:
1. Visitar la raz.
2. Recorrer el subrbol ms a la izquierda en preorden.
3. Recorrer el subrbol de la derecha en preorden.
Vamos a escribir dos procedimientos uno recursivo y otro no recursivo que toman un rbol y
listan las etiquetas de sus nodos en preorden.Supongamos que existen los tipos nodo y tArbol
con etiquetas del tipo tEtiqueta definidos anteriormente en la implementacin por punteros.El
siguiente procedimiento muestra un procedimiento recursivo que , dado el nodo n,lista las
etiquetas en preorden del subrbol con raz en n.
En esta funcin hemos supuesto que existe una rutina Escribir que tiene como parmetro de
entrada un valor de tipo tEtiqueta que se encarga de imprimir en la salida estndar.Por
ejemplo,si hemos realizado typedef int tEtiqueta la funcin podra ser la siguiente:
Por otro lado,en los programas C hemos usado el operador de desigualdad entre un dato de
tipo nodo y la constante ARBOL_VACIO.Para hacerlo ms independiente de la impementacin
sera conveniente programar una funcin que podramos llamar Arbol_Vacio que se aadira
como una nueva primitiva que nos devuelve si el subrbol que cuelga del nodo es un rbol
vaco.
Para el procedimiento no recursivo,usaremos una pila para encontrar el camino alrededor del
rbol.El tipo PILA es realmente pila de nodos,es decir,pila de posiciones de nodos. La idea
bsica subyacente al algoritmo es que cuando estamos en la posicin p,la pila alojar el
camino desde la raz a p,con la raz en el fondo de la pila y el nodo p a la cabeza.El programa
tiene dos modos de operar.En el primer modo desciende por el camino ms a la izquierda en el
rbol,escribiendo y apilando los nodos a lo largo del camino,hasta que encuentra una hoja.A
continuacin el programa entra en el segundo modo de operacin en el cual vuelve hacia atrs
por el camino apilado en la pila,extrayendo los nodos de la pila hasta que se encuentra un nodo
en el camino con un hermano a la derecha.Entonces el programa vuelve al primer modo de
operacin,comenzando el descenso desde el inexplorado hermano de la derecha.El programa
comienza en modo uno en la raz y termina cuando la pila est vaca.
void PreordenArbol(tArbol T)
{
pila P; /*Pila de posiciones:tElemento de la pila es el tipo nodo*/
73
Manual del Alumno
nodo m;
P=CREAR(); /*Funcion de creacion del TDA PILA*/
m=raizAr(T);
do{
if(m!=NODO_NULO){
Escribir(etiquetaAr(n,T));
PUSH(m,P);
m=hizqdaAr(m,T);
}
else if(!VACIA(P)){
m=herdrchaAr(TOPE(P),T);
POP(P);
}
}while(!VACIA(P));
INORDEN
Vamos a escribir un procedimiento recursivo para listar las etiquetas de sus nodos en inorden.
c=hizqdaAr(n,T);
if(c!=NODO_NULO){
InordenArbol(c,T);
Escribir(etiquetaAr(n,T));
for(c=herdrchaAr(c,T);c!=NODO_NULO;c=herdrchaAr(c,T))
InordenArbol(c,T);
}
else Escribir(etiquetaAr(n,T));
}
POSTORDEN
El procedimiento recursivo para listar las etiquetas de sus nodos en postorden es el siguiente:
for(c=hizqdaAr(n,T);c!=NODO_NULO;c=herdrchaAr(c,T))
74
Manual del Alumno
PostordenArbol(c,T);
Escribir(etiquetaAr(n,T));
}
LECTURA
A continuacin veremos un procedimiento que nos realizar la lectura de los nodos de un rbol
introducindolos en preorden.La funcin implementada se llama Lectura aunque se listan dos
funciones(la rutina Lectura2 es una funcin auxiliar que es usada por la primera).
if(comparar(etHijo,FINAL)){
Hijo=creaRaiz(etHijo);
insertar_hijo_izqda(n,Hijo,T);
Lectura2(hizqdaAr(n,T),T);
}
if(comparar(etHermano,FINAL)){
Hermano=creaRaiz(etHermano);
insertar_hermano_drcha(n,Hermano,T);
Lectura2(herdrchaAr(n,T),T);
}
}
tArbol Lectura()
{
tArbol T;
tEtiqueta et;
Hemos supuesto que existe una funcin Leer que tiene como parmetro de entrada un
puntero a una zona de memoria que almacena un valor de tipo tEtiqueta,y que sirve
para leer de la entrada estndar un dato de ese tipo y almacenarlo en dicha zona de
memoria.
75
Manual del Alumno
Existe una variable FINAL que contiene un valor para la etiqueta que "no es legal" para
indicar la inexistencia de un hijo a la izquierda y/o de un hermano a la derecha.
Suponemos que existe una funcin comparar que tiene como parmetros de entrada
dos variables de tipo tEtiqueta y que devuelve un valor entero distinto de 0 en caso de
que las variables sean distintas segn el criterio implementado en la funcin.
Las sentencias insertar_hijo_izqda(...);Lectura2(...);no son intercambiables,es decir,si
hubieramos programado esas sentencias en otro orden
(Lectura2(...);insertar_hijo_izqda(...);) la funcin de lectura no funcionara
correctamente.La comprobacin de que esta afirmacin es correcta se deja como
ejercicio al lector.
En la segunda sentencia if ocurre una situacin similar al punto anterior.
Se puede completar la rutina de lectura para que prescinda de la lectura de un posible
hermano a la derecha de la raz simplemente preguntndonos si n es la raz del rbol
T.
ARBOLES BINARIOS
1. INTRODUCCIN.
Un rbol binario puede definirse como un rbol que en cada nodo puede tener como mucho
grado 2,es decir,a lo ms 2 hijos.Los hijos suelen denominarse hijo a la izquierda e hijo a la
derecha,establecindose de esta forma un orden en el posicionamiento de los mismos.
Todas las definiciones bsicas que se dieron para rboles generales permanecen inalteradas
sin ms que hacer las particularizaciones correspondientes.En los rboles binarios hay que
tener en cuenta el orden izqda-drcha de los hijos.Por ejemplo:los rboles binarios a) y b) de la
figura 1(adoptamos el convenio de que los hijos a la izquierda son extrados extendindonos
hacia la izquierda y los hijos a la derecha a la derecha) son diferentes,puesto que difieren en el
nodo 5.El rbol c por convenio se supone igual al b) y no al a).
76
Manual del Alumno
Para construir el TDA Arbol Binario bastara con utilizar las primitivas de los rboles generales
pero dado la gran importancia y peculiaridades que tienen este tipo de rboles, construiremos
una serie de operaciones especficas.Consideraremos las siguientes:
raiz=(tarbolBin)malloc(sizeof(struct tipoceldaBin));
if(!raiz){
error("Memoria Insuficiente.");
}
raiz->padre=NULL;
raiz->hizqda=ti;
raiz->hdrcha=td;
raiz->etiqueta=et;
if(ti!=NULL)
td->padre=raiz;
78
Manual del Alumno
return raiz;
}
void Destruir(tArbolBin A)
{
if(A){
Destruir(A->hizqda);
Destruir(A->hdrcha);
free(A);
}
}
nodoBin Raiz(tArbolBin A)
{
return A;
}
Aaux=n->hizqda;
n->hizqda=BINARIO_VACIO;
if(Aaux)
Aaux->padre=BINARIO_VACIO;
return Aaux;
}
Aaux=n->hdrcha;
n->hdrcha=BINARIO_VACIO;
if(Aaux)
Aaux->padre=BINARIO_VACIO;
return Aaux;
}
Con las cuales podemos hacer la siguiente implementacin de los recorridos en preorden,
postorden e inorden:
if(n!=NODO_NULO){
Escribir(Etiqueta(n,A));
PreordenArbol(HizqdaB(n,A),A);
PreordenArbol(HdrchaB(n,A),A);
}
}
if(n!=NODO_NULO){
PostordenArbol(Hizquierda(n,A),A);
PostordenArbol(Hderecha(n,A),A);
Escribir(etiquetaB(n,A));
}
}
80
Manual del Alumno
void InordenArbol(nodoBin n,tArbolBin A)
{
if(n!=NODO_NULO){
InordenArbol(Hizquierda(n,A),A);
Escribir(Etiqueta(n,A));
InordenArbol(HderechaB(n,A),A);
}
}
1. INTRODUCCIN.
Un rbol binario de bsqueda(ABB) es un rbol binario con la propiedad de que todos los
elementos almacenados en el subrbol izquierdo de cualquier nodo x son menores que el
elemento almacenado en x ,y todos los elementos almacenados en el subrbol derecho de x
son mayores que el elemento almacenado en x.
Obsrvese la interesante propiedad de que si se listan los nodos del ABB en inorden nos da la
lista de nodos ordenada.Esta propiedad define un mtodo de ordenacin similar al
Quicksort,con el nodo raz jugando un papel similar al del elemento de particin del Quicksort
aunque con los ABB hay un gasto extra de memoria mayor debido a los punteros.La propiedad
de ABB hace que sea muy simple disear un procedimiento para realizar la bsqueda. Para
determinar si k est presente en el rbol la comparamos con la clave situada en la raz, r.Si
coinciden la bsqueda finaliza con xito, si k<r es evidente que k,de estar presente,ha de ser
un descendiente del hijo izquierdo de la raz,y si es mayor ser un descendiente del hijo
derecho.La funcin puede ser codificada fcilmente de la siguiente forma:
if(!t)
return FALSE
else if(t->etiqueta==x)
return TRUE;
else if(t->etiqueta>x)
return pertenece(x,t->hizqda);
else return pertenece(x,t->hdrcha);
}
if(!(*t)){
*t=(nodoABB)malloc(sizeof(struct tipoceldaABB));
if(!(*t)){
error("Memoria Insuficiente.");
}
(*t)->etiqueta=x;
(*t)->hizqda=NULL;
(*t)->hdrcha=NULL;
} else if(x<(*t)->etiqueta)
inserta(x,&((*t)->hizqda));
else
inserta(x,&((*t)->hdrcha));
}
Por ejemplo supongamos que queremos construir un ABB a partir del conjunto de enteros
{10,5,14,7,12} aplicando reiteradamente el proceso de insercin.El resultado es el que muestra
la figura 2.
Al tener tanto en un lado como en otro todos los elementos igual probabilidad se espera que los
subrboles izqdo y drcho de la raz tengan longitudes de camino medias P(i) y P(n-i-1)
respectivamente.Como es posible acceder a esos elementos desde la raz del rbol completo
es necesario agregar 1 al nmero de nodos de cada camino de forma que para todo i entre 0 y
n-1,P(n) puede calcularse obteniendo el promedio de la suma:
B-RBOLES
1. INTRODUCCIN.
Los B-rboles sugieron en 1972 creados por R.Bayer y E.McCreight.El problema original
comienza con la necesidad de mantener ndices en almacenamiento externo para acceso a
bases de datos,es decir,con el grave problema de la lentitud de estos dispositivos se pretende
aprovechar la gran capacidad de almacenamiento para mantener una cantidad de informacin
muy alta organizada de forma que el acceso a una clave sea lo ms rpido posible.
Como se ha visto anteriormente existen mtodos y estructuras de datos que permiten realizar
una bsqueda dentro de un conjunto alto de datos en un tiempo de orden O(log2n). As
tenemos el caso de los rboles binarios AVL.Por qu no usarlos para organizar a travs de
ellos el ndice de una base de datos?la respuesta aparece si estudiamos ms de cerca el
problema de acceso a memoria externa.Mientras que en memoria interna el tiempo de acceso
a n datos situados en distintas partes de la memoria es independiente de las direcciones que
estos ocupen(n*cte donde cte es el tiempo de acceso a 1 dato),en memoria externa es
85
Manual del Alumno
fundamental el acceder a datos situados en el mismo bloque para hacer que el tiempo de
ejecucin disminuya debido a que el tiempo depende fuertemente del tiempo de acceso del
dispositivo externo,si disminuimos el nmero de accesos a disco lgicamente el tiempo
resultante de ejecucin de nuestra bsqueda se ve fuertemente recortado.Por consiguiente,si
tratamos de construir una estructura de datos sobre disco es fundamental tener en cuenta que
uno de los factores determinantes en el tiempo de ejecucin es el nmero total de accesos,de
forma que aunque dicho n&uacite;mero pueda ser acotado por un orden de eficiencia es muy
importante tener en cuenta el nmero real ya que el tiempo para realizar un acceso es
suficientemente alto como para que dos algoritmos pese a tener un mismo orden,puedan tener
en un caso un tiempo real de ejecucin aceptable y en otro inadmisible.
En resumen,los rboles con mltiples hijos hacen que el mantenimiento de ndices en memoria
externa sea mucho ms eficiente y es justamente ste el motivo por el que este tipo de rboles
han sido los que tradicionalmente se han usado para el mantenimiento de ndices en sistemas
de bases de datos.Lgicamente,aunque este tipo de estructuras sean ms idneas para
mantener grandes cantidades de datos en almacenamiento externo es posible construirlas de
igual forma en memoria principal,y por consiguiente pueden ser mantenidas en memoria
(mediante el uso de punteros por ejemplo)al igual que las que hemos estudiado hasta ahora.
2. B-RBOLES.
DEFINICIN.
Los B-rboles son rboles cuyos nodos pueden tener un nmero mltiple de hijos tal como
muestra el esquema de uno de ellos en la figura 1.
Como se puede observar en la figura 1,un B-rbol se dice que es de orden m si sus nodos
pueden contener hasta un mximo de m hijos.En la literatura tambin aparece que si un rbol
es de orden m significa que el mnimo nmero de hijos que puede tener es m+1(m
claves).Nosotros no la usaremos para diferenciar el caso de un nmero mximo par e impar de
claves en un nodo.
de forma que los elementos que cuelgan del primer hijo tienen una clave con valor menor que
K1,los que cuelgan del segundo tienen una clave con valor mayor que K1 y menor que
K2,etc...Obviamente,los que cuelgan del ltimo hijo tienen una clave con valor mayor que la
ltima clave(hay que tener en cuenta que el nodo puede tener menos de m hijos y por
consiguiente menos de m-1 claves).
Todos los nodos excepto la raz tienen al menos E((m-1)/2) claves.Lgicamente para
los nodos interiores eso implica que tienen al menos E((m+1)/2) hijos.
Todas las hojas estn en el mismo nivel.
El hecho de que la raz pueda tener menos descendientes se debe a que si el crecimiento del
rbol hace que la raz se divida en dos hay que permitir dicha situacin para que los nuevos
nodos mantengan esa propiedad.En el caso de que eso ocurra en un nodo interior distinto a la
raz se soluciona propagando hacia arriba;lgicamente esta operacin no se puede realizar en
el caso de raz.
Por otro lado,con el hecho de que los nodos interiores tengan un nmero mnimo de
descendientes aseguramos que en el nivel n(nivel 1 corresponde a la raz)haya un mnimo de
n-1
2E ((m+1)/2)(el 2 es el mnimo de hijos de la raz y E((m+1)/2) el mnimo para los dems)y
teniendo en cuenta que un rbol con N claves tiene N+1 descendientes en el nivel de las
hojas,podemos establecer la siguiente desigualdad:
Resolviendo:
que nos da una cota superior del nmero de nodos a recorrer para localizar un elemento en el
rbol.
BSQUEDA EN UN B-RBOL.
Localizar una clave en un B-rbol es una operacin simple pues consiste en situarse en el nodo
raz del rbol,si la clave se encuentra ah hemos terminado y si no es as seleccionamos de
entre los hijos el que se encuentra entre dos valores de clave que son menor y mayor que la
buscada respectivamente y repetimos el proceso hasta que la encontremos.En caso de que se
llegue a una hoja y no podamos proseguir la bsqueda la clave no se encuentra en el rbol.En
definitiva,los pasos a seguir son los siguientes:
INSERCIN EN UN B-RBOL.
Para insertar una nueva clave usaremos un algoritmo que consiste en dos pasos recursivos:
En la figura 2 podemos observar el efecto de insertar una nueva clave en un nodo que est
lleno.
Podemos realizar una modificacin al algoritmo de forma que se retrase al mximo el momento
de romper un nodo en dos.Con ello podramos vernos beneficiados por dos razones
fundamentalmente:
88
Manual del Alumno
1. La razn ms importante para modificar as el algoritmo es que los nodos en el rbol
estn ms llenos con lo cual el gasto en memoria para mantener la estructura es
mucho menor.
2. Retrasamos el momento en que la raz llega a dividirse y por consiguiente retrasamos
el momento en que la altura del rbol aumenta.
La forma ms sencilla de realizar esta modificacin es que en el caso de que tengamos que
realizar esa divisin,antes de llevarla a cabo,comprobemos si los hermanos adyacentes tienen
espacio libre de forma que si alguno de ellos lo tiene se redistribuyen las claves que se
encuentran en el nodo actual ms las de ese hermano m&as la clave que los separa(que se
encuentra en el padre)ms la clave a insertar de forma que en el padre se queda la mediana y
las dems quedan distribuidas entre los dos nodos.
En la figura 3 podemos observar el efecto de insertar una nueva clave en un nodo que est
lleno pero con redistribucin.
BORRADO EN UN B-RBOL.
La idea para realizar el borrado de una clave es similar a la insercin teniendo en cuenta que
ahora,en lugar de divisiones,realizamos uniones.Existe un problema aadido,las claves a
borrar pueden aparecer en cualquier lugar del rbol y por consiguiente no coincide con el caso
de la insercin en la que siempre comenzamos desde una hoja y propagamos hacia arriba.La
solucin a esto es inmediata pues cuando borramos una clave que est en un nodo interior,lo
primero que realizamos es un intercambio de este valor con el inmediato sucesor en el rbol,es
decir,el hijo ms a la izquierda del hijo derecho de esa clave.
Las operaciones a realizar para poder llevar a cabo el borrado son por tanto:
3. PRIMITIVAS DE UN B-RBOL.
AB Crear0(int ne)
{
AB raiz;
l = lenght(nod->etiquetas[]);
for(i=0;inod->etiquetas[i];i++)
;
*pos = i;
if(*posetiquetas[*pos])
return 1;
else;
90
Manual del Alumno
return 0;
}
enc = Buscar(eti,&nod,&pos);
if (enc == 1)
return 1;
do {
if (etietiquetas[i] && nod->hijos[i]!=NULO)
enc = BuscarNodo(eti,&nod->hijos[i],&pos);
else
if ((etietiquetas[i+1]||nod->etiquetas[i+1]==-1)&&nod->hijos[i+1]!=-1)
enc = BuscarNodo(eti,&nod->hijos[i+1],&pos);
i++;
} while (i<lenght(nod->etiquetas[]) && enc==0);
return (enc);
}
RBOLES B*
1. INTRODUCCIN.
Existe un problema aadido a este tipo de rboles pues cuando la divisin de nodos se
propaga hasta la raz dividirla segn hemos visto implicara que junto a uno de sus hermanos
que estuvieran completos se crearn tres nuevos nodos llenos en dos terceras partes.Esto no
es posible pues la raz no tiene hermanos.La solucin al problema puede ser permitir que la
raz tenga un nmero superior de claves de forma que si se divide se puedan producir 3 nodos
*
cumpliendo las caractersticas de los rboles B .
Los cambios crticos entre el anterior conjunto de propiedades y el conjunto que se define para
*
un rbol B convencional estn en las reglas 2 y 6: un rbol B tiene nodos que contienen un
91
Manual del Alumno
mnimo de (2m-1)/3 llaves. Por supuesto, esta nueva propiedad afecta los procedimientos de
eliminacin y redistribucin.
*
Para realizar los procedimientos de rboles B tambin se debe abordar la cuestin de dividir la
raz, la cual, por definicin, nunca tiene hermanos. Si no existen hermanos, no es posible la
divisin de dos a tres. Knuth sugiere permitir que la raz crezca hasta un tamao mayor que los
dems nodos, de tal forma que, cuando se divida, pueda producir dos nodos cada uno lleno
casi a las dos terceras partes. Esta sugerencia tiene la ventaja de asegurar que todos los
*
nodos por debajo del nivel de la raz se adhieren a las caractersticas de los rboles B . Sin
embargo, tiene la desventaja de requerir que los procedimientos sean capaces de manejar un
nodo que sea de mayor tamao que todos los dems. Otra solucin es realizar la divisin de la
raz como una divisin convencional de uno a dos. Esta segunda solucin evita cualquier lgica
especial de manejo de nodos. Por otro lado, complica la eliminacin, la redistribucin y otros
procedimientos que deben ser sensibles al nmero mnimo de llaves permitidas en un nodo.
Tales procedimientos tendran que ser capaces de reconocer que los nodos descendientes de
la raz legalmente pueden estar solo llenos.
RBOLES B+
1. INTRODUCCIN
Los rboles B+ constituyen otra mejora sobre los rboles B,pues conservan la propiedad de
acceso aleatorio rpido y permiten adems un recorrido secuencial rpido.En un rbol B+ todas
las claves se encuentran en hojas,duplicndose en la raz y nodos interiores aquellas que
resulten necesarias para definir los caminos de bsqueda.Para facilitar el recorrido secuencial
rpido las hojas se pueden vincular,obtenindose ,de esta forma,una trayectoria secuencial
para recorrer las claves del rbol.
Su principal caracterstica es que todas las claves se encuentran en las hojas.Los rboles B+
ocupan algo ms de espacio que los rboles B,pues existe duplicidad en algunas claves.En los
rboles B+ las claves de las pginas raz e interiores se utilizan nicamente como ndices.
2. BUSQUEDA EN UN RBOL B+
92
Manual del Alumno
En este caso,la bsqueda no debe detenerse cuando se encuentre la clave en la pgina raz o
en una pgina interior,si no que debe proseguir en la pgina apuntada por la rama derecha de
dicha clave.
3. INSERCIN EN UN RBOL B+
Su diferencia con el proceso de insercin en rboles B consiste en que cuando se inserta una
nueva clave en una pgina llena,sta se divide tambin en otras dos,pero ahora la primera
contendr con m/2 claves y la segunda 1+m/2, y lo que subir a la pgina antecesora ser una
copia de la clave central.
4. BORRADO EN UN RBOL B+
Arboles AVL.
1. MOTIVACIN.
Comencemos con un ejemplo: Supongamos que deseamos construir un ABB para la siguiente
tabla de datos:
Diremos que un rbol binario est equilibrado (en el sentido de Addelson-Velskii y Landis) si,
para cada uno de sus nodos ocurre que las alturas de sus dos subrboles difieren como mucho
en 1. Los rboles que cumplen esta condicin son denominados a menudo rboles AVL.
A travs de los rboles AVL llegaremos a un procedimiento de bsqueda anlogo al de los ABB
pero con la ventaja de garantizaremos un caso peor de O(log2 n), manteniendo el rbol en todo
momento equilibrado. Para llegar a este resultado , podramos preguntarnos cual podra ser el
peor AVL que podramos construir con n nodos, o dicho de otra forma cuanto podramos
permitir que un rbol binario se desequilibrara manteniendo la propiedad de AVL. Para
responder a la pregunta podemos construir para una altura h el AVL Th, con mnimo nmero de
nodos. Cada uno de estos rboles mnimos debe constar de una raiz, un subrbol AVL minimo
de altura h-1 y otro subrbol AVL tambin minimo de altura h-2. Los primeros Ti pueden verse
en la siguiente figura:
95
Manual del Alumno
Es fcil ver que el nmero de nodos n(T h) est dado por la relacin de recurencia [1]:
Relacin similar a la que aparece en los nmeros de Fibonacci (F n = Fn-1 + Fn-2) , de forma que
la ss, de valores para n(Th) est relacionada con los valores de la ss. de Fibonacci:
es decir [2],
n(Th) = Fh+2 - 1
o dicho de otra forma, la longitud de los caminos de bsqueda (o la altura) para un AVL de n
nodos, nunca excede al 44% de la longitud de los caminos (o la altura) de un rbol
completamente equilibrado con esos n nodos. En consecuencia, an en el peor de los casos
llevara un tiempo O(log2 n) al encontrar un nodo con una clave dada.
Parece, pues, que el nico problema es el mantener siempre tras cada insercin la condicin
de equilibrio, pero esto puede hacerse muy fcilmente sin ms que hacer algunos reajustes
locales, cambiando punteros.
Antes de estudiar mas detalladamente este tipo de rboles realizamos la declaracin de tipos
siguiente:
arbolAVL Crear_AVL()
{
return AVL_VACIO;
}
if (e == A->elemento)
return 1;
else
if (e < A->elemento)
return miembro_AVL(e,A->izqda);
else
return miembro_AVL(e,A->drcha);
}
Veamos ahora la forma en que puede afectar una insercin en un rbol AVL y la forma en que
deberiamos reorganizar los nodos de manera que siga equilibrado. Consideremos el esquema
general de la siguiente figura, supongamos que la insercin ha provocado que el subrbol que
cuelga de Ai pasa a tener una altura 2 unidades mayor que el subrbol que cuelga de Ad .
Qu operaciones son necesarias para que el nodo r tenga 2 subrboles que cumplan la
propiedad de rboles AVL?.
97
Manual del Alumno
Para responder a esto estudiaremos dos situaciones distintas que requieren 2 secuencias de
operaciones distintas:
p = (*A)->izqda;
(*A)->izqda = p->drcha;
p->drcha = (*A);
(*A) = p;
/* Ajustamos las alturas */
p = (*A)->drcha;
p->altura = maximo(altura(p->izqda),altura(p->drcha))+1;
(*A)->altura = maximo(a1tura((*T)->izqda),altura((*T)->drcha))+1;
}
p = (*A)->drcha;
(*A)->drcha = p->izqda;
p->izqda = (*A);
(*A) = p;
/*Ajustamos las alturas */
p = (*A)->izqda;
p->altura = maximo(altura(p->izqda),altura(p->drcha))+1;
99
Manual del Alumno
(*A)->altura = maximo(altura((*A)->izqda),altura((*A)->drcha))+1;
}
switch (altura((*A)->izqda)-altura((*A)->drcha)) {
case 2:
if (altura((*A)->izqda->izqda) > altura((*A)->izqda->drcha))
simple_derecha(A);
else doble_izquierda_derecha(A);
break;
case -2:
if (altura((*A)->drcha->drcha) > altura((*A)->drcha->izqda))
simple_izquierda(A);
else doble_derecha_izquierda(A);
break;
default:
(*A)->altura = maximo(altura((*A)->izqda),altura((*A)->drcha))+1;
}
}
Para la operacin de insercin se deber profundizar en el rbol hasta llegar a un nodo hoja o
un nodo con un solo hijo de forma que se aade un nuevo hijo con el elemento insertado. Una
vez aadido slo resta ajustar los nodos que existen en el camino de la raz al nodo insertado.
El cdigo es el siguiente:
p=T;
while (*p!=NULL)
if ((*p)->elemento > e)
100
Manual del Alumno
p = &((*p)->izqda);
else p = &((*p)->drcha);
(*p)=(nodo_avl *)malloc(sizeof(nodoAVL));
if (!(*p))
error("Error: Memoria insuficiente.");
(*p)->elemento = e;
(*p)->altura = 0;
(*p)->izqda = NULL;
(*p)->drcha = NULL;
ajustaAVL(e,A);
}
En el caso de la operacin de borrado es un poco ms complejo pues hay que determinar el
elemento que se usar para la llamada a la funcin de ajuste. Por lo dems es muy similar al
borrado en los rboles binarios de bsqueda. En la implementacin que sigue usaremos la
variable elem para controlar el elemento involucrado en la funcin de ajuste.
p=A;
elem=e;
while ((*p)->elemento!=e) {
elem=(*p)->elemento;
if ((*p)->elemento > e)
p=&((*p)->izqda);
else p=&((*p)->drcha);
}
1. INTRODUCCIN.
Para ejecutar el borrado (y eventual almacenamiento del valor) de la raz,no se puede quitar el
nodo sin ms ya que se desconectaria la estructura. Por otro lado si se quiere mantener la
propiedad de orden parcial y el mayor balanceo posible con las hojas en el nivel ms bajo
alojadas de izquierda a derecha lo que podra hacerse es poner provisionalmente la hoja ms a
la derecha del nivel ms bajo como raz provisional. Empujaremos entonces esta raz hacia
abajo intercambindola con el hijo de etiqueta menor hasta que no podamos hacerlo ms
(porque sea ya una hoja o porque la etiqueta sea ya menor que la de cualquiera de sus hijos).
El anterior proceso aplicado a un rbol con n nodos toma un tiempo O(log2 n) puesto que en el
rbol ningn camino tiene ms de 1+log2 n nodos y el proceso de empujar hacia abajo
intercambiando con los hijos toma un tiempo constante por nodo. En la siguiente figura
podemos observar este proceso sobre un ejemplo de borrado en un APO en el cual se elimina
el nodo a para seguidamente subir a la raz el nodo g que es empujado hacia abajo.
102
Manual del Alumno
103
Manual del Alumno
Para implementar la insercin habra que hacer unas consideraciones similares a las
anteriores. El nuevo elemento que se inserta, lo podriamos situar provisionalmente en el nivel
ms bajo tan a la izquierda como sea posible (se comienza en un nuevo nivel si el ltimo nivel
est completo). A continuacin se intercambia con su padre repitindose este proceso hasta
que se cumpla la condicin de orden parcial (bien porque ya est en la raz o porque tenga ya
una etiqueta mayor que la de su padre).
Al igual que en el borrado puede verse fcilmente que este proceso no lleva ms de O(log 2 n)
pasos. En la siguiente figura podemos ver un ejemplo de insercin en un APO.
104
Manual del Alumno
105
Manual del Alumno
El hecho de que los rboles que hemos estado considerando sean binarios, tan balanceados
como sea posible y tengan las hojas en el nivel inferior empujadas hacia la izquierda hace que
podamos usar una representacin muy usual para estos rboles llamada MONTON que,
bsicamente es un vector en el que guardamos los nodos del rbol por niveles. Si existen n
nodos, se usan las n primeras posiciones de un vector M (M[0] aloja la raz). El hijo izquierdo
del nodo en M[k], si existe, est en M[2k+1], y el hijo derecho, si existe, est en M[2k+2] ,en
consecuencia el padre de M[k] sea M[(k-1)/2], para i>0.
106
Manual del Alumno
Podemos declarar un APO de elementos de algn tipo, digamos tipoelemento, que consistira
en un vector de tipoelemento y un entero 'ltimo' indicando el ltimo elemento actual (en uso)
del vector. Asi podriamos declarar:
if (max < 1) {
error("El rbol debe tener al menos un nodo.");
}
A = (APO)malloc(sizeof(struct nodoAPO));
if (A == NULL)
error("No hay memoria suficiente.");
A->apo = (tElemento*)malloc(max*sizeof(tElemento));
if (A->apo == NULL)
error("No hay memoria suficiente.");
A->ultimo = -1;
A->maximo = max;
return A;
}
107
Manual del Alumno
void DestruirAPO (APO A)
{
free(A->apo);
free(A);
}
if (A->ultimo == A->maximo-1) {
A->ultimo++;
pos=A->ultimo;
A->apo[pos]=el;
/* Bucle para subir el elemento hasta su posicin. */
while ((pos>0) && (A->apo[pos] < A->apo[(pos-1)/2])) {
aux = A->apo[pos];
A->apo[pos] = A->apo[(pos-1)/2];
A->apo[(pos-1)/2] = aux;
pos = (pos-1)/2;
}
}
if (A->ultimo == -1) {
error("No hay elementos.");
}
minimo = A->apo[0];
A->apo[0] = A->apo[A->ultimo];
A->ultimo--;
if (A->ultimo <= 0) return minimo;
pos = 0;
acabar = 0;
while (pos <= (A->ultimo-1)/2 && !acabar) {
if (2*pos+1 == A->ultimo)
pos_min = 2*pos+1;
else if (A->apo[2*pos+1] < A->apo[2*pos+2])
pos_min = 2*pos+1;
GRAFOS
1. INTRODUCCIN.
Para facilitar el estudio de este tipo de dato,a continuacin se realizar un estudio de la teora
de grafos desde el punto de vista de las ciencias de la computacin. Considerando que dicha
teora es compleja y amplia,aqu slo se realizar una introduccin a la misma,describindose
el grafo como un tipo de dato y mostrndose los problemas tpicos y los algoritmos que
permiten solucionarlos usando un ordenador.
109
Manual del Alumno
Los grafos son estructuras de datos no lineales que tienen una naturaleza generalmente
dinmica. Su estudio podra dividirse en dos grandes bloques:
Grafos Dirigidos.
Grafos no Dirigidos(pueden ser considerados un caso particular de los anteriores).
Un ejemplo de grafo dirigido lo constituye la red de aguas de una ciudad ya que cada tubera
slo admite que el agua la recorra en un nico sentido.Por el contrario,la red de carreteras de
un pas representa en general un grafo no dirigido,puesto que una misma carretera puede ser
recorrida en ambos sentidos.No obstante,podemos dar unas definiciones generales para
ambos tipos.
A continuacin daremos definiciones de los dos tipos de grafos y de los conceptos que llevan
asociados.
Si las aristas tienen asociada una direccin(las aristas (x,y) y (y,x) no son equivalentes)
diremos que el grafo es dirigido,en otro caso ((x,y)=(y,x)) diremos que el grafo es no dirigido.
Diremos que un grafo es completo si A=VxV,o sea,si para cualquier pareja de vrtices
existe una arista que los une(en ambos sentidos si el grafo es no dirigido).El nmero de
aristas ser:
o grafos dirigidos:
o grafos no dirigidos:
110
Manual del Alumno
donde n=|V|
Tanto a las aristas como a los vrtices les puede ser asociada informacin.A esta
informacin se le llama etiqueta.Si la etiqueta que se asocia es un nmero se le llama
peso,costo o longitud.Un grafo cuyas aristas o vrtices tienen pesos asociados recibe
el nombre de grafo etiquetado o ponderado.
Se dice que dos arcos son adyacentes cuando tienen un vrtice comn que es a la vez
origen de uno y extremo del otro.
Un camino se dice simple cuando todos sus arcos son distintos y se dice elemental
cuando no utiliza un mismo vrtice dos veces.Por tanto todo camino elemental es
simple y el recproco no es cierto.
Un camino se dice Euleriano si es simple y adems contiene a todos los arcos del
grafo.
Diremos que un grafo no dirigido es bipartido si el conjunto de sus vrtices puede ser
dividido en dos subconjuntos(disjuntos) de tal forma que cualquiera de las aristas que
componen el grafo tiene cada uno de sus extremos en un subconjunto distinto.Un grafo
no dirigido ser bipartido si y slo si no contiene ciclos con un nmero de aristas par.
' '
Dado un grafo G=(V,A),diremos que G =(V,A ) con es un grafo parcial de G y
' ' ' '
un subgrafo de G es todo grafo G =(V ,A ) con y donde A ser el
conjunto de todas aquellas aristas que unan en el grafo G dos vrtices que estn en
'
V . Se podran combinar ambas definiciones dando lugar a lo que llamaremos subgrafo
parcial
denota .
Para grafos no dirigidos tanto el grado de entrada como el de salida coinciden y
hablamos entonces de grado y lo notamos por .
A todo grafo no dirigido se puede asociar un grafo denominado dual construido de la
siguiente forma:
donde A' est construido de la siguiente forma:si e1,e2 pertenece a A son adyacentes --
> (e1,e2)pertenece a A' con e1,e2 pertenece a V'.En definitiva,para construir un grafo
dual se cambian vrtices por aristas y viceversa.
112
Manual del Alumno
Dado un grafo G,diremos que dos vrtices estn conectados si entre ambos existe un
camino que los une.
Llamaremos componente conexa a un conjunto de vrtices de un grafo tal que entre
cada par de vrtices hay al menos un camino y si se aade algn otro vrtice esta
concicin deja de verificarse.Matemticamente se puede ver como que la conexin es
una relacin de equivalencia que descompone a V en clases de equivalencia,cada uno
de los subgrafos a los que da lugar cada una de esas clases de equivalencia
constituira una componente conexa.Un grafo diremos que es conexo si slo existe
una componente conexa que coincide con todo el grafo. .
3. TDA GRAFO.
A la hora de disear el TDA grafo hay que tener en cuenta que hay que manejar datos
correspondientes a sus vrtices y aristas,pudiendo cada uno de ellos estar o no
etiquetados.Adems hay que proporcionar operaciones primitivas que permitan manejar el tipo
de dato sin necesidad de conocer la implementacin.As,los tipos de datos que se usarn y las
operaciones primitivas consideradas son las siguientes:
grafo.
vertice.
arista.
MATRIZ DE ADYACENCIA.
Grafos dirigidos.
G=(V,A) un grafo dirigido con |V|=n .Se define la matriz de adyacencia o booleana asociada a
G como Bnxn con
Como se ve,se asocia cada fila y cada columna a un vrtice y los elementos bi,j de la matriz son
1 si existe el arco (i,j) y 0 en caso contrario.
Grafos no dirigidos.
G=(V,A) un grafo no dirigido con |V|=n .Se define la matriz de adyacencia o booleana asociada
a G como Bnxn con:
Si el grafo es etiquetado,entonces tanto bi,j como bi,j representan al coste o valor asociado al
arco (i,j) y se suelen denominar matrices de coste. Si el arco (i,j) no pertenece a A entonces se
asigna bi,j o bi,j un valor que no puede ser utilizado como una etiqueta valida.
La principal ventaja de la matriz de adyacencia es que el orden de eficiencia de las operaciones
de obtencion de etiqueta de un arco o ver si dos vertices estan conectados son independientes
del nmero de vrtices y de arcos. Por el contrario, existen dos grandes inconvenientes:
Para evitar estos inconvenientes se introduce otra representacin: las listas de adyacencia.
LISTAS DE ADYACENCIA.
En esta estructura de datos la idea es asociar a cada vertice i del grafo una lista que contenga
todos aquellos vrtices j que sean adyacentes a l. De esta forma sllo reservar memoria para
los arcos adyacentes a i y no para todos los posibles arcos que pudieran tener como origen i.
El grafo, por tanto, se representa por medio de un vector de n componentes (si |V|=n) donde
cada componente va a ser una lista de adyacencia correspondiente a cada uno de los vertices
del grafo. Cada elemento de la lista consta de un campo indicando el vrtice adyacente. En
caso de que el grafo sea etiquetado, habr que aadir un segundo campo para mostrar el valor
de la etiqueta.
115
Manual del Alumno
Como puede verse en el ejemplo de las figuras anteriores tanto el vector de listas de
adyacencias como en la lista de listas se ha razonado en funcin de los vrtices que actan
como origenes de los arcos. Anlogamente se poda haber hecho con lod vertices destino, y
combinando ambas representaciones podra pensarse en utilizar dos vectores de listas de
adyacencia o dos listas de listas de adyacencia.
REPRESENTACION PROPUESTA.
La eleccin de una estructura idnea para representar el TDA grafo no es una tarea fcil ya
que existen dos representaciones totalmente contrapuestas: por un lado tenemos la matriz de
adyacencias que es muy eficiente para comprobar si existe una arista uniendo dos vertices
peero que sin embargo desperdicia una gran cantidad de espacio si el grafo no es completo o
esta lejos de serlo, adems no tiene la posibilidad de aadir nuevos vrtices; y por otra parte
est la lista de adyacencias que no tiene el problema de la anterior respecto al espacio pero
que sin embargo no es tan eficiente a la hora de ver si existe una arista entre dos nodos
determinados.
Teniendo en cuenta estas consideraciones se ha optado por realizar una mezcla de ambas
representaciones intentando aprovechar de alguna forma las ventajas que ambas poseen. Por
otra parte siguiendo con la idea de tratar tanto los grafos dirigidos como los no dirigidos bajo
una misma estructura, la estructura elegida posee dos apariencias ligeramente diferentes para
tratar de forma adecuada cada uno de estos dos tipos de grafos.
La estructura consiste (en el caso de que tengamos un grafo dirigido en una lista de vrtices
donde cada uno de estos posee dos listas, una de aristas incidentes a l y otra de adyacentes.
Cada vez que se aade una arista al grafo se inserta en la lista de aristas adyacentes del
vertice origen y en la de incidentes del vrtice destino. De esta forma la estructura desplegada
se asemejara a una matriz de adyacencia en la cual hay una arista por cada 1 y el ndice de la
matriz es la posicin dentro de la lista de vertices.
117
Manual del Alumno
Graficamente la estructura para un grafo dirigido queda como se puede apreciar en la siguiente
figura.El puntero que de la estructura arco que apunta al destino se ha sustituido por la etiqueta
del nodo destino en el grafico para simplificarlo y hacerlo mas claro.
Esta estructura no seria la mas idonea si trabajamos con solo con grafos no dirigidos ya que
por cada arista no dirigida tendriamos que insertar en la estructura una misma arista dirigida
repetida dos veces (una con un vrtice como origen y el otro como destino y al contrario). En
muchos problemas si asumimos el desperdicio de espacio podria , de todas formas, resultar
interesante representar un grafo no dirigido como un grafo dirigido simetrico, el problema se
preesenta cuando al tener dos aristas dirigidas esto supone la presencia de un ciclo en el grafo
que realmente no existe.
118
Manual del Alumno
Teniendo en cuenta el razonamiento anterior, en el caso de que queramos manejar grafos no
dirigido la estructura consistiria en tener una lista de adyacencia para cada uno de los vertices
pero tratando aquellas aristas que aparecen en la lista de adyacencia de dos vertices distintos
y que unen ambos vrtices como una nica arista lgica (a estas dos aristas que forman una
misma arista lgica las llamaremos aristas gemelas).
Estructuras Internas.
Esta representacion tiene tres estructuras diferenciadas:
En realidad se usa la misma estructura que para los nodos pero poniendo los campos
etiq, ady y sig a NULL. Los dos campos restantes contienen:
o nodo: Contien el nmero de nodos del grafo.
o sig: Es un puntero que apunta al vrtice que ocupa la primera posicion dentro
de la lista de vertices.
#include
#include
#include
#define TE 5
#define Nulo NULL
LISTA DE PRIMITIVAS.
Lista de primitivas para los grafos dirigidos:
tgrafo Crear(void)
{
tnodo aux;
a=o->ady;
while (a!=NULL) {
if ((a->origen==o) && (a->destino==d))
return 1;
121
Manual del Alumno
else
a = a->sig;
}
return 0;
}
tnodo PrimerNodo(tgrafo g)
{
return(g->sig);
}
n=PrimerNodo(g);
while (n!=Nulo) {
a=PrimerArco(n,g);
while (a!=Nulo) {
printf(\"%s -> %s \",a->origen->etiq,a->destino->etiq);
printf(\" (%f)\\n\",a->valor);
a=SiguienteArco(n,a,g);
}
n=SiguienteNodo(n,g);
}
}
int NumeroNodos(tgrafo g)
{
return(g->nodo);
}
int GrafoVacio(tgrafo g)
{
return(g->sig == NULL);
}
a=o->ady;
while (a!=NULL) {
if ((a->origen == o) && (a->destino == d))
return (a->valor);
else
a = a->sig;
}
return 0;
}
aux_inv->origen = org;
aux_inv->destino = dest;
aux_inv-> valor= valor;
aux_inv-> sig= dest->inc;
des_inc-> = aux_inv;
}
}
if (org->ady==NULL) return;
else if (org->ady->destino==dest) {
a = org->ady;
org->ady = a->sig;
free(a);
}
else {
ant = org->ady;
a = ant->sig;
while (!enc && (a!=NULL)) {
if (a->destino==dest) enc=1;
else {
a = a->sig;
ant = ant->sig;
}
}
124
Manual del Alumno
if (a==NULL) return;
else {
ant->sig = a->sig;
free(a);
}
}
enc=0;
if (dest->inc==NULL) return;
else if (dest->inc->origen==org) {
a = dest->inc;
dest->inc = a->sig;
free(a);
}
else {
ant = dest->inc;
a = ant->sig;
while (!enc && (a!=NULL)) {
if (a->origen == org) enc=1;
else {
a = a->sig;
ant = ant->sig;
}
}
if (a==NULL) return;
else {
ant->sig = a->sig;
free(a);
}
}
}
void Destruir(tgrafo G)
{
tnodo n;
tarco a_aux;
g_nd = Crear();
for (n=PrimerNodo(g); n!=NULL; n=SiguienteNodo(n,g))
InsertarNodo(Etiqueta(n,g),g_nd);
tgrafo CopiarGrafo(tgrafo g)
{
tgrafo g_nd;
tnodo n;
tnodo org;dst;
tnodo o,d;
tarco a;
int lb;
g_nd = Crear();
for (n=PrimerNodo(g); n!=NULL; n=SiguienteNodo(n,g))
InsertarNodo(Etiqueta(n,g),g_nd);
}
126
Manual del Alumno
GRAFOS
1. INTRODUCCIN.
Para facilitar el estudio de este tipo de dato,a continuacin se realizar un estudio de la teora
de grafos desde el punto de vista de las ciencias de la computacin. Considerando que dicha
teora es compleja y amplia,aqu slo se realizar una introduccin a la misma,describindose
el grafo como un tipo de dato y mostrndose los problemas tpicos y los algoritmos que
permiten solucionarlos usando un ordenador.
Los grafos son estructuras de datos no lineales que tienen una naturaleza generalmente
dinmica. Su estudio podra dividirse en dos grandes bloques:
Grafos Dirigidos.
Grafos no Dirigidos(pueden ser considerados un caso particular de los anteriores).
Un ejemplo de grafo dirigido lo constituye la red de aguas de una ciudad ya que cada tubera
slo admite que el agua la recorra en un nico sentido.Por el contrario,la red de carreteras de
127
Manual del Alumno
un pas representa en general un grafo no dirigido,puesto que una misma carretera puede ser
recorrida en ambos sentidos.No obstante,podemos dar unas definiciones generales para
ambos tipos.
A continuacin daremos definiciones de los dos tipos de grafos y de los conceptos que llevan
asociados.
Si las aristas tienen asociada una direccin(las aristas (x,y) y (y,x) no son equivalentes)
diremos que el grafo es dirigido,en otro caso ((x,y)=(y,x)) diremos que el grafo es no dirigido.
Diremos que un grafo es completo si A=VxV,o sea,si para cualquier pareja de vrtices
existe una arista que los une(en ambos sentidos si el grafo es no dirigido).El nmero de
aristas ser:
o grafos dirigidos:
o grafos no dirigidos:
128
Manual del Alumno
donde n=|V|
Tanto a las aristas como a los vrtices les puede ser asociada informacin.A esta
informacin se le llama etiqueta.Si la etiqueta que se asocia es un nmero se le llama
peso,costo o longitud.Un grafo cuyas aristas o vrtices tienen pesos asociados recibe
el nombre de grafo etiquetado o ponderado.
Se dice que dos arcos son adyacentes cuando tienen un vrtice comn que es a la vez
origen de uno y extremo del otro.
Un camino se dice simple cuando todos sus arcos son distintos y se dice elemental
cuando no utiliza un mismo vrtice dos veces.Por tanto todo camino elemental es
simple y el recproco no es cierto.
Un camino se dice Euleriano si es simple y adems contiene a todos los arcos del
grafo.
Un circuito elemental que incluye a todos los vrtices de un grafo lo llamaremos circuito
Hamiltoniano.
' '
Dado un grafo G=(V,A),diremos que G =(V,A ) con es un grafo parcial de G y
' ' ' '
un subgrafo de G es todo grafo G =(V ,A ) con y donde A ser el
conjunto de todas aquellas aristas que unan en el grafo G dos vrtices que estn en
'
V . Se podran combinar ambas definiciones dando lugar a lo que llamaremos subgrafo
parcial
denota .
Para grafos no dirigidos tanto el grado de entrada como el de salida coinciden y
hablamos entonces de grado y lo notamos por .
A todo grafo no dirigido se puede asociar un grafo denominado dual construido de la
siguiente forma:
donde A' est construido de la siguiente forma:si e1,e2 pertenece a A son adyacentes --
> (e1,e2)pertenece a A' con e1,e2 pertenece a V'.En definitiva,para construir un grafo
dual se cambian vrtices por aristas y viceversa.
130
Manual del Alumno
Dado un grafo G,diremos que dos vrtices estn conectados si entre ambos existe un
camino que los une.
Llamaremos componente conexa a un conjunto de vrtices de un grafo tal que entre
cada par de vrtices hay al menos un camino y si se aade algn otro vrtice esta
concicin deja de verificarse.Matemticamente se puede ver como que la conexin es
una relacin de equivalencia que descompone a V en clases de equivalencia,cada uno
de los subgrafos a los que da lugar cada una de esas clases de equivalencia
constituira una componente conexa.Un grafo diremos que es conexo si slo existe
una componente conexa que coincide con todo el grafo. .
3. TDA GRAFO.
A la hora de disear el TDA grafo hay que tener en cuenta que hay que manejar datos
correspondientes a sus vrtices y aristas,pudiendo cada uno de ellos estar o no
etiquetados.Adems hay que proporcionar operaciones primitivas que permitan manejar el tipo
de dato sin necesidad de conocer la implementacin.As,los tipos de datos que se usarn y las
operaciones primitivas consideradas son las siguientes:
grafo.
vertice.
arista.
MATRIZ DE ADYACENCIA.
Grafos dirigidos.
G=(V,A) un grafo dirigido con |V|=n .Se define la matriz de adyacencia o booleana asociada a
G como Bnxn con
Como se ve,se asocia cada fila y cada columna a un vrtice y los elementos bi,j de la matriz son
1 si existe el arco (i,j) y 0 en caso contrario.
Grafos no dirigidos.
G=(V,A) un grafo no dirigido con |V|=n .Se define la matriz de adyacencia o booleana asociada
a G como Bnxn con:
Si el grafo es etiquetado,entonces tanto bi,j como bi,j representan al coste o valor asociado al
arco (i,j) y se suelen denominar matrices de coste. Si el arco (i,j) no pertenece a A entonces se
asigna bi,j o bi,j un valor que no puede ser utilizado como una etiqueta valida.
La principal ventaja de la matriz de adyacencia es que el orden de eficiencia de las operaciones
de obtencion de etiqueta de un arco o ver si dos vertices estan conectados son independientes
del nmero de vrtices y de arcos. Por el contrario, existen dos grandes inconvenientes:
Para evitar estos inconvenientes se introduce otra representacin: las listas de adyacencia.
LISTAS DE ADYACENCIA.
En esta estructura de datos la idea es asociar a cada vertice i del grafo una lista que contenga
todos aquellos vrtices j que sean adyacentes a l. De esta forma sllo reservar memoria para
los arcos adyacentes a i y no para todos los posibles arcos que pudieran tener como origen i.
El grafo, por tanto, se representa por medio de un vector de n componentes (si |V|=n) donde
cada componente va a ser una lista de adyacencia correspondiente a cada uno de los vertices
del grafo. Cada elemento de la lista consta de un campo indicando el vrtice adyacente. En
caso de que el grafo sea etiquetado, habr que aadir un segundo campo para mostrar el valor
de la etiqueta.
133
Manual del Alumno
Como puede verse en el ejemplo de las figuras anteriores tanto el vector de listas de
adyacencias como en la lista de listas se ha razonado en funcin de los vrtices que actan
como origenes de los arcos. Anlogamente se poda haber hecho con lod vertices destino, y
combinando ambas representaciones podra pensarse en utilizar dos vectores de listas de
adyacencia o dos listas de listas de adyacencia.
REPRESENTACION PROPUESTA.
La eleccin de una estructura idnea para representar el TDA grafo no es una tarea fcil ya
que existen dos representaciones totalmente contrapuestas: por un lado tenemos la matriz de
adyacencias que es muy eficiente para comprobar si existe una arista uniendo dos vertices
peero que sin embargo desperdicia una gran cantidad de espacio si el grafo no es completo o
esta lejos de serlo, adems no tiene la posibilidad de aadir nuevos vrtices; y por otra parte
est la lista de adyacencias que no tiene el problema de la anterior respecto al espacio pero
que sin embargo no es tan eficiente a la hora de ver si existe una arista entre dos nodos
determinados.
Teniendo en cuenta estas consideraciones se ha optado por realizar una mezcla de ambas
representaciones intentando aprovechar de alguna forma las ventajas que ambas poseen. Por
otra parte siguiendo con la idea de tratar tanto los grafos dirigidos como los no dirigidos bajo
una misma estructura, la estructura elegida posee dos apariencias ligeramente diferentes para
tratar de forma adecuada cada uno de estos dos tipos de grafos.
La estructura consiste (en el caso de que tengamos un grafo dirigido en una lista de vrtices
donde cada uno de estos posee dos listas, una de aristas incidentes a l y otra de adyacentes.
Cada vez que se aade una arista al grafo se inserta en la lista de aristas adyacentes del
vertice origen y en la de incidentes del vrtice destino. De esta forma la estructura desplegada
se asemejara a una matriz de adyacencia en la cual hay una arista por cada 1 y el ndice de la
matriz es la posicin dentro de la lista de vertices.
135
Manual del Alumno
Graficamente la estructura para un grafo dirigido queda como se puede apreciar en la siguiente
figura.El puntero que de la estructura arco que apunta al destino se ha sustituido por la etiqueta
del nodo destino en el grafico para simplificarlo y hacerlo mas claro.
Esta estructura no seria la mas idonea si trabajamos con solo con grafos no dirigidos ya que
por cada arista no dirigida tendriamos que insertar en la estructura una misma arista dirigida
repetida dos veces (una con un vrtice como origen y el otro como destino y al contrario). En
muchos problemas si asumimos el desperdicio de espacio podria , de todas formas, resultar
interesante representar un grafo no dirigido como un grafo dirigido simetrico, el problema se
preesenta cuando al tener dos aristas dirigidas esto supone la presencia de un ciclo en el grafo
que realmente no existe.
136
Manual del Alumno
Teniendo en cuenta el razonamiento anterior, en el caso de que queramos manejar grafos no
dirigido la estructura consistiria en tener una lista de adyacencia para cada uno de los vertices
pero tratando aquellas aristas que aparecen en la lista de adyacencia de dos vertices distintos
y que unen ambos vrtices como una nica arista lgica (a estas dos aristas que forman una
misma arista lgica las llamaremos aristas gemelas).
Estructuras Internas.
Esta representacion tiene tres estructuras diferenciadas:
En realidad se usa la misma estructura que para los nodos pero poniendo los campos
etiq, ady y sig a NULL. Los dos campos restantes contienen:
o nodo: Contien el nmero de nodos del grafo.
o sig: Es un puntero que apunta al vrtice que ocupa la primera posicion dentro
de la lista de vertices.
#include
#include
#include
#define TE 5
#define Nulo NULL
LISTA DE PRIMITIVAS.
Lista de primitivas para los grafos dirigidos:
tgrafo Crear(void)
{
tnodo aux;
a=o->ady;
while (a!=NULL) {
if ((a->origen==o) && (a->destino==d))
return 1;
139
Manual del Alumno
else
a = a->sig;
}
return 0;
}
tnodo PrimerNodo(tgrafo g)
{
return(g->sig);
}
n=PrimerNodo(g);
while (n!=Nulo) {
a=PrimerArco(n,g);
while (a!=Nulo) {
printf(\"%s -> %s \",a->origen->etiq,a->destino->etiq);
printf(\" (%f)\\n\",a->valor);
a=SiguienteArco(n,a,g);
}
n=SiguienteNodo(n,g);
}
}
int NumeroNodos(tgrafo g)
{
return(g->nodo);
}
int GrafoVacio(tgrafo g)
{
return(g->sig == NULL);
}
a=o->ady;
while (a!=NULL) {
if ((a->origen == o) && (a->destino == d))
return (a->valor);
else
a = a->sig;
}
return 0;
}
aux_inv->origen = org;
aux_inv->destino = dest;
aux_inv-> valor= valor;
aux_inv-> sig= dest->inc;
des_inc-> = aux_inv;
}
}
if (org->ady==NULL) return;
else if (org->ady->destino==dest) {
a = org->ady;
org->ady = a->sig;
free(a);
}
else {
ant = org->ady;
a = ant->sig;
while (!enc && (a!=NULL)) {
if (a->destino==dest) enc=1;
else {
a = a->sig;
ant = ant->sig;
}
}
142
Manual del Alumno
if (a==NULL) return;
else {
ant->sig = a->sig;
free(a);
}
}
enc=0;
if (dest->inc==NULL) return;
else if (dest->inc->origen==org) {
a = dest->inc;
dest->inc = a->sig;
free(a);
}
else {
ant = dest->inc;
a = ant->sig;
while (!enc && (a!=NULL)) {
if (a->origen == org) enc=1;
else {
a = a->sig;
ant = ant->sig;
}
}
if (a==NULL) return;
else {
ant->sig = a->sig;
free(a);
}
}
}
void Destruir(tgrafo G)
{
tnodo n;
tarco a_aux;
g_nd = Crear();
for (n=PrimerNodo(g); n!=NULL; n=SiguienteNodo(n,g))
InsertarNodo(Etiqueta(n,g),g_nd);
tgrafo CopiarGrafo(tgrafo g)
{
tgrafo g_nd;
tnodo n;
tnodo org;dst;
tnodo o,d;
tarco a;
int lb;
g_nd = Crear();
for (n=PrimerNodo(g); n!=NULL; n=SiguienteNodo(n,g))
InsertarNodo(Etiqueta(n,g),g_nd);
}
144
Manual del Alumno
Ejercicio No 1:
Ejercicio No 2:
Ejercicio No 3:
1. Qu nodo es la raz?
2. Cuntos caminos diferentes de longitud tres hay?
3. Es un camino la sucesin de nodos HGFBACI?
4. Qu nodos son los ancestros de K?
5. Qu nodos son los ancestros propios de N?
6. Qu nodos son los descendientes propios de M?
7. Qu nodos son las hojas?
8. Cul es la altura del nodo C?
9. Cul es la altura del rbol?
10. Cul es la profundidad del nodo C?
11. Cul es el hermano a la derecha de D?
12. Es I hermano a la derecha de F?
13. Est F a la izquierda de J?
14. Est L a la derecha de J?
15. Qu nodos estn a la izquierda y a la derecha de J?
16. Cuntos hijos tiene A?
17. Listar los nodos del rbol en preorden,postorden e inorden.
145
Manual del Alumno
Ejercicio No 4:
Considerando la funcin de listado en preorden de un rbol general que se ha presentado en la
pgina web referente a rboles generales,escribir dos funciones de escritura y lectura de un
rbol con etiquetas de tipo entero usando la misma estructura recursiva.Supngase que se
dispone de dos funciones de escritura y lectura (Escribir y Leer respectivamente)as como de
una variable FINAL tal como se indic en teora de rboles generales
Ejercicio No 1:
Supongamos que tenemos una funcin valor tal que dado un valor de tipo char (una letra del
alfabeto)devuelve un valor entero asociado a dicho identificador.Supongamos tambien la
existencia de un rbol de expresin T cuyos nodos hoja son letras del alfabeto y cuyos nodos
interiores son los caracteres *,+,-,/.Disear una funcin que tome como parmetros un nodo y
un rbol binario y devuelva el resultado entero de la evaluacin de la expresin representada.
Ejercicio No 2:
C)Disear una funcin para dar el recorrido en postorden dado el recorrido en preorden e
inorden y escribir un programa para comprobar el resultado del apartado anterior
Ejercicio No 3:
Ejercicio No 4:
Ejercicio No 5:
Ejercicio No 6:
Escribir una funcin recursiva que encuentre el nmero de nodos de un rbol binario.
Ejercicio No 7:
Ejercicio No 1:
Ejercicio No 2:
Ejercicio No 3:
Ejercicio No 4:
Ejercicio No 1:
Ejercicio No 2:
Ejercicio No 3:
1. 10,100,20,80,40,70.
2. 5,10,20,30,40,50,60.
Ejercicio No 1:
Ejercicio No 3:
Ejercicio No 1:
Ejercicio No 2:
Ejercicio No 3:
Ejercicio No 4:
Construir cada uno de los B-rboles que se van generando conforme se van insertando los
nmeros 1,9,32,3,53,43,44,57,67,7,45,34,23,12,23,56,73,65,49,85,89, 64,54,75,77,49, en un B-
rbol de orden 5.
Ejercicio No 5:
EJERCICIOS DE GRAFOS
Ejercicio No 1:
Ejercicio No 2:
148
Manual del Alumno
Construir un procedimiento que determine el nmero de componentes conexas que posee un
grafo.
Ejercicio No 3:
Ejercicio No 4:
Un grafo no dirigido se dice de Euler si existe un camino Euleriano que incluye a todas sus
aristas.Construir una funcin que dado un grafo no dirigido determine si es de Euler.
Ejercicio No 5:
Realizar un procedimiento que dado un grafo no dirigido determine cual es su grafo dual.
Ejercicio No 6:
Ejercicio No 7: