Árboles Las listas enlazadas, pilas y colas son estructuras de datos lineales (es decir, secuencias). Un árbol es una estructura de datos bidimensional no lineal, con propiedades especiales. Los nodos de un árbol contienen dos o más enlaces. En esta sustentación hablaremos sobre los árboles binarios cuyos nodos contienen dos enlaces (uno de los cuales puede ser null). El nodo raíz es el primer nodo en un árbol. Cada enlace en el nodo raíz hace referencia a un hijo. El hijo izquierdo es el primer nodo en el subárbol izquierdo (también conocido como el nodo raíz del subárbol izquierdo), y el hijo derecho es el primer nodo en el subárbol derecho (también conocido como el nodo raíz del subárbol derecho). Los hijos de un nodo específico se llaman hermanos. Un nodo sin hijos se llama nodo hoja. Generalmente, los científicos computacionales dibujan árboles desde el nodo raíz hacia abajo; exactamente lo opuesto a la manera en que crecen los árboles naturales. En nuestro ejemplo de árbol binario crearemos un árbol binario especial, conocido como árbol de búsqueda binaria. Un árbol de búsqueda binaria (sin valores de nodo duplicados) cuenta con la característica de que los valores en cualquier subárbol izquierdo son menores que el valor del nodo padre de ese subárbol, y los valores en cualquier subárbol derecho son mayores que el valor del nodo padre de ese subárbol. La forma del árbol de búsqueda binaria que corresponde a un conjunto de datos puede variar, dependiendo del orden en el que se inserten los valores en el árbol. La aplicación crea un árbol de búsqueda binaria compuesto por valores enteros, y lo recorre (es decir, avanza a través de todos sus nodos) de tres maneras: usando los recorridos inorden, preorden y postorden recursivos. El programa genera 10 números aleatorios e inserta a cada uno de ellos en el árbol. Analicemos el programa del árbol binario. El método main de la clase PruebaArbol, empieza creando una instancia de un objeto Árbol vacío y asigna su referencia a la variable árbol Arbol arbol = new Arbol(); En las líneas for ( int i = 1; i <= 10; i++ ){ valor = numeroAleatorio.nextInt( 100 ); System.out.print( valor + " " ); arbol.insertarNodo( valor ); } // fin de for se generan 10 enteros al azar, cada uno de los cuales se inserta en el árbol binario mediante una llamada al método insertarNodo.Después el programa realiza recorridos preorden, inorden, postorden y demás metodos El método insertarNodo de la clase Arbol public void insertarNodo( int valorInsertar ) { if ( raiz == null ) raiz = new NodoArbol( valorInsertar ); // crea el nodo raíz aquí else raiz.insertar( valorInsertar ); // llama al método insertar } // fin del método insertarNodo Determina primero si el árbol está vacío. De ser así, en la línea raiz = new NodoArbol( valorInsertar ); se asigna un nuevo objeto NodoArbol, se inicializa el nodo con el entero que se insertará en el árbol y se asigna el nuevo nodo a la referencia raíz. Si el árbol no está vacío, en la línea raiz.insertar( valorInsertar ); se hace una llamada al método insertar de NodoArbol. public void insertar( int valorInsertar ){ // inserta en el subárbol izquierdo if ( valorInsertar < datos ) { // inserta nuevo NodoArbol if ( nodoIzq == null ) nodoIzq = new NodoArbol( valorInsertar ); else // continúa recorriendo el subárbol izquierdo nodoIzq.insertar( valorInsertar ); } // fin de if else if ( valorInsertar > datos ) {// inserta en el subárbol derecho // inserta nuevo NodoArbol if ( nodoDer == null ) nodoDer = new NodoArbol( valorInsertar ); else // continúa recorriendo el subárbol derecho nodoDer.insertar( valorInsertar ); } // fin de else if } // fin del método insertar } // fin de la clase NodoArbol Este método utiliza la recursividad para determinar la posición del nuevo nodo en el árbol e inserta el nodo en esa posición. En un árbol de búsqueda binaria, un nodo puede insertarse solamente como nodo hoja. El método insertar de NodoArbol compara el valor a insertar con el valor de datos en el nodo raíz. Si el valor a insertar es menor que los datos del nodo raíz, el programa determina si el subárbol izquierdo está vacío. De ser así, se asigna un nuevo objeto NodoArbol, se inicializa con el entero que se insertará y se asigna el nuevo nodo a la referencia nodoIzquierdo. En caso contrario, se hace una llamada recursiva a insertar para que se inserte el valor en el subárbol izquierdo. Si el valor a insertar es mayor que los datos del nodo raíz, el programa determina si el subárbol derecho está vacío. De ser así, se asigna un nuevo objeto NodoArbol, se inicializa con el entero que se insertará y se asigna el nuevo nodo a la referencia nodoDerecho. En caso contrario, se hace una llamada recursiva a insertar para que se inserte el valor en el subárbol derecho. Si el valorInsertar ya se encuentra en el árbol, simplemente se ignora. Los métodos recorridoInorden, recorridoPreorden y recorridoPostorden llaman a los métodos ayudantes de Árbol llamados ayudanteInorden, ayudantePreorden y ayudantePostorden, respectivamente, para recorrer el árbol e imprimir los valores de los nodos. Los métodos ayudantes en la clase Arbol permiten iniciar un recorrido sin tener que pasar el nodo raíz al método. La referencia raíz es un detalle de implementación que no debe ser accesible para el programador. Los métodos recorridoInorden, recorridoPreorden y recorridoPostorden simplemente toman la referencia privada raiz y la pasan al método ayudante apropiado para iniciar un recorrido del árbol. El caso base para cada método ayudante determina si la referencia que recibe es null y, de ser así, regresa inmediatamente. El método ayudanteInorden define los pasos para un recorrido inorden: 1. Recorrer el subárbol izquierdo con una llamada a ayudanteInorden 2. Procesar el valor en el nodo. 3. Recorrer el subárbol derecho con una llamada a ayudanteInorden. El recorrido inorden no procesa el valor en un nodo sino hasta que se procesan los valores en el subárbol izquierdo de ese nodo. El recorrido inorden de un árbol de búsqueda binaria imprime los valores de los nodos en orden ascendente. El proceso de crear un árbol de búsqueda binaria ordena los datos de antemano; por lo tanto, a este proceso se le conoce como ordenamiento de árbol binario. El método ayudantePreorden define los pasos para un recorrido preorden: 1. Procesar el valor en el nodo 2. Recorrer el subárbol izquierdo con una llamada a ayudantePreorden 3. Recorrer el subárbol derecho con una llamada a ayudantePreorden El recorrido preorden procesa el valor en cada uno de los nodos, a medida que se van visitando. Después de procesar el valor en un nodo dado, el recorrido preorden procesa los valores en el subárbol izquierdo y después los valores en el subárbol derecho. El método ayudantePostorden define los pasos para un recorrido postorden: 1. Recorrer el subárbol izquierdo con una llamada a ayudantePostorden 2. Recorrer el subárbol derecho con una llamada a ayudantePostorden 3. Procesar el valor en el nodo El recorrido postorden procesa el valor en cada nodo después de procesar los valores de todos los hijos de ese nodo. El árbol de búsqueda binaria facilita la eliminación de valores duplicados. Al crear un árbol, la operación de inserción reconoce los intentos de insertar un valor duplicado, ya que éste sigue las mismas decisiones de “ir a la izquierda” o “ir a la derecha” en cada comparación, al igual que el valor original. Por lo tanto, la operación de inserción eventualmente comparará el valor duplicado con un nodo que contenga el mismo valor. En este punto, la operación de inserción puede decidir descartar el valor duplicado (como lo hicimos en esta sustentacion). /** * Universidad del Quindío – Estructuras de Datos – Árbol de búsqueda binaria * @authores * Mauricio Duque * German Ramirez */ public class PruebaArbol { public static void main(String[] args) { Arbol arbol = new Arbol(); int valor; Random numeroAleatorio = new Random(); System.out.println( "Insertando los siguientes valores: " ); // inserta 10 enteros aleatorios de 0 a 99 en arbol for ( int i = 1; i <= 10; i++ ) { valor = numeroAleatorio.nextInt( 100 ); System.out.print( valor + " " ); arbol.insertarNodo( valor ); } // fin de for System.out.println ( "\n\nRecorrido preorden" ); arbol.recorridoPreorden(); // realiza recorrido preorden de arbol System.out.println ( "\n\nRecorrido inorden" ); arbol.recorridoInorden(); // realiza recorrido inorden de arbol System.out.println ( "\n\nRecorrido postorden" ); arbol.recorridoPostorden(); // realiza recorrido postorden de arbol System.out.println ( "\n\nContar Nodos" ); System.out.println( arbol.contarNodos() ); // realiza recorrido Contar nodos System.out.println ( "\n\nContar Hijos" ); System.out.println( arbol.contarHijos() ); // realiza recorrido Contar hijos System.out.println ( "\n\nContar Hermanos" ); System.out.println( arbol.contarHermanos() ); // realiza recorrido Contar hermanos System.out.println ( "\n\nContar Nivel" ); System.out.println( arbol.contarNivel() ); // realiza recorrido Contar nivel System.out.println ( "\n\nContar Raices" ); System.out.println( arbol.contarRaices() ); // realiza recorrido Contar raices System.out.println ( "\n\nContar Hojas" ); System.out.println( arbol.contarHojas() ); // realiza recorrido Contar hojas System.out.println(); } // fin de main } // fin de la clase PruebaArbol -------------------------------------------------------------///////////////////////////////////////////////////////////////////////----------------------------------------------------- public class Arbol { // Atributos private NodoArbol raiz; // el constructor inicializa un Arbol vacio de enteros public Arbol() { raiz = null; } // fin del constructor de Arbol sin argumentos // inserta un nuevo nodo en el arbol de busqueda binaria public void insertarNodo( int valorInsertar ) { if ( raiz == null ) raiz = new NodoArbol( valorInsertar ); // crea el nodo raiz aqui else raiz.insertar( valorInsertar ); // llama al metodo insertar } // fin del metodo insertarNodo // Comienza el recorrido preorden public void recorridoPreorden() { ayudantePreorden( raiz ); } // fin del metodo recorridoPreorden // metodo recursivo para realizar el recorrido preorden private void ayudantePreorden( NodoArbol nodo ) { if ( nodo == null ) //caso base del metodo return; System.out.printf( "%d ", nodo.datos ); // imprime los datos del nodo ayudantePreorden( nodo.nodoIzq ); // recorre el subarbol izquierdo ayudantePreorden( nodo.nodoDer ); // recorre el subarbol derecho } // fin del metodo ayudantePreorden // Comienza recorrido inorden public void recorridoInorden() { ayudanteInorden( raiz ); } // fin del metodo recorridoInorden // metodo recursivo para realizar el recorrido inorden private void ayudanteInorden( NodoArbol nodo ) { if ( nodo == null ) return; ayudanteInorden( nodo.nodoIzq ); // recorre el subarbol izquierdo System.out.printf( "%d ", nodo.datos ); // imprime los datos del nodo ayudanteInorden( nodo.nodoDer ); // recorre el subarbol derecho } // fin del metodo ayudanteInorden // Comienza recorrido postorden public void recorridoPostorden() { ayudantePostorden( raiz ); } // fin del metodo recorridoPostorden // metodo recursivo para realizar el recorrido postorden private void ayudantePostorden( NodoArbol nodo ) { if ( nodo == null ) return; ayudantePostorden( nodo.nodoIzq ); // recorre el subarbol izquierdo ayudantePostorden( nodo.nodoDer ); // recorre el subarbol derecho System.out.printf( "%d ", nodo.datos ); // imprime los datos del nodo } // fin del metodo ayudantePostorden //metodo que permite contar nodos del arbol public int contarNodos() { return ayudanteContarNodos( raiz ); } // fin del metodo contarNodos // metodo recursivo para contar todos los nodos del arbol private int ayudanteContarNodos( NodoArbol nodo ) { if ( nodo == null ) return 0; int hijosIzquierdo = ayudanteContarNodos( nodo.nodoIzq ); int hijosDerecho = ayudanteContarNodos( nodo.nodoDer ); return hijosIzquierdo+hijosDerecho+1; } // fin del metodo ayudanteContarNodos //metodo que permite contar nodos hijos del arbol public int contarHijos() { int nodos = ayudanteContarNodos( raiz ); if( nodos <= 1 ) { return 0; } else { return nodos-1; } } // fin del metodo contarHijos // metodo para contar todos los nodos hermanos del arbol public int contarHermanos() { return ayudanteContarHermanos( raiz ); } // fin del metodo contarHermanos // metodo recursivo que permite contar todos los nodos hermanos del arbol private int ayudanteContarHermanos( NodoArbol nodo ) { if ( nodo == null ) return 0; // recorre el subarbol izquierdo // recorre el subarbol derecho int hermanos = 0; if( nodo.nodoIzq != null && nodo.nodoDer != null ) hermanos += 2; hermanos += ayudanteContarHermanos( nodo.nodoIzq ); hermanos += ayudanteContarHermanos( nodo.nodoDer ); // recorre el subarbol izquierdo // recorre el subarbol derecho return hermanos; } // fin del metodo ayudanteContarHermanos // metodo para contar los niveles del arbol public int contarNivel() { return ayudanteContarNivel( raiz )-1; } // fin del metodo recorridoPostorden // metodo recursivo que permite contar todos los niveles del arbol private int ayudanteContarNivel( NodoArbol nodo ) { if ( nodo == null ) return 0; int nivelIzquierdo = ayudanteContarNivel( nodo.nodoIzq ); int nivelDerecho = ayudanteContarNivel( nodo.nodoDer ); // recorre el subarbol izquierdo // recorre el subarbol derecho if( nivelIzquierdo > nivelDerecho ) return nivelIzquierdo+1; else if( nivelIzquierdo < nivelDerecho ) return nivelDerecho+1; else return nivelDerecho+1; } // fin del metodo ayudanteContarNivel // metodo para contar todos los nodos raiz del arbol public int contarRaices() { return ayudanteContarRaices( raiz ); } // fin del metodo contarRaices // metodo recursivo que permite contar todos los nodos raiz del arbol private int ayudanteContarRaices( NodoArbol nodo ) { if ( nodo == null ) return 0; int raices = 0; if( nodo.nodoIzq != null || nodo.nodoDer != null ) raices ++; raices += ayudanteContarRaices( nodo.nodoIzq ); raices += ayudanteContarRaices( nodo.nodoDer ); return raices; } // fin del metodo ayudanteContarRaices // metodo para contar todos los nodos hojas del arbol public int contarHojas() { return ayudanteContarHojas( raiz ); } // fin del metodo contarHojas // recorre el subarbol izquierdo // recorre el subarbol derecho // metodo recursivo que permite contar todos los nodos hojas del arbol private int ayudanteContarHojas( NodoArbol nodo ) { if ( nodo == null ) return 0; int hojas = 0; if( nodo.nodoIzq == null && nodo.nodoDer == null ) hojas ++; hojas += ayudanteContarHojas( nodo.nodoIzq ); hojas += ayudanteContarHojas( nodo.nodoDer ); // recorre el subarbol izquierdo // recorre el subarbol derecho return hojas; } // fin del metodo ayudanteContarHojas } // fin de la clase Arbol -------------------------------------------------------------///////////////////////////////////////////////////////////////////////----------------------------------------------------public class NodoArbol { // Atributos NodoArbol nodoIzq; // nodo izquierdo int datos; // valor del nodo NodoArbol nodoDer; // nodo derecho // El constructor inicializa los datos y hace de este nodo un nodo raiz public NodoArbol( int datosNodo ){ datos = datosNodo; nodoIzq = nodoDer = null; // el nodo no tiene hijos } // fin del constructor de NodoArbol // localiza el punto de insercion e inserta un nuevo nodo; ignora los valores duplicados public void insertar( int valorInsertar ) { // inserta en el subarbol izquierdo if ( valorInsertar < datos ){ // inserta nuevo NodoArbol if ( nodoIzq == null ) nodoIzq = new NodoArbol( valorInsertar ); else // continua recorriendo el subarbol izquierdo nodoIzq.insertar( valorInsertar ); } // fin de if else if ( valorInsertar > datos ){ // inserta en el subarbol derecho // inserta nuevo NodoArbol if ( nodoDer == null ) nodoDer = new NodoArbol( valorInsertar ); else // continua recorriendo el subarbol derecho nodoDer.insertar( valorInsertar ); } // fin de else if } // fin del metodo insertar } // fin de la clase NodoArbol