Programación 2 Solución Práctico 9 - Árbol Binario de Búsqueda, Árbol Finitario y Árbol n-ario Solución Práctico 9 Objetivos Trabajar con los tipos abstractos de datos Árbol Binario de Búsqueda, Árbol Finitario y Árbol n-ario. Desarrollar y analizar implementaciones para estos TADs. Usarlos para la resolución de problemas simples y complejos. Ejercicio 1 Desarrollar una especificación funcional (sólo funciones, no procedimientos) para el TAD BST - Árbol Binario de Búsqueda (Binary Search Tree) de caracteres, la cual contenga un conjunto mínimo de constructores, selectores, predicados y destructores para: Crear un BST vacío. Determinar si un BST es vacío o no Inserción y supresión de un carácter en un BST. Saber si un carácter pertenece un BST. Solución Ejercicio 1 La Figura 1 presenta una especificación funcional para el TAD BST - Árbol Binario de Búsqueda (Binary Search Tree). 1 2 BST nullBST (); /* Devuelve el arbol vacío */ 3 4 5 6 BST insertBST ( char c , ListBST l ); /* Agrega el caracter c al árbol t . Debe de mantener la cualidad de árbol binario de búsqueda */ 7 8 9 10 BST removeBST ( char c , ListBST l ); /* Elimina el caracter c del árbol t . Debe de mantener la cualidad de árbol binario de búsqueda */ 11 12 13 bool isEmptyBST ( BST t ); /* Determina si un árbol dado es vacío */ 14 15 16 char rootBST ( BST t ); /* Devuelve el valor en la raíz de un árbol no vacío */ 17 18 19 BST leftBST ( BST t ); /* Devuelve el subárbol izquierdo de un árbol t no vacío . */ 20 21 22 BST rightBST ( BST t ); /* Devuelve el subárbol d de un árbol t no vacío . */ Figura 1: Especificación funcional para el TAD BST - Árbol Binario de Búsqueda. Instituto de Computación - Facultad de Ingeniería - UdelaR Página 1 de 8 Programación 2 Solución Práctico 9 - Árbol Binario de Búsqueda, Árbol Finitario y Árbol n-ario Ejercicio 2 Las funciones presentadas en el Práctico 4, forman una especificación para el TAD FTree - Árbol Finitario (Finitary Tree) de naturales, ver Figura 2. 1 2 FTree nullFTree (); /* Devuelve el arbol vacío */ 3 4 5 FTree consFTree ( unsigned int x , ListFTree l ); /* Crea un árbol no vacío a partir de un natural y una lista de hijos ( subárboles ) */ 6 7 8 bool IsEmptyFTree ( FTree t ); /* Determina si un árbol dado es o no vacío */ 9 10 11 unsigned int rootFTree ( FTree t ); /* Devuelve el valor en la raíz de un árbol no vacío */ 12 13 14 ListFTree off spring FTree ( FTree t ); /* Devuelve la lista de hijos ( subárboles ) de la raíz de un árbol no vacío */ Figura 2: Especificación funcional para el TAD FTree - Árbol Finitario. Los Árboles n-arios son un caso particular de los finitarios, donde cada nodo del árbol tiene n hijos o subárboles. ¿Qué diferencias hay entre la especificación de Árboles Finitarios y n-arios? Solución Ejercicio 2 En la Figura 3 se modifica la especificación del TAD FTree para contemplar el hecho de que un árbol n-ario tiene exactamente n subárboles y por lo tanto tiene sentido tener una operción selectora (nthOffspringNTree) que permita obtener cada uno de ellos en lugar de una lista de hijos. 1 2 NTree nullNTree (); /* Devuelve el árbol vacío */ 3 4 5 6 NTree consNTree ( unsigned int x , ListNTree l ); /* Crea un árbol no vacío a partir de un natural y un array n - ario de hijos ( subárboles ) */ 7 8 9 boolean isEmptyNTree ( NTree t ); /* Determina si un árbol dado es o no vacío */ 10 11 12 unsigned int rootNTree ( NTree t ); /* Devuelve el valor en la raíz de un árbol no vacío */ 13 14 15 NTree n t h O f f s p r i n g N T r e e ( unsigned int i , Ntree t ); /* Devuelve el nth hijo ( subárbol ) de la raíz de un árbol no vacío */ Figura 3: Especificación funcional para el TAD NTree - Árbol n-ario. La operación nthOffspringNTree recibe un índice para referirse a cada hijo y de esta forma se evita de tener que definir y usa la lista que tiene como nodos arboles NTree. Alternativamente, si se quisiera devolver el conjunto de hijos, se podría devolver un array con todos los hijos, ver Figura 4. 1 2 3 NTree off spring NTree ( NTree t ); /* Devuelve un array n - ario con los hijos ( subárboles ) de la raíz de un árbol no vacío */ Figura 4: Devolver un array con todos los subárboles de un nodo de un NTree. Instituto de Computación - Facultad de Ingeniería - UdelaR Página 2 de 8 Programación 2 Solución Práctico 9 - Árbol Binario de Búsqueda, Árbol Finitario y Árbol n-ario Para más claridad se puede hacer typedef NTree arrayNTree; para explicitar que se devuelve un array. La operacion se especificaría como en la Figura 5. 1 2 arrayNTree o ffspri ngNTre e ( NTree t ); /* Devuelve un array n - ario con los hijos de la raíz de un árbol no vacío */ Figura 5: Devolver un array con todos los subárboles de un nodo de un NTree. Instituto de Computación - Facultad de Ingeniería - UdelaR Página 3 de 8 Programación 2 Solución Práctico 9 - Árbol Binario de Búsqueda, Árbol Finitario y Árbol n-ario Ejercicio 4 Desarrollar una implementación completa del TAD FTree - Árbol Finitario de naturales, utilizando la representación de primer hijo- siguiente hermano. Solución Ejercicio 4 La representación del árbol finitario ya fue vista en el práctico 4 y se presenta en la Figura 6. 1 2 3 4 struct nodoFTree { unsigned int : dato ; nodoFTree * pH , * sH ; } 5 6 typedef nodoFTree * FTree ; 7 8 9 10 11 struct nodoLista { FTree elem ; nodoLista * sig ; } 12 13 typedef nodoLista * listFTree ; Figura 6: Representación TAD FTree - Árbol Finitario de naturales, utilizando la representación de primer hijo- siguiente hermano. Observar diferencias con el Ejercicio 5, que viene a continuación, donde se pide la especificación del TAD FTree que contempla el hecho de que un árbol n-ario tiene exactamente n hijos. Instituto de Computación - Facultad de Ingeniería - UdelaR Página 4 de 8 Programación 2 Solución Práctico 9 - Árbol Binario de Búsqueda, Árbol Finitario y Árbol n-ario Ejercicio 5 Desarrollar una implementación completa del TAD Árbol n-ario de naturales, que tome provecho de que todos los nodos del árbol tienen n hijos. ¿Qué representación considera adecuada para representar los hijos? Solución Ejercicio 5 Se puede sacar provecho de que cada nodo tiene exactamente n hijos para tener un array que los contiene y de esta forma implementar eficientemente la operación nthOffspringNTree. Observar que un array de hijos en C* se define como un puntero, ver Figura 7. 1 2 3 4 struct nodoNTree { unsigned int dato ; nodoNTree * hijos ; }; 5 6 typedef nodoNTree * NTree ; Figura 7: Representación TAD NTree. Para la creación de un árbol con consNTree se tienen diferentes posibilidades que se presentan a continuación. Las variantes se diferencian en si comparten memoria con los parámetros de entrada, en qué grado comparten memoria y esto se refleja en la foma de copiar la información, en particular la copia de los hijos ver Figura 8, Figura 9 y Figura 10. 1 2 3 4 5 NTree consNTree ( unsigned int x , NTree hijos ) { NTree res = new nodoNTree ; res -> dato = x ; res -> hijos = hijos ; }; Figura 8: consNTree donde los hijos comparten memoria con los árboles del array hijos y el array que los contiene también. 1 2 3 4 5 6 NTree consNTree ( unsigned int x , NTree hijos ) { NTree res = new nodoNTree ; res -> dato = x ; res -> hijos = new nodoNTree [ N ]; for ( int i = 0; i < N ; i ++) res - > hijos [ i ] = copiaLimpia ( hijos [ i ]); 7 // copiaLimpia debe implementarse aparte . 8 9 }; Figura 9: consNTree donde los hijos NO comparten memoria con los árboles del array hijos y el array que los contiene tampoco. 1 2 3 4 5 NTree consNTree ( unsigned int x , NTree * hijos ) { NTree res = new nodoNTree ; res -> dato = x ; memcpy ( res -> hijos , hijos , sizeof hijos ); }; Figura 10: consNTree donde los hijos comparten memoria con los árboles del array hijos y el array que los contiene no. Se utiliza la operación memcpy de C que permite copiar un bloque de memoria. Instituto de Computación - Facultad de Ingeniería - UdelaR Página 5 de 8 Programación 2 Solución Práctico 9 - Árbol Binario de Búsqueda, Árbol Finitario y Árbol n-ario La operación nthOffspringNTree se implementa de forma eficiente devolviendo i-ésimo hijo del array de hijos, ver Figura 11. 1 2 3 NTree n t h O f f s p r i n g N T r e e ( unsigned int i , Ntree t ) { return t - > hijos [ i - 1]; // el primer hijo esta en la posicion 0 } Figura 11: nthOffspringNTree aprovechando la representación que almacena cada hijo en su lugar correspondiente en el array hijos. ¿Cómo se crea un nodo hoja? Instituto de Computación - Facultad de Ingeniería - UdelaR Página 6 de 8 Programación 2 Solución Práctico 9 - Árbol Binario de Búsqueda, Árbol Finitario y Árbol n-ario Ejercicio 6 (Segundo Parcial 2009) Suponga que se define una variante de árboles binarios de búsqueda (BST) balanceados según la cantidad de nodos de los subárboles. Se considera que un BST está balanceado si para cada nodo se cumple que la diferencia de la cantidad de nodos de sus subárboles (izquierdo y derecho) difiere en a lo sumo una unidad. Considere la declaración del tipo de los árboles binarios de búsqueda de enteros que se presenta en la Figura 12a, donde cada nodo de un árbol de tipo BST guarda en el campo CN la cantidad de nodos del árbol cuya raíz es dicho nodo. Recordar que la cantidad de nodos de un árbol vacío es 0, por definición. La Figura 12b presenta un ejemplo gráfico. Cada nodo se representa por un par de valores donde la primer componente es el valor de dato y el segundo el de CN para dicho nodo. ⊥ representa el árbol vacío. (10,8) struct BSNode { int dato ; unsigned int CN ; BSNode * izq , * der ; }; typedef BSNode * BST ; 1 2 3 4 5 6 (5,3) (3,1) (25,4) (8,1) (a) Definición de tipo para el TAD BST balanceado (15,2) ⊥ (30,1) (20,1) (b) Ejemplo Figura 12: Ejericio 6, BST balanceado. Implementar una función recursiva insertar que, dados un árbol binario de búsqueda de enteros A de tipo BST, y un entero x, inserte a x en A y retorne TRUE, si y sólo si, luego de la inserción cada nodo cumple la condición de balanceo previamente definida. Tenga en cuenta las siguientes consideraciones: Si x ya estaba en A, la función no deberá modificar el árbol y el resultado será TRUE. La función insertar debe dejar el campo CN de cada nodo consistente con su definición, asumiendo que en el árbol parámetro se cumple que para cada nodo el campo CN contiene la cantidad de nodos del árbol cuya raíz es dicho nodo y además, que cada nodo (del árbol parámetro) cumple la condición de balanceo referida. La función insertar debe recorrer un sólo camino del árbol parámetro. No se permite usar funciones o procedimientos auxiliares en este ejercicio, salvo las funciones que se presentan en la Figura 13, las cuales se consideran predefinidas. Cabe aclarar que no se pide balancear el árbol, sino simplemente insertar según el criterio de los árboles binarios de búsqueda, actualizar los campos CN, que correspondan, y retornar un booleano que indique si luego de la inserción se cumple la condición de balanceo definida, asumiendo que ésta originalmente se verificaba en cada nodo del árbol parámetro. La firma de la función insertar es la siguiente: 1 bool insertar ( BST &A , int x ); 1 unsigned int abs ( int x ); /* Retorna el valor absoluto de x */ 2 3 unsigned int cantNodos ( BST A ); /* Retorna 0 si A es el árbol vacío y el valor del campo CN de la raíz de A en otro 4 5 caso */ Figura 13: Funciones auxiliares para el Ejericio 6. Instituto de Computación - Facultad de Ingeniería - UdelaR Página 7 de 8 Programación 2 Solución Práctico 9 - Árbol Binario de Búsqueda, Árbol Finitario y Árbol n-ario Solución Ejercicio 6 Se propone implementar insertar de forma recursiva. Para esto se debe considerar el paso base y el paso recursivo: El paso base lo tomamos cuando se quiere insertar en un arbol vacío, en ese caso simplemente se crea un nodo nuevo y se coloca ahí el dato x. En el paso recursivo habrán varias acciones a realizar a la vuelta que tienen que ver con que se haya insertado o no, por lo tanto se debe hacer recursión de cola. Las acciones a realizar son análogas para la inserción hacia la izquierda o a la derecha. Por ejemplo, si se va a insertar a la izquierda se debe: 1. Primero guardar el dato del tamaño del súbarbol izquierdo: cant = cantNodos(A->izq); 2. Luego se invoca recursivamente a la función insertar con el súbarbol izquierdo. 3. Se compara la cantidad de elementos que había en el súbarbol izquierdo antes de la invocación recursiva a insertar con los que hay después para saber si se insertó: inserte = cant != A->izq->CN; 4. Finalmente se controla si hubo desbalanceo en el nodo actual o alguno de los subárboles para devolver el valor booleano correspondiente: balanceado = balanceado && (abs(cantNodos(A->izq)-cantNodos(A->der))<=1); En la Figura 14 se presenta una versión más refinada del pseudocódigo de insertar. 1 2 bool insertar ( BST &A , int x ) { bool balanceado = true ; 3 if ( A == NULL ){ A = new BSTNode ; A - > dato = x ; A - > CN = 1; A - > izq = A - > der = NULL ; } else { 4 5 6 7 8 9 10 11 // paso base // paso recursivo ; no considero el caso x == A - > dato // ya que entonces no hago nada y retorno true int cant ; bool inserte ; 12 13 14 if ( x < A - > dato ){ cant = cantNodos (A - > izq ); balanceado = insertar (A - > izq , inserte = cant != A - > izq - > CN ; 15 16 17 18 19 20 // debería insertar a la izquierda // evito chequear si A - > izq == NULL x ); // notar que acá si puedo evitar la llamada a // cantNodos , accediendo a través de A - > izq porque // nunca sería NULL . } 21 22 if ( x > A - > dato ){ ... // ídem para A - > der . } 23 24 25 // debería insertar a la derecha 26 if inserte { A - > CN ++; balanceado = balanceado && ( abs ( cantNodos (A - > izq ) - cantNodos (A - > der )) <=1); } 27 28 29 30 } 31 32 retornar balanceado ; 33 34 } Figura 14: Pseudocódigo de insertar del Ejericio 6. Instituto de Computación - Facultad de Ingeniería - UdelaR Página 8 de 8