Beruflich Dokumente
Kultur Dokumente
* * * * * * * *
Definicin de rbol Formas de representacin Nomenclatura sobre rboles Declaracin de rbol binario Recorridos sobre rboles binarios Construccin de un rbol binario rbol binario de bsqueda Problemas
Definicin de rbol
Un rbol es una estructura de datos, que puede definirse de forma recursiva como: - Una estructura vaca o - Un elemento o clave de informacin (nodo) ms un nmero finito de estructuras tipo rbol, disjuntos, llamados subrboles. Si dicho nmero de estructuras es inferior o igual a 2, se tiene un rbol binario. Es, por tanto, una estructura no secuencial. Otra definicin nos da el rbol como un tipo de grafo (ver grafos): un rbol es un grafo acclico, conexo y no dirigido. Es decir, es un grafo no dirigido en el que existe exactamente un camino entre todo par de nodos. Esta definicin permite implementar un rbol y sus operaciones empleando las representaciones que se utilizan para los grafos. Sin embargo, en esta seccin no se tratar esta implementacin.
Formas de representacin
- Mediante un grafo:
En la computacin se utiliza mucho una estructura de datos, que son los rboles binarios. Estos rboles tienen 0, 1 2 descendientes como mximo. El rbol de la figura anterior es un ejemplo vlido de rbol binario.
clave del nodo por pantalla), y despus visitar el subrbol izquierdo y una vez visitado, visitar el subrbol derecho. Es un proceso recursivo por naturaleza. Si se hace el recorrido en preorden del rbol de la figura 1 las visitas seran en el orden siguiente: a,b,d,c,e,f. void preorden(tarbol *a) { if (a != NULL) { visitar(a); preorden(a->izq); preorden(a->der); } }
* Recorrido en inorden u orden central: se visita el subrbol izquierdo, el nodo actual, y despus se visita el subrbol derecho. En el ejemplo de la figura 1 las visitas seran en este orden: b,d,a,e,c,f. void inorden(tarbol *a) { if (a != NULL) { inorden(a->izq); visitar(a); inorden(a->der); } } * Recorrido en postorden: se visitan primero el subrbol izquierdo, despus el subrbol derecho, y por ltimo el nodo actual. En el ejemplo de la figura 1 el recorrido quedara as: d,b,e,f,c,a. void postorden(arbol *a) { if (a != NULL) { postorden(a->izq); postorden(a->der); visitar(a); } } La ventaja del recorrido en postorden es que permite borrar el rbol de forma consistente. Es decir, si visitar se traduce por borrar el nodo actual, al ejecutar este recorrido se borrar el rbol o subrbol que se pasa como parmetro. La razn para hacer esto es que no se debe borrar un nodo y despus sus subrboles, porque al borrarlo se pueden perder los enlaces, y aunque no se perdieran se rompe con la regla de manipular una estructura de datos inexistente. Una alternativa es utilizar una variable auxiliar, pero es innecesario aplicando este recorrido.
- Recorrido en amplitud: Consiste en ir visitando el rbol por niveles. Primero se visitan los nodos de nivel 1 (como mucho hay uno, la raz), despus los nodos de nivel 2, as hasta que ya no queden ms. Si se hace el recorrido en amplitud del rbol de la figura una visitara los nodos en este orden: a,b,c,d,e,f En este caso el recorrido no se realizar de forma recursiva sino iterativa, utilizando una cola (ver Colas) como estructura de datos auxiliar. El procedimiento consiste en encolar (si no estn vacos) los subrboles izquierdo y derecho del nodo extraido de la cola, y seguir desencolando y encolando hasta que la cola est vaca. En la codificacin que viene a continuacin no se implementan las operaciones sobre colas. void amplitud(tarbol *a) { tCola cola; /* las claves de la cola sern de tipo rbol binario */
arbol *aux; if (a != NULL) { CrearCola(cola); encolar(cola, a); while (!colavacia(cola)) { desencolar(cola, aux); visitar(aux); if (aux->izq != NULL) encolar(cola, aux->izq); if (aux->der != NULL) encolar(cola, aux->der); } } }
Por ltimo, considrese la sustitucin de la cola por una pila en el recorrido en amplitud. Qu tipo de recorrido se obtiene?
A continuacin comienza un proceso recursivo. Se procede a crear el subrbol izquierdo, cuyo tamao est limitado por los ndices izq y der. La siguiente posicin en el recorrido en preorden es la raz de este subrbol. Queda esto:
El subrbol b tiene un subrbol derecho, que no tiene ningn descendiente, tal y como indican los ndices izq y der. Se ha obtenido el subrbol izquierdo completo de la raz a, puesto que b no tiene subrbol izquierdo:
Despus seguir construyndose el subrbol derecho a partir de la raz a. La implementacin de la construccin de un rbol partiendo de los recorridos en preorden y en inorden puede consultarse aqu (en C).
Figura 5 Al definir el tipo de datos que representa la clave de un nodo dentro de un rbol binario de bsqueda es necesario que en dicho tipo se pueda establecer una relacin de orden. Por ejemplo, suponer que el tipo de datos de la clave es un puntero (da igual a lo que apunte). Si se codifica el rbol en Pascal no se puede establecer una relacin de orden para las claves, puesto que Pascal no admite determinar si un puntero es mayor o menor que otro. En el ejemplo de la figura 5 las claves son nmeros enteros. Dada la raz 4, las claves del subrbol izquierdo son menores que 4, y las claves del subrbol derecho son mayores que 4. Esto se cumple tambin para todos los subrboles. Si se hace el recorrido de este rbol en orden central se obtiene una lista de los nmeros ordenada de menor a mayor. Cuestin: Qu hay que hacer para obtener una lista de los nmeros ordenada de mayor a menor? Una ventaja fundamental de los rboles de bsqueda es que son en general mucho ms rpidos para localizar un elemento que una lista enlazada. Por tanto, son ms rpidos para insertar y borrar elementos. Si el rbol est perfectamente equilibrado -esto es, la diferencia entre el nmero de nodos del subrbol izquierdo y el nmero de nodos del subrbol derecho es a lo sumo 1, para todos los nodos- entonces el nmero de comparaciones necesarias para localizar una clave es aproximadamente de logN en el peor caso. Adems, el algoritmo de insercin en un rbol binario de bsqueda tiene la ventaja -sobre los arrays ordenados, donde se empleara bsqueda dicotmica para localizar un elemento- de que no necesita hacer una reubicacin de los elementos de la estructura para que esta siga ordenada despus de la insercin. Dicho algoritmo funciona avanzando por el rbol escogiendo la rama izquierda o derecha en funcin de la clave que se inserta y la clave del nodo actual, hasta encontrar su ubicacin; por ejemplo, insertar la clave 7 en el rbol de la figura 5 requiere avanzar por el rbol hasta llegar a la clave 8, e introducir la nueva clave en el subrbol izquierdo a 8. El algoritmo de borrado en rboles es algo ms complejo, pero ms eficiente que el de borrado en un array ordenado. Ahora bien, suponer que se tiene un rbol vaco, que admite claves de tipo entero. Suponer que se van a ir introduciendo las claves de forma ascendente. Ejemplo: 1,2,3,4,5,6 Se crea un rbol cuya raz tiene la clave 1. Se inserta la clave 2 en el subrbol derecho de 1. A continuacin se inserta la clave 3 en el subrbol derecho de 2. Continuando las inserciones se ve que el rbol degenera en una lista secuencial, reduciendo drsticamente su eficacia para localizar un elemento. De todas formas es poco probable que se de un caso de este tipo en la prctica. Si las claves a introducir llegan de forma ms o menos aleatoria entonces la implementacin de operaciones sobre un rbol binario de bsqueda que vienen a continuacin son en general suficientes. Existen variaciones sobre estos rboles, como los AVL o Red-Black (no se tratan aqu), que sin llegar a cumplir al 100% el criterio de rbol perfectamente equilibrado, evitan problemas como el de obtener una lista degenerada.
Operaciones bsicas sobre rboles binarios de bsqueda - Bsqueda Si el rbol no es de bsqueda, es necesario emplear uno de los recorridos anteriores sobre el rbol para localizarlo. El resultado es idntico al de una bsqueda secuencial. Aprovechando las propiedades del rbol de bsqueda se puede acelerar la localizacin. Simplemente hay que descender a lo largo del rbol a izquierda o derecha dependiendo del elemento que se busca. boolean buscar(tarbol *a, int elem) { if (a == NULL) return FALSE; else if (a->clave < elem) return buscar(a->der, elem); else if (a->clave > elem) return buscar(a->izq, elem); else return TRUE; }
- Insercin La insercin tampoco es complicada. Es ms, resulta practicamente idntica a la bsqueda. Cuando se llega a un rbol vaco se crea el nodo en el puntero que se pasa como parmetro por referencia, de esta manera los nuevos enlaces mantienen la coherencia. Si el elemento a insertar ya existe entonces no se hace nada. void insertar(tarbol **a, int elem) { if (*a == NULL) { *a = (arbol *) malloc(sizeof(arbol)); (*a)->clave = elem; (*a)->izq = (*a)->der = NULL; } else if ((*a)->clave < elem) insertar(&(*a)->der, elem); else if ((*a)->clave > elem) insertar(&(*a)->izq, elem); }
- Borrado La operacin de borrado si resulta ser algo ms complicada. Se recuerda que el rbol debe seguir siendo de bsqueda tras el borrado. Pueden darse tres casos, una vez encontrado el nodo a borrar: 1) El nodo no tiene descendientes. Simplemente se borra. 2) El nodo tiene al menos un descendiente por una sola rama. Se borra dicho nodo, y su primer descendiente se asigna como hijo del padre del nodo borrado. Ejemplo: en el rbol de la figura 5 se borra el nodo cuya clave es -1. El rbol resultante es:
3) El nodo tiene al menos un descendiente por cada rama. Al borrar dicho nodo es necesario mantener
la coherencia de los enlaces, adems de seguir manteniendo la estructura como un rbol binario de bsqueda. La solucin consiste en sustituir la informacin del nodo que se borra por el de una de las hojas, y borrar a continuacin dicha hoja. Puede ser cualquier hoja? No, debe ser la que contenga una de estas dos claves: la mayor de las claves menores al nodo que se borra. Suponer que se quiere borrar el nodo 4 del rbol de la figura 5. Se sustituir la clave 4 por la clave 2. la menor de las claves mayores al nodo que se borra. Suponer que se quiere borrar el nodo 4 del rbol de la figura 5. Se sustituir la clave 4 por la clave 5. El algoritmo de borrado que se implementa a continuacin realiza la sustitucin por la mayor de las claves menores, (aunque se puede escoger la otra opcin sin prdida de generalidad). Para lograr esto es necesario descender primero a la izquierda del nodo que se va a borrar, y despus avanzar siempre a la derecha hasta encontrar un nodo hoja. A continuacin se muestra grficamente el proceso de borrar el nodo de clave 4:
Codificacin: el procedimiento sustituir es el que desciende por el rbol cuando se da el caso del nodo con descencientes por ambas ramas. void borrar(tarbol **a, int elem) { void sustituir(tarbol **a, tarbol **aux); tarbol *aux; if (*a == NULL) /* no existe la clave */ return; if ((*a)->clave < elem) borrar(&(*a)->der, elem); else if ((*a)->clave > elem) borrar(&(*a)->izq, elem); else if ((*a)->clave == elem) { aux = *a; if ((*a)->izq == NULL) *a = (*a)->der; else if ((*a)->der == NULL) *a = (*a)->izq; else sustituir(&(*a)->izq, &aux); /* se sustituye por la mayor de las menores */ free(aux); } }
Ficheros relacionados
Implementacin de algunas de las operaciones sobre rboles binarios.
Ejercicio resuelto
Escribir una funcin que devuelva el numero de nodos de un rbol binario. Una solucin recursiva puede ser la siguiente: funcion nodos(arbol : tipoArbol) : devuelve entero; inicio si arbol = vacio entonces devolver 0; en otro caso devolver (1 + nodos(subarbol_izq) + nodos(subarbol_der)); fin Adaptarlo para que detecte si un rbol es perfectamente equilibrado o no.
Problemas propuestos
rboles binarios: OIE 98. (Enunciado)
rbol y se obtendr la solucin pedida en cuestin de segundos. Una posible definicin de la estructura rbol es la siguiente: typedef struct tarbol { char clave[MAXPALABRA]; int contador; /* numero de apariciones. Iniciar a 0 */ struct tarbol *izq, *der; } tarbol;
Grafos
Definicin
Un grafo es un objeto matemtico que se utiliza para representar circuitos, redes, etc. Los grafos son muy utilizados en computacin, ya que permiten resolver problemas muy complejos. Imaginemos que disponemos de una serie de ciudades y de carreteras que las unen. De cada ciudad saldrn varias carreteras, por lo que para ir de una ciudad a otra se podrn tomar diversos caminos. Cada carretera tendr un coste asociado (por ejemplo, la longitud de la misma). Gracias a la representacin por grafos podremos elegir el camino ms corto que conecta dos ciudades, determinar si es posible llegar de una ciudad a otra, si desde cualquier ciudad existe un camino que llegue a cualquier otra, etc. El estudio de grafos es una rama de la algoritmia muy importante. Estudiaremos primero sus rasgos generales y sus recorridos fundamentales, para tener una buena base que permita comprender los algoritmos que se pueden aplicar.
Glosario
Un grafo consta de vrtices (o nodos) y aristas. Los vrtices son objetos que contienen informacin y las aristas son conexiones entre vrtices. Para representarlos, se suelen utilizar puntos para los vrtices y lneas para las conexiones, aunque hay que recordar siempre que la definicin de un grafo no depende de su representacin. Un camino entre dos vrtices es una lista de vrtices en la que dos elementos sucesivos estn conectados por una arista del grafo. As, el camino AJLOE es un camino que comienza en el vrtice A y pasa por los vrtices J,L y O (en ese orden) y al final va del O al E. El grafo ser conexo si existe un camino desde cualquier nodo del grafo hasta cualquier otro. Si no es conexo constar de varias componentes conexas. Un camino simple es un camino desde un nodo a otro en el que ningn nodo se repite (no se pasa dos veces). Si el camino simple tiene como primer y ltimo elemento al mismo nodo se denomina ciclo.
Cuando el grafo no tiene ciclos tenemos un rbol (ver rboles). Varios rboles independientes forman un bosque. Un rbol de expansin de un grafo es una reduccin del grafo en el que solo entran a formar parte el nmero mnimo de aristas que forman un rbol y conectan a todos los nodos. Segn el nmero de aristas que contiene, un grafo es completo si cuenta con todas las aristas posibles (es decir, todos los nodos estn conectados con todos), disperso si tiene relativamente pocas aristas y denso si le faltan pocas para ser completo. Las aristas son la mayor parte de las veces bidireccionales, es decir, si una arista conecta dos nodos A y B se puede recorrer tanto en sentido hacia B como en sentido hacia A: estos son llamados grafos no dirigidos. Sin embargo, en ocasiones tenemos que las uniones son unidireccionales. Estas uniones se suelen dibujar con una flecha y definen un grafo dirigido. Cuando las aristas llevan un coste asociado (un entero al que se denomina peso) el grafo es ponderado. Una red es un grafo dirigido y ponderado.
Representacin de grafos
Una caracterstica especial en los grafos es que podemos representarlos utilizando dos estructuras de datos distintas. En los algoritmos que se aplican sobre ellos veremos que adoptarn tiempos distintos dependiendo de la forma de representacin elegida. En particular, los tiempos de ejecucin variarn en funcin del nmero de vrtices y el de aristas, por lo que la utilizacin de una representacin u otra depender en gran medida de si el grafo es denso o disperso. Para nombrar los nodos utilizaremos letras maysculas, aunque en el cdigo deberemos hacer corresponder cada nodo con un entero entre 1 y V (nmero de vrtices) de cara a la manipulacin de los mismos.
entero a cada nodo, que ser el utilizado en la matriz de adyacencia. Como se puede apreciar, la matriz de adyacencia siempre ocupa un espacio de V*V, es decir, depende solamente del nmero de nodos y no del de aristas, por lo que ser til para representar grafos densos.
situacin as podra ser muy conveniente modificar la forma de meter los nodos en la lista (por ejemplo, hacerlo al final y no al principio, o incluso insertarlo en una posicin adecuada), de manera que el algoritmo mismo diera las soluciones ya ordenadas.
Exploracin de grafos
A la hora de explorar un grafo, nos encontramos con dos mtodos distintos. Ambos conducen al mismo destino (la exploracin de todos los vrtices o hasta que se encuentra uno determinado), si bien el orden en que stos son "visitados" decide radicalmente el tiempo de ejecucin de un algoritmo, como se ver posteriormente. En primer lugar, una forma sencilla de recorrer los vrtices es mediante una funcin recursiva, lo que se denomina bsqueda en profundidad. La sustitucin de la recursin (cuya base es la estructura de datos pila) por una cola nos proporciona el segundo mtodo de bsqueda o recorrido, la bsqueda en amplitud o anchura.
Suponiendo que el orden en que estn almacenados los nodos en la estructura de datos correspondiente es A-B-C-D-E-F... (el orden alfabtico), tenemos que el orden que seguira el recorrido en profundidad sera el siguiente: A-B-E-I-F-C-G-J-K-H-D En un recorrido en anchura el orden sera, por contra: A-B-C-D-E-G-H-I-J-K-F Es decir, en el primer caso se exploran primero los verdes y luego los marrones, pasando primero por los de mayor intensidad de color. En el segundo caso se exploran primero los verdes, despus los rojos, los naranjas y, por ltimo, el rosa. Es destacable que el nodo D es el ltimo en explorarse en la bsqueda en profundidad pese a ser adyacente al nodo de origen (el A). Esto es debido a que primero se explora la rama del nodo C, que tambin conduce al nodo D. En estos ejemplos hay que tener en cuenta que es fundamental el orden en que los nodos estn almacenados en las estructuras de datos. Si, por ejemplo, el nodo D estuviera antes que el C, en la bsqueda en profundidad se tomara primero la rama del D (con lo que el ltimo en visitarse sera el C), y en la bsqueda en anchura se explorara antes el H que el G.
Bsqueda en profundidad
Se implementa de forma recursiva, aunque tambin puede realizarse con una pila. Se utiliza un array val para almacenar el orden en que fueron explorados los vrtices. Para ello se incrementa una variable global id (inicializada a 0) cada vez que se visita un nuevo vrtice y se almacena id en la entrada del array val correspondiente al vrtice que se est explorando. La siguiente funcin realiza un mximo de V (el nmero total de vrtices) llamadas a la funcin visitar, que implementamos aqu en sus dos variantes: representacin por matriz de adyacencia y por listas de adyacencia. int id=0; int val[V]; void buscar() { int k; for (k=1; k<=V; k++) val[k]=0; for (k=1; k<=V; k++) if (val[k]==0) visitar(k); } void visitar(int k) // matriz de adyacencia { int t; val[k]=++id; for (t=1; t<=V; t++) if (a[k][t] && val[t]==0) visitar(t); } void visitar(int k) // listas de adyacencia { struct nodo *t; val[k]=++id; for (t=a[k]; t!=z; t=t->sig) if (val[t->v]==0) visitar(t->v); }
El resultado es que el array val contendr en su i-sima entrada el orden en el que el vrtice i-simo fue explorado. Es decir, si tenemos un grafo con cuatro nodos y fueron explorados en el orden 3-1-2-4, el array val quedar como sigue: val[1]=2; // el primer nodo fue visto en segundo lugar val[2]=3; // el segundo nodo fue visto en tercer lugar val[3]=1; // etc. val[4]=4; Una modificacin que puede resultar especialmente til es la creacin de un array "inverso" al array val que contenga los datos anteriores "al revs". Esto es, un array en el que la entrada i-sima contiene el vrtice que se explor en i-simo lugar. Basta modificar la lnea val[k]=++id; sustituyndola por val[++id]=k; Para el orden de exploracin de ejemplo anterior los valores seran los siguientes: val[1]=3; val[2]=1; val[3]=2;
val[4]=4;