Estructuras de Datos y Algoritmos. Curso 2004/05 Tema 6. Árboles y Árboles Binarios Mabel Galiano Natividad Prieto Departamento de Sistemas Informáticos y Computación Escuela Técnica Superior de Informática Aplicada Índice 1. Introducción a las Estructuras Jerárquicas: Árboles 2 1.1. Conceptos generales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.2. Árboles Binarios: definición, propiedades y tipos de Recorrido . . . . . . . . . . 4 1.3. Representación Contigua de un Árbol Binario Completo . . . . . . . . . . . . . 8 1.4. Representación Enlazada de un Árbol Binario General: el concepto de Nodo Binario 9 2. Implementación en Java de un Árbol Binario General 9 2.1. La clase ArbolBinario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.2. La clase NodoBinario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Objetivos y Bibliografı́a El objetivo general de este tema es introducir y estudiar el Árbol Binario como la estructura jerárquica básica a partir de la cual se obtienen tanto el Montı́culo Binario o Heap como el Árbol Binario de Búsqueda, que se presentarán más tarde en la asignatura como posibles Implementaciones de alguno de los Modelos avanzados, bien Cola de Prioridad o bien Diccionario. Para ello, a partir de los conceptos generales asociados al Árbol, se definirá el Árbol Binario, sus propiedades y operaciones tanto básicas como de Recorrido (In-Orden, Pre-Orden, Post-Orden y Por Niveles). Además, se presentarán sus dos Representaciones posibles: contigua para un Árbol Binario Completo, que se utilizará para representar un Montı́culo, y enlazada para un Árbol Binario general, que será el fundamento de la Representación de los Árboles Binarios de Búsqueda y en función de la cual se diseñará en Java la clase ArbolBinario que representa un Árbol Binario general mediante un único Enlace a su Nodo raı́z, esto es como una estructura Enlazada de Objetos de la clase NodoBinario. Como bibliografı́a básica de este tema se utilizarán los siguientes capı́tulos y apartados del libro de Weiss, M.A. Estructuras de datos en Java: el apartado 5 del capı́tulo 6 para la introducción de las estructuras de Árbol y Árbol Binario y los apartados del 1 al 3 del capı́tulo 17 para lo referente a las clases Java ArbolBinario y NodoBinario. 1 1. Introducción a las Estructuras Jerárquicas: Árboles En ocasiones los Datos de una Colección mantienen relaciones de tipo Jerárquico que no es posible expresar mediante una Representación Lineal de la Colección, como la Lista Enlazada Indirecta o el simple array estudiadas en el tema anterior. Por ejemplo, son claramente jerárquicas las relaciones que mantienen los distintos directorios, y ficheros, en los que se organiza la información de un Sistema Operativo o los operadores y operandos que conforman una expresión aritmética a evaluar durante la compilación o interpretación de un programa. En estos casos, la forma más natural de representar la Colección es la de Árbol, la estructura Jerárquica por excelencia; la siguiente figura muestra gráficamente el Árbol que representa la Colección de Directorios de las prácticas de EDA a partir del directorio home de un usuario: $HOME EDA libJava estructurasDeDatos jerarquicos lineales progJava util misFiguras bancoDeModems hashing En cuanto a la expresión aritmética (((a*b)+(c+d))*(e-f)), en notación infija y parentizada, se muestra a continuación el Árbol que la puede representar: * + − * a e + b c f d Muy importante también para los propósitos de la asignatura resulta destacar que la estructura en Árbol, además de representar las relaciones jerárquicas existentes entre los Datos de una Colección, puede favorecer la Búsqueda Dinámica caracterı́stica de los Modelos Cola de Prioridad y Diccionario en tiempos sublineales, que como es sabido no consigue su Representación Lineal. Por ejemplo, si se desea representar la Colección de Palabras mes, mesa, meta, metro, coro y corona cualquier estructura Lineal requiere un tiempo de Búsqueda del orden del número de Palabras que la forman, mientras que el Árbol de Palabras (Trie) que se muestra a continuación sólo exige un tiempo proporcional a la longitud de la Palabra buscada: m c e o t s a r o r a o n a 2 Otro ejemplo significativo de Búsqueda Dinámica eficiente es el que se consigue con Árboles como el que muestra la siguiente figura y que representa la Colección de Integer 100, 50, 200, 25, 75, 150, 300 y 180: 100 50 200 75 25 300 150 180 En este Árbol, cara a favorecer cualquier proceso de Búsqueda Binaria (insertar, borrar o buscar un Dato), los Integer de la Colección se disponen de la siguiente forma ordenada: el primero, el 100, ocupa la Raı́z del Árbol y a partir de ésta, cada Dato menor que el primero se sitúa en la parte izquierda del Árbol y cada Dato mayor en la derecha; además, para cada subÁrbol o parte de éste, el proceso se repite. Por ejemplo, el Dato 75 al ser menor que 100 pero mayor que 50 se sitúa a la izquierda de la Raı́z, ocupada por 100, y a la derecha del sub-Árbol cuya Raı́z ocupa 50; con ello bastarán 2 comparaciones para encontrar el Integer 200 en el Árbol. Árboles de este tipo, denominados Árboles Binarios de Búsqueda, se estudiarán más adelante con detalle para conocer bajo qué condiciones especı́ficas garantizan una Búsqueda Dinámica con coste logarı́tmico. 1.1. Conceptos generales Un Árbol es una estructura Jerárquica que se puede definir por medio de un conjunto de Nodos, uno de los cuales es distinguido como la Raı́z del Árbol, y un conjunto de Aristas tal que cualquier Nodo H, a excepción de la Raiz, está conectado por medio de una Arista a un único Nodo P; se dice entonces que P es el Padre de H y H es un Hijo de P. Si un Nodo no tiene ningún Hijo se denomina Hoja; excluidas las Hojas, al resto de Nodos del Árbol, los que tienen algún Hijo, se les denomina Internos. Para ejemplificar las definiciones realizadas, en la siguiente figura se muestra un Árbol con 14 Nodos etiquetados con las Letras de la A a la N: A B D C E F I M G J H K L N Figura 1: Árbol Ejemplo con 14 Nodos Nótese que el Nodo etiquetado como A es la Raı́z del Árbol y B y C son sus Hijos; el Nodo etiquetado con C es el Padre de los Nodos F, G y H y el Nodo I tiene dos Hijos etiquetados como M.Galiano,N.Prieto 3 M y N. Asimismo, los Nodos con las etiquetas D, M, N, F, J, K y L son Hojas y el resto Nodos Internos. Caminos entre Nodos: Profundidad, Nivel y Altura de un Nodo Un Árbol se caracteriza porque desde la Raı́z a cada uno de los Nodos hay un Camino único cuya Longitud viene dada por el número de Aristas que lo componen. Por ejemplo, en el Árbol de la Figura 1 el Camino desde la Raı́z hasta la Hoja etiquetada como N es A-B-E-I-N y tiene longitud 4. A partir de la Longitud de un Camino se definen la Profundidad, el Nivel y la Altura de un Nodo y un Árbol. Se denomina Profundidad de un Nodo a la Longitud del Camino que va desde la Raı́z hasta él; por tanto, la Profundidad de la Raı́z es 0. Además, se dice que todos los Nodos que están a la misma Profundidad están en el mismo Nivel, siendo cero el de la Raı́z. Por ejemplo, en la Figura 1 los Nodos D, E, F, G y H ocupan el Nivel 2, pues todos ellos tienen la misma Profundidad, también 2. Esta misma figura permite también observar que la representación gráfica que se hace de un Árbol es precisamente por Niveles, empezando por el de la Raı́z, el cero, hasta el que ocupan las Hojas. Finalmente, la Altura de un Nodo se define como la Longitud del Camino que va desde dicho Nodo hasta la Hoja más profunda bajo él. En base a esta definición, la Altura de un Árbol es la de su Nodo Raı́z, 4 la del Árbol de la Figura 1. Relaciones entre Nodos: Tamaño de un Nodo Utilizando la nomenclatura tı́pica de la Genealogı́a, a los Nodos de un Árbol que tienen el mismo Padre se les denomina Hermanos; ası́, en el Árbol de la Figura 1 los Nodos F, G y H son Hermanos porque tienen el mismo Padre, el Nodo etiquetado con C. Asimismo, si hay un Camino desde el Nodo u hasta el Nodo v, se dice que u es un Ascendiente de v y que v es un Descendiente de u. Si u = v, entonces u es un Ascendiente Propio de v y v es un Descendiente Propio de u; ası́, en el Árbol ejemplo son Ascendientes Propios del Nodo etiquetado con N los Nodos I, E, B y A y son Descendientes propios de C los Nodos F, G, H, J, K y L. En base a la nomenclatura que se acaba de introducir, el Tamaño de un Nodo se define como el número de Descendientes que tiene y el de un Árbol como el de su Nodo Raı́z. Por ejemplo, el Tamaño del Nodo C de la Figura 1 es 7, el del Nodo B es 6 y el del Árbol, i.e. el de su Nodo Raı́z, es 14. Definición Recursiva de un Árbol Una definición de Árbol alternativa a la presentada es la recursiva. Según ésta, un Árbol es bien un Árbol vacı́o o bien un Nodo Raı́z y cero o más sub-Árboles no vacı́os, cada una de cuyas Raı́ces está conectada por medio de una Arista con la Raı́z del Árbol. Lo interesante de esta definición es que permite identificar el concepto de Nodo con el de Árbol: un Nodo define el Árbol del que es Raı́z. Será al hablar de Árboles Binarios cuando se explotará esta identificación. 1.2. Árboles Binarios: definición, propiedades y tipos de Recorrido Un Árbol Binario es un Árbol en el que ningún Nodo puede tener más de dos Hijos, que por ello pueden ser distinguidos como Hijo Izquierdo e Hijo Derecho. Nótese cómo el Árbol ejemplo que se ha venido utilizando hasta ahora, Figura 1, no es un Árbol Binario: tiene un Nodo, el etiquetado con C, que tiene 3 Hijos; sin embargo, el que muestra la siguiente figura sı́ lo es y 4 tiene 7 Nodos etiquetados: con A la Raı́z, con B la Raı́z de su Hijo Izquierdo y con C la de su Hijo Derecho (que a su vez, nótese, sólo tiene Hijo Izquierdo, que es el Nodo F). A B D C E F G En la siguiente figura aparece un Árbol Binario que sólo difiere del que se acaba de presentar en que su Nodo F es Hijo Derecho, y no Izquierdo, de C: A B D C E F G Como se puede demostrar fácilmente por Inducción, en un Árbol Binario de Altura H el número máximo de Nodos del Nivel i es 2i , 0 ≤ i ≤ H. A partir de este resultado y en base a las definiciones previamente realizadas se pueden establecer los siguientes: 1. su número máximo de Nodos es i = 0 ... H 2i = 2H+1 − 1; 2. su número máximo de Hojas es (2H+1 − 1) − ( i = 0 ... H-1 2i ) = 2H ; 3. su número máximo de Nodos Internos es (2H+1 − 1) − (2H ) = 2H − 1. Árbol Binario Lleno y Completo Un Árbol Binario de Altura H es Lleno si tiene todos sus Niveles completos, esto es si en cada Nivel i, 0 ≤ i ≤ H, tiene exactamente 2i Nodos. Por ejemplo, el Árbol Binario de Altura 3 que muestra la siguiente figura es Lleno: A partir de esta definición, en base a las propiedades expuestas para un Árbol Binario, es inmediato deducir que en un Árbol Binario Lleno de Tamaño N y Altura H se cumplen las siguientes propiedades: H = log2 N y N = 2H+1 − 1. Asimismo, se dice que un Árbol Binario es Completo si tiene todos sus Niveles completos a excepción quizás del último que, entonces, debe de tener situados todos sus Nodos, las Hojas del Árbol, tan a la izquierda como sea posible; por ejemplo, el Árbol Binario que aparece en la siguiente figura es un Árbol Binario Completo: M.Galiano,N.Prieto 5 Nótese que mientras un Árbol Binario Lleno es Completo no es cierta la afirmación inversa, por lo que para la Altura y Tamaño de un Árbol Binario Completo se cumplen, respectivamente, las siguientes desigualdades: H ≤ log2 N y 2H ≤ N ≤ 2H+1 − 1. A modo de resumen, y para su uso posterior en la asignatura, conviene destacar ahora que la relación expresada entre la Altura H y el Número de Nodos N de un Árbol Binario Completo (o Lleno) le confiere una caracterı́stica muy importante: es Equilibrado, pues los N Datos de una Colección se distribuyen en él de forma que la longitud de su Camino más largo, i.e. H, es a lo sumo log2 N. Definición Recursiva de Árbol Binario De forma análoga a la empleada para un Árbol, y como se expresa gráficamente en la siguiente figura, un Árbol Binario se define recursivamente bien como un Árbol Binario vacı́o o bien como un Nodo Raı́z y dos sub-Árboles Binarios, uno Izquierdo y otro Derecho, que pueden ser vacı́os. RAIZ HIJO IZQUIERDO HIJO DERECHO Figura 2: Representación gráfica de un Árbol Binario definido recursivamente Esta definición resulta de gran utilidad para lograr implementar un Árbol Binario General, por lo que se empleará repetidamente en el resto del tema. En primer lugar, al identificar los conceptos de Árbol y Nodo, permite que un Árbol Binario se represente mediante un Enlace a su Nodo (Binario) Raı́z y que, por tanto, las operaciones sobre un Árbol Binario sean operaciones que efectivamente se realizan, se lanzan, sobre su Nodo Raı́z. En segundo lugar, aunque igualmente importante, proporciona una descomposición recursiva del Árbol Binario en base a la cual es posible diseñar fácil y naturalmente la mayorı́a de sus operaciones; a modo de ejemplo, obsérvese que el Tamaño o número de Nodos del Árbol Binario que muestra la Figura 2 se define recursivamente bien como cero si el Árbol Binario es vacı́o y sino como la suma del Tamaño de su Raı́z, esto es 1, y la del Tamaño de sus Hijos Izquierdo y Derecho. Operaciones de un Árbol Binario A continuación se clasifican en base a su coste las operaciones que se pueden realizar sobre un Árbol Binario, o equivalentemente sobre su Nodo (Binario) Raı́z; al presentarlas se describirán 6 de manera somera, reservando para la última sección de este tema la definición de sus perfiles y los detalles de su implementación en Java: 1. Operaciones Básicas o de coste del orden de una constante. Agrupadas por su funcionalidad, éstas son: dos constructoras, la de un Árbol Binario vacı́o y la de una de sus Hojas, i.e. la de un Árbol con un único Nodo Raı́z compuesto por un Dato y dos Hijos vacı́os; una modificadora, denominada unión, que dados dos Árboles Binarios arBinIzq y arbinDer y un Dato x obtiene como resultado un Árbol cuya Raı́z contiene a x y cuyos Hijos son arBinIzq y arbinDer; finalmente, la última operación Básica es la tı́pica consultora que comprueba si un Árbol Binario está vacı́o. 2. Operaciones de Recorrido cuyo coste es lineal con el Número de Nodos del Árbol visitados; en particular, según el orden en que se visitan los Nodos durante un Recorrido se pueden establecer los siguientes tipos: de Recorrido en Anchura en el que los Nodos se visitan Nivel a Nivel y, dentro de un Nivel, siempre de izquierda a derecha. En base a esta definición es claro que la forma más natural de describirlas es la iterativa, función que más tarde se explicitará durante el diseño del método Java imprimirPorNiveles, que a modo de toString obtiene en formato texto el Recorrido en Anchura de los Nodos de un Árbol Binario. de Recorrido en Profundidad en el que los Nodos se visitan bajando por las Ramas del Árbol y empezando siempre por la situada más a la izquierda; es por ello que, al contrario que lo que ocurrı́a en el Recorrido en Anchura, la forma más natural de describirlo es la recursiva. Nótese que el cálculo del Tamaño y la Altura de un Árbol Binario suponen un Recorrido en Profundidad de éste, lo mismo que la obtención de la representación en formato texto de sus Nodos (de nuevo toString pero en Profundidad) o la de su copia o la de su imagen simétrica o ... Teniendo en cuenta que un Nodo se identifica con el Árbol del que es Raı́z, según la naturaleza de la operación a realizar cada Nodo puede ser visitado durante un Recorrido en Profundidad de cualquiera de las tres formas siguientes: bien antes que los Hijos Izquierdo y Derecho o en Pre-Orden, bien después que éstos o en PostOrden, bien después que el Hijo Izquierdo y antes que el Derecho o en In-Orden. Conviene mencionar ahora que también son posibles los Recorridos en Profundidad simétricos a los tres indicados, i.e. en los que se visita primero el Hijo Derecho y después el Izquierdo. Aplicando las definiciones de los cuatro tipos de Recorrido expuestos al Árbol Binario que muestra la siguiente figura, A B D C E F G I H J K las diferentes secuencias de Nodos que se visitan durante cada uno de ellos son: M.Galiano,N.Prieto 7 en In-Orden: D-B-E-H-A-F-C-J-I-K-G en Pre-Orden: A-B-D-E-H-C-F-G-I-J-K en Post-Orden: D-H-E-B-F-J-K-I-G-C-A Por Niveles: A-B-C-D-E-F-G-H-I-J-K Es más, nótese que si en lugar de Caracteres el Árbol Binario representa una expresión aritmética, sus Recorridos en In-Orden, Pre-Orden y Post-Orden corresponden, respectivamente, a las notaciones Infija, Prefija y Postfija de dicha expresión. Cuestión: utilizando como Árbol ejemplo el que se acaba de mostrar, determı́nese qué tipo de Recorrido en Profundidad requieren el cálculo de su Altura, Tamaño y la obtención de un duplicado de él o copia. 1.3. Representación Contigua de un Árbol Binario Completo Como ya se ha indicado, un Árbol Binario Completo es un tipo especial de Árbol Binario pues en él los Datos de una Colección se distribuyen de forma equilibrada. Además de esta caracterı́stica, se presenta en esta sección una propiedad igualmente importante, estructural, que será utilizada en el siguiente tema para realizar la Implementación eficiente de la EDA Cola de Prioridad: para representar un Árbol Binario Completo sin ambigüedad alguna basta con almacenar el resultado de su Recorrido por Niveles en un array. 1 13 2 3 21 4 5 24 8 0 6 31 9 65 10 26 16 7 19 68 32 13 21 16 24 31 19 68 65 26 32 1 2 3 4 5 6 7 8 9 10 Especı́ficamente, como muestra la figura, el Dato que ocupa la Raı́z del Árbol se sitúa en la posición 1 del array (la cero se deja libre por motivos que se explicarán más tarde), el primer Dato del primer Nivel en la 2, el segundo Dato del primer Nivel en la 3 y ası́ sucesivamente hasta que situar el último Dato, el N-ésimo si N es el Tamaño del Árbol, del Nivel H, el último si H es la Altura del Árbol. Con una Representación como la descrita, que recibe el nombre de Implı́cita, no se necesita espacio adicional alguno para representar los dos Hijos del Árbol, sólo el del array que almacena sus Datos y un ı́ndice que indica su talla actual. En efecto, si la i-ésima componente del array, 1 ≤ i ≤ N, representa el i-ésimo Nodo (Raı́z) de su Recorrido por Niveles entonces: su Hijo Izquierdo se encuentra en la posición 2 ∗ i, siempre que 2 ∗ i ≤ N; su Hijo Derecho se encuentra en la posición 2 ∗ i + 1, si 2 ∗ i + 1 ≤ N; su Nodo Padre está en la posición i2 si i = 1, ya que la Raı́z del Árbol es el único Nodo que no tiene Padre; si lo tuviera, ocuparı́a la posición cero del array, por lo que, precisamente, ésta debe dejarse libre. Además de su destacada eficiencia, una ventaja adicional de la Representación Implı́cita es que simplifica las operaciones de Recorrido del Árbol. 8 1.4. Representación Enlazada de un Árbol Binario General: el concepto de Nodo Binario Como ya se ha indicado, la definición recursiva de un Árbol Binario permite identificar cualquiera de sus Nodos con los (sub)Árboles Binarios de los que son Raı́z; de donde resulta directo establecer que un Árbol Binario General sea representado únicamente mediante un Enlace a su Nodo Raı́z. Para que la identificación sea completa y congruente, la Representación de un Nodo Binario, es decir de un Nodo de un Árbol Binario, debe constar de tres elementos: un Objeto que representa el Dato que en él se sitúe y un Enlace al Nodo Binario que es Raı́z de su Hijo Izquierdo y otro al Nodo Binario Raı́z de su Hijo Derecho, como se muestra en la siguiente figura: NodoBinario dato izq der Figura 3: Estructura de un Nodo Binario Nótese que mientras los Nodos de una Lista Enlazada Indirecta sólo contienen un Enlace al siguiente, una Representación Enlazada de un Árbol Binario requiere dos Enlaces, tantos como Hijos tiene el Árbol que representa. 2. Implementación en Java de un Árbol Binario General A partir de las operaciones y Representación definidas previamente, en esta última sección se desarrollan las dos clases Java que implementan un Árbol Binario General: ArbolBinario y NodoBinario. Mientras que la primera se declarará pública, por si se quisiera utilizar aposteriori en aplicaciones tı́picas de los Árboles Binarios, la segunda es sólo friendly pues, como se recordará, sólo debe resultar visible a las clases que se desarrollarán en temas posteriores como posibles implementaciones de los Modelos Avanzados Cola de Prioridad y Diccionario; ambas se ubicarán en el paquete jerarquicos del proyecto estructurasDeDatos de libJava. 2.1. La clase ArbolBinario La clase Java ArbolBinario que se presenta a continuación representa un Árbol Binario General mediante un único Enlace a su Nodo Binario Raı́z; por ello consta de un único atributo NodoBinario raiz sobre el que la gran mayorı́a de sus métodos (tamanyo, altura, etc.) se limitan a lanzar o invocar el correspondiente método de NodoBinario. Además de su simplicidad, resulta conveniente señalar dos detalles más presentes en la codificación de los métodos de ArbolBinario. En primer lugar, obsérvese que la instrucción principal de su método unir, raiz = new NodoBinario(x, arBin1.raiz, arBin2.raiz), no se llega a ejecutar si los subÁrboles parámetro arBin1 y arBin2 son iguales; si lo son, se lanza y propaga la Excepción UnionNoPermitida. Además, en caso de ejecutarse se evita el posible aliasing entre dichos subÁrboles parámetro y los Hijos del Árbol resultado de la unión: si no se ejecutase arBin1.raiz = null cuando arBin1 != null, tanto arbin1.raiz como this.raiz.izq apuntarı́an al mismo Nodo, lo mismo que arbin2.raiz y this.raiz.der si no M.Galiano,N.Prieto 9 se ejecutara arBin2.raiz = null cuando arBin2 != null. En segundo lugar, los métodos de Recorrido siguen dos estilos de diseño distintos al lanzar el método homónimo de NodoBinario: tamanyo, altura y duplicar lo hacen invocando a un método estático mientras que los distintos imprimir invocan a un método dinámico; como se puede observar ahora, y también en su implementación en NodoBinario, la única diferencia entre ambos tipos de métodos radica en cómo tratan el caso de un Nodo o (sub)Árbol vacı́o. package jerarquicos; import excepciones.*; public class ArbolBinario { protected NodoBinario raiz; public ArbolBinario() { raiz = null;} public ArbolBinario(Object x) { raiz = new NodoBinario(x);} public void unir(Object x, ArbolBinario arBin1, ArbolBinario arBin2) throws UnionNoPermitida { if( arBin1.raiz == arBin2.raiz && arBin1.raiz != null ) throw new UnionNoPermitida("Los dos subÁrboles son iguales; unión imposible !!!"); this.raiz = new NodoBinario( x, arBin1.raiz, arBin2.raiz ); /** Para evitar el aliasing, desreferenciar los subÁrboles utilizados en la unión */ if( this != arBin1 ) arBin1.raiz = null; if( this != arBin2 ) arBin2.raiz = null; } public boolean esVacio() { return ( this.raiz == null ); } public int tamanyo() { return NodoBinario.tamanyo(raiz);} public int altura() { return NodoBinario.altura(raiz); } public void duplicar(ArbolBinario original){ if ( original.raiz != null ) this.raiz = original.raiz.duplicar(); } public String imprimirPostOrden(){ if ( this.raiz != null ) return raiz.imprimirPostOrden(); else return "*"; } public String imprimirPreOrden(){ if ( this.raiz != null ) return raiz.imprimirPreOrden(); else return "*"; } public String imprimirInOrden(){ if ( this.raiz != null ) return raiz.imprimirInOrden(); else return "*"; } public String imprimirPorNiveles(){ if ( this.raiz != null ) return raiz.imprimirPorNiveles(); else return "*"; } } 2.2. La clase NodoBinario La clase Java NodoBinario representa un Nodo de un Árbol Binario, por lo que sus atributos serán tres (según muestra la Figura 3): Object dato, que representa el Dato (de tipo genérico) que contiene el Nodo y dos Enlaces NodoBinario izq, der que representan, respectivamente, a sus Hijos Izquierdo y Derecho. Nótese también que en NodoBinario se re-utiliza el software desarrollado para implementar una Cola: el interfaz Cola del paquete modelos y la clase que lo implementa ArrayCola del paquete lineales; la razón es que su método imprimirPorNiveles implementa un Recorrido en Anchura de un Nodo que sólo se puede realizar iterativamente, y no en base a su descomposición recursiva. Para ello, como ejemplifica el código propuesto, se debe utilizar como estructura auxiliar una Cola que inicialmente contiene el Nodo a recorrer por Niveles; mientras queden elementos en ella, el tratamiento que sistemáticamente recibe su primer elemento o Nodo encolado es: tratar el Dato a él asociado, eliminarlo y, si no son 10 vacı́os, encolar sus Hijos empezando por el Izquierdo. Obsérvese que es al tratar la Cola cuando, indirectamente, se realiza el Recorrido iterativo por Niveles un Nodo. package jerarquicos; import modelos.*; import lineales.*; final class NodoBinario { Object dato; NodoBinario izq, der; NodoBinario(Object x) {this(x, null, null); } NodoBinario(Object x, NodoBinario izquierdo, NodoBinario derecho) { dato = x; izq = izquierdo; der = derecho; } static int tamanyo(NodoBinario actual) { if ( actual == null ) return 0; else return 1 + tamanyo(actual.izq) + tamanyo(actual.der); } static int altura(NodoBinario actual) { if ( actual == null) return -1; else return (Math.max(altura(actual.izq), altura(actual.der)) + 1); } NodoBinario duplicar(){ NodoBinario raiz = new NodoBinario(this.dato); if( this.izq != null ) raiz.izq = this.izq.duplicar( ); if( this.der != null ) raiz.der = this.der.duplicar( ); return raiz; } String imprimirPostOrden(){ String res = ""; if( this.izq != null ) res += izq.imprimirPostOrden( ); if( this.der != null ) res += der.imprimirPostOrden( ); res += dato.toString()+"\n"; return res; } String imprimirInOrden(){ String res = ""; if( this.izq != null ) res += this.izq.imprimirInOrden( ); res += dato.toString()+"\n"; if( this.der != null ) res += this.der.imprimirInOrden( ); return res; } String imprimirPreOrden(){ String res = dato.toString()+"\n"; if( this.izq != null ) res += this.izq.imprimirPreOrden( ); if( this.der != null ) res += this.der.imprimirPreOrden( ); return res; } String imprimirPorNiveles(){ String res = ""; Cola q = new ArrayCola(); q.encolar(this); while ( !q.esVacia() ){ NodoBinario nodoActual = (NodoBinario)(q.desencolar()); res += nodoActual.dato.toString()+"\n"; if ( nodoActual.izq != null ) q.encolar(nodoActual.izq); if ( nodoActual.der != null ) q.encolar(nodoActual.der); } return res; } } M.Galiano,N.Prieto 11 Ejercicios propuestos: 1. Se desean añadir a la clase ArbolBinario nuevos métodos para realizar las siguientes operaciones: obtener el número de Hojas de un Árbol Binario; obtener el Árbol Binario que es la imagen especular de uno dado; visualizar la información asociada a los Nodos del Nivel k de un Árbol Binario; comprobar si algún Nodo de un Árbol Binario contiene un Objeto dado x; obtener el Nivel en el que está el Nodo que contiene un Objeto dado x. Diséñense como invocaciones a sus homónimos de NodoBinario, estáticos o dinámicos según resulte más conveniente. 2. Modifı́quese el método imprimirPorNiveles() de ArbolBinario para que muestre en lı́neas diferentes los Nodos de cada Nivel. 3. Impleméntense de forma iterativa los métodos imprimirPreOrden(), imprimirInOrden() y imprimirPostOrden() de ArbolBinario. 4. Diséñese un método que obtenga el Padre de un Nodo de un Árbol Binario e indı́quese su coste Temporal ¿Se podrı́a simplificar modificando la estructura de los Nodos? 12