BSTs Definición Un árbol binario de búsqueda (binary sort tree, BST) es una estructura de datos basada en nodos que tiene las siguientes propiedades: El subárbol izquierdo de un nodo sólo contienen nodos con claves menores que la clave del nodo. El subárbol derecho de un nodo sólo contienen nodos con claves mayores que la clave del nodo. Tanto el subárbol derecho como el izquierdo tienen que ser también arboles binarios de búsqueda. Datos a almacenar Generalmente, la información que se inserta en cada nodo es un registro en vez de un único elemento de datos. Esta estructura suele estar compuesta de una clave y de un elemento en sí mismo. La clave se utiliza para comparar y por tanto, tiene que se posible dicha comparación. En el caso del lenguaje de programación java, un nodo de un BST sigue la siguiente definición public class NodoBST { /** * Atributo que representa la clave de un Nodo de un BST */ private Comparable clave; /** * Atributo que representa el objeto del nodo de un BST */ private Object elemento; /** * El hijo derecho de un nodo */ private NodoBST izquierdo; /** * El hijo derecho de un nodo */ private NodoBST derecho; /** * Constructor de la clase nodo * * @param clave * La clave del nodo * @param elemento * El elemento del nodo */ public NodoBST(Comparable clave, Object elemento) { this.clave = clave; this.elemento = elemento; } /** * Método que devuelve la clave de un nodo * * @return La clave del nodo */ public Comparable getClave() { return clave; } /** * Método que modifica la clave de un nodo * * @param clave * La nuevo clave del nodo */ public void setClave(Comparable clave) { this.clave = clave; } /** * Método que devuelve el elemento de un nodo * * @return El elemento de un nodo */ public Object getElemento() { return elemento; } /** * Método que que modifica el elemento del nodo * * @param elemento * El nuevo elemento del nodo */ public void setElemento(Object elemento) { this.elemento = elemento; } /** * Método que devuelve el hijo izquierdo de un nodo * * @return El hijo izquierdo del nodo */ public NodoBST getIzquierdo() { return izquierdo; } /** * Método que modifica el hijo izquierdo de un nodo * * @param izquierdo * El nuevo hijo izquierdo del nodo */ public void setIzquierdo(NodoBST izquierdo) { this.izquierdo = izquierdo; } /** * Método que devuelve el hijo derecho de un nodo * * @return El hijo derecho del nodo */ public NodoBST getDerecho() { return derecho; } /** * Método que modifica el hijo derecho de un nodo * * @param izquierdo * El nuevo hijo derecho del nodo */ public void setDerecho(NodoBST derecho) { this.derecho = derecho; } } La clave es comparable que es una interfaz en el lenguaje java. Un objeto de tipo comparable tiene que implementar el método compareTo que devuelve: >0 si el elemento a comparar es menor que “this” <0 si el elemento a comparar es mayor que “this” 0 si los objetos son iguales. Normalmente, las claves de los nodos BSTs son datos primitivos como int, doublé, etc, que teniendo en cuenta las restricciones de java, han de ser representados por sus clases envoltorio, Integer, Double, etc. Estos tipos de datos son comparables por definición y no es necesario reimplementar su método compareTo. Definición del tipo BST Un BST se define por su nodo raíz y por nada más. A partir de ese nodo, se pueden realizar las operaciones básicas del BST siempre comenzando con el nodo raíz. public class BST { /** * Atributo que representa el elemento raiz del BST */ private NodoBST raiz; /** * Método que permite el acceso al nodo raiz * del BST. Se utilizará para hacer el recorrido * de un árbol desde un main * @return El nodo raiz del BST */ public NodoBST getRaiz() { return raiz; } Operaciones a realizar en un árbol Se pueden realizar las siguientes operaciones en un árbol: 1. Inserción 2. Búsqueda 3. Eliminación Inserción de un elemento en un árbol Para insertar un elemento lo primero que se hace es buscar el sitio para insertar ese elemento. En otras palabras, utilizando la comparación de claves se busca el sitio apropiado para el elemento, que será aquél que sea nulo. Se comienza con la raíz, si el elemento es menor se continua con el sub-árbol izquierdo y si es mayor con el subárbol derecho. Este proceso se repita hasta que se encuentra un sub-árbol que sea nulo y, por consiguiente, el lugar en el que insertar el nodo Por ejemplo, supongamos que tenemos el siguiente árbol: Y queremos insertar el elemento con clave 4. El proceso comenzaría comparando la clave 4 con la clave de la raíz que es 2 y, por tanto, menor que la clave de la raíz. Como la clave a insertar es mayor que la raíz, comparamos con la raíz del sub-árbol derecho, esto es, 5. En este caso, la clave a insertar es menor y, por lo tanto, procesamos el sub-árbol izquierdo. Comparamos la clave 4 con la clave 3 y es mayor por lo que tenemos que procesar el sub-árbol derecho, que no existe. Hemos encontrado la posición del elemento con clave 4. El código java para realizar este proceso es el siguiente: /** * Método que inserta un elemento en el BST (requiere una clave) Es un * método concha * * @param clave * La clave del elemento * @param elemento * El elemento en si mismo */ public void insertar(Comparable clave, Object elemento) { // Devuelve false si se produce error por existir elemento con la misma // clave // Llamada al método recursivo this.raiz = insertarRec(this.raiz, clave, elemento); } /** * Método recursivo que inserta un elemento en el BST (requiere una clave) * * @param actual * El recorrido actual * @param clave * La clave del elemento * @param elemento * El elemento en si mismo * @return false si se produce error por existir elemento con la misma clave */ private NodoBST insertarRec(NodoBST actual, Comparable clave, Object elemento) { // Si hemos llegado a null es que tenemos un hueco para insertar el nodo // ES EL CASO BASE if (actual == null) { // Creamos el nodo actual = new NodoBST(clave, elemento); } else if (actual.getClave().compareTo(clave) < 0) { // Si el recorrido actual es más pequeño que lo que queremos // insertar entonces lo insertaremos por // su hijo derecho actual.setDerecho (insertarRec(actual.getDerecho(), clave, elemento)); } else { // Si el recorrido actual es más grande que lo que queremos insertar // entonces lo insertaremos por // su hijo izquierdo actual.setIzquierdo (insertarRec(actual.getIzquierdo(), clave, elemento)); } return actual; } Búsqueda de un elemento dentro de un árbol Para buscar un elemento lo primero que se hace es buscar su sitio para recuperar ese elemento. En otras palabras, utilizando la comparación de claves se busca el elemento. Se comienza con la raíz, si el elemento es menor se continua con el sub-árbol izquierdo y si es mayor con el subárbol derecho. Este proceso se repita hasta que se encuentra un elemento con clave igual. En ese caso se recupera el elemento del nodo. Por ejemplo, supongamos que tenemos el siguiente árbol: Y queremos buscar el elemento con clave 4. El proceso comenzaría comparando la clave 4 con la clave de la raíz que es 2 y, por tanto, menor que la clave de la raíz. Como la clave a insertar es mayor que la raíz, comparamos con la raíz del sub-árbol derecho, esto es, 5. En este caso, la clave a insertar es menor y, por lo tanto, procesamos el sub-árbol izquierdo. Comparamos la clave 4 con la clave 3 y es mayor por lo que tenemos que procesar el sub-árbol derecho, que contiene el elemento con clave 4. El código java para realizar esta operación es el siguiente /** * Método que devuelve un objeto dada la clave * * @param clave * La clave del objeto a buscar * @return El objeto de la clave */ public Object get(Comparable clave) { // Llamada al método recursivo return getRec(this.raiz, clave); } /** * Método que devuelve un objeto dada la clave. * * @param actual * El recorrido actual * @param clave * La clave del objeto a buscar * @return El objeto de la clave */ private Object getRec(NodoBST actual, Comparable clave) { // Si hemos llegado a null es que no existe ese elemento if (actual == null) { // devolvemos null return null; } // Si no es nulo else { // Comprobamos la comparación int comp = clave.compareTo(actual.getClave()); // Si son iguales es que lo hemos encontrado if (comp == 0) { // devolvemos el objeto del nodo return actual.getElemento(); } else if (comp > 0) { // Si el recorrido actual es más pequeño que lo que queremos // encontrar entonces lo buscaremos por // su hijo derecho return getRec(actual.getDerecho(), clave); } else { // Si el recorrido actual es más grande que lo que queremos // encontrar // entonces lo buscaremos por // su hijo izquierdo return getRec(actual.getIzquierdo(), clave); } } } /** * Método que devuelve un objeto dada la clave * * @param clave * La clave del objeto a buscar * @return El objeto de la clave */ public Object getIter(Comparable obj) { // Empezamos por la raiz NodoBST act = raiz; // Hasta el fin del árbol while (act != null) { // Comprobamos la comparación int comp = obj.compareTo(act.getClave()); // Si son iguales lo devolvemos if (comp == 0) { return act.getElemento(); } // Si es menor vamos por la rama izquierda else if (comp < 0) { act = act.getIzquierdo(); } // Si es mayor vamos por la rama derecha else { act = act.getDerecho(); } } return null; } El método devolverá el objeto con la clave que se busca o null si ese elemento no existe en el BST. Borrado de un elemento dentro de un árbol Para borrar un elemento lo primero que se hace es buscar su sitio para borrar ese elemento. Se utiliza la comparación de claves se busca el elemento. Se comienza con la raíz, si el elemento es menor se continua con el sub-árbol izquierdo y si es mayor con el sub-árbol derecho. Una vez encontrado el elemento podemos encontrar 3 diferentes opciones: 1. Que el nodo no tenga hijos: En este caso eliminamos el nodo sin más operaciones 2. Que el nodo tenga sólo un hijo: En este caso se elimina el nodo y el hijo ocupa su lugar 3. Que tenga ambos hijos: El nodo con el que sustituye el nodo a eliminar puede ser el nodo menor del sub-árbol derecho o el nodo mayor del sub-árbol izquierdo El código java para esta operación es el siguiente. /** * Método para borrar un elemento de un árbol * * @param clave * La clave del elemento a borrar * @return Devuelve si lo ha borrado o no */ public void borrar(Comparable clave) { // Llamada al método recursivo this.raiz = borrarRec(this.raiz, clave); } /** * Método para borrar un elemento de un árbol * * @param actual * El recorrido actual * @param clave * La clave del elemento a borrar * @return Devuelve si lo ha * borrado o no */ private NodoBST borrarRec(NodoBST actual, Comparable clave) { // Obtenemos el valor de la comparación int comp = clave.compareTo(actual.getClave()); // Si es mayor vamos por la derecha if (comp > 0) { actual.setDerecho(borrarRec(actual.getDerecho(), clave)); } // Si es menor vamos por la izquierda else if (comp < 0) { actual.setIzquierdo(borrarRec(actual.getIzquierdo(), clave)); } // Si es igual lo borramos else { // Si no tiene hijos if (actual.getIzquierdo() == null && actual.getDerecho() == null) { actual = null; } // Si tiene hijo izquierdo else if (actual.getDerecho() == null) { actual = actual.getIzquierdo(); } // Si tiene hijo derecho else if (actual.getIzquierdo() == null) { actual = actual.getDerecho(); } // Si tiene ambos hijos else { // Si el sucesor está enseguida if (actual.getDerecho().getIzquierdo() == null) { actual.getDerecho().setIzquierdo(actual.getIzquierdo()); actual = actual.getDerecho(); } else { // Si no recorremos por la rama izquierda del hijo derecho // para encontrar el más a la izquierda // Además, guardamos su padre para hacer las modificaciones // pertinentes NodoBST sucesor, anteriorSucesor = actual.getDerecho(); while (anteriorSucesor.getIzquierdo().getIzquierdo() != null) { anteriorSucesor = anteriorSucesor.getIzquierdo(); } sucesor = anteriorSucesor.getIzquierdo(); anteriorSucesor.setIzquierdo(sucesor.getDerecho()); anteriorSucesor.setIzquierdo(actual.getIzquierdo()); sucesor.setDerecho(actual.getDerecho()); actual = sucesor; } } } return actual; } Recorrido por los elementos de un árbol Además de las operaciones que se han mencionado, se puede recorrer un árbol para realizar un proceso con cada uno de los elementos del mismo. En concreto, existen 3 tipos de recorridos que vamos a ver: Recorrido en pre-orden: Se realiza el proceso de un nodo y luego se visita el hijo izquierdo y el hijo derecho. Código java: /** * Método que recorre en preorden un árbol */ public void recorrerPreOrder() { this.recorrerPreOrderRec(this.raiz); } /** * Método que recorre en preorden un árbol * * @param nodoRecorrido * El nodo actual */ private void recorrerPreOrderRec(NodoBST nodoRecorrido) { if (nodoRecorrido != null) { System.out.println(nodoRecorrido.getElemento()); recorrerPreOrderRec(nodoRecorrido.getIzquierdo()); recorrerPreOrderRec(nodoRecorrido.getDerecho()); } } Recorrido en post-orden: Se visita el hijo izquierdo y el hijo derecho y luego se procesa el nodo. Código java: /** Método que recorre en postorden un árbol */ public void recorrerPostOrder() { this.recorrerPostOrderRec(this.raiz); } /** * Método que recorre en postorden un árbol * * @param nodoRecorrido * El nodo actual */ private void recorrerPostOrderRec(NodoBST nodoRecorrido) { if (nodoRecorrido != null) { recorrerPostOrderRec(nodoRecorrido.getIzquierdo()); recorrerPostOrderRec(nodoRecorrido.getDerecho()); System.out.println(nodoRecorrido.getElemento()); } } Recorrido en in-orden: Se visita el hijo izquierdo, luego se procesa el nodo y, finalmente, se procesa el hijo derecho. Código java: /** * Método que recorre en orden un árbol */ public void recorrerInOrder() { this.recorrerInOrderRec(this.raiz); } /** * Método recursivo que recorre en orden un árbol * * @param nodoRecorrido * El nodo actual */ private void recorrerInOrderRec(NodoBST nodoRecorrido) { if (nodoRecorrido != null) { recorrerInOrderRec(nodoRecorrido.getIzquierdo()); System.out.println(nodoRecorrido.getElemento()); recorrerInOrderRec(nodoRecorrido.getDerecho()); } }