Árboles balanceados Habíamos visto que se puede demostrar por inducción que en un árbol completo el número de nodos totales es (2 ** h+1) –1. Esto nos indica que las cosas que se hacen en árboles son de orden log n pero sólo en el caso en que el árbol esté lleno. En el peor de los casos las cosas pueden ser lineales O(n). ¿Cómo poder garantizar, entonces que se mantenga un balanceo? Cada vez que debido a una inserción o eliminación el árbol se desbalancee, reubicar los nodos de modo que, manteniendo el invariante de un árbol de búsqueda binaria, se trate de mantener la altura del árbol. Ej: El primer tipo de árboles balanceados fue el AVL (Adelson Velskii Landis). No son frecuentemente implementados, ya que hay otros mejores, pero las ideas que hay detrás de ellos se ven en los demás tipos de árboles balanceados. Se trata de incluir otra condición más en el invariante de modo de asegurar que la búsqueda sea O(log n): LO más simple sería requerir que para un árbol AVL la altura de su subárbol derecho sea igual a la de su subárbil izquierdo. Recordemos que los árboles se definen recursivanmete, por lo tanto esto se debería cumplir para cada nodo. Esto es sin embargo muy restrictivo ya que implicaría que todo árbol AVL debería ser completo además. La definición de árboles AVL es entonces algo más relajada: Def: Un árbol AVL es un árbol binario con la propiedad adicional que para cualquier nodo En el árbol la altura de su subárbol izquierdo y de su subárbol derecho difieren a lo más en 1. Esta condición asegura que el árbol sólo tendrá altura logarítmica. Para probar esto necesitamos mostrar que un árbol de altura H tiene por lo menos C**H nodos para alguna constante H>1. En otras palabras, si el mínimo número de nodos en un árbol es exponencial a su altura, entonces la máxima altura de un árbol con N elementos es dada por Log en base C de N. Esto se puede probar con los números de Fibonacci: Sea S(H) un árbol AVL con altura H y con el mínimo de elementos para esa altura. Entonces S(0) = 1 y S(1) = 2. Ahora, por la condición de un árbol AVL sabemos que un árbol AVL mínimo de altura H tiene como hijos uno mínimo de altura H-1 y otro mínimo de altura H-2, ya que el desbalanceo puede ser a lo más de 1. Del dibujo podemos ver que la cantidad de nodos de este árbol es S(H) = S(H-1) + S(H-2) + 1. Ahora los números de Fibonacci eran F(N) = F(N-1)+F(N-2) con F(0) = 1 y F(1) = 1. Corrigiendo: S(H) = F(H+3). Ahora, se sabe que el fibonacci de un número i es alrededor de (K**i)/sqrt(5) con K alrededor de 1.618 (o sea > 1). Consecuentemente un árbol AVL de altura H tiene a lo menos (gruesamente estimando) K**(H+3)/sqrt(5), por lo cual la altura para un árbol AVL mínimo es logarítmica con respecto al número de nodos. Esto implica que las operaciones sobre un árbol AVL están acotadas logaritmicamente. Pero esto no se logra gratis, el costo es la complicación de las operaciones insertar y eliminar ya que estas son las que pueden desbalancear un árbol. Una observación clave es que es que después de una inserción sólo los nodos que estaban en el recorrido desde la raíz hasta el lugar de inserción pueden resultar desblanceados. Al volver recursivamente hacia la raíz después de haber insertado o eliminado un nodo es posible encontrar nodos cuyo nuevo balance viole el principio de árbol AVL. Para hacer más fácil este control, los nodos de un árbol AVL además de tener la información normal (el elemento) tiene además un número de balanceo que es la diferencia de alturas entre el árbol izquierdo y el árbol derecho (altura(izq) – atura(der)). Veamos los casos de inserción que pueden desbalancear un árbol AVL. Un nodo X podria necesitar ser rebalanceado si se inserta un nuevo nodo: 1234- en el subárbol izquierdo del hijo izquierdo de X en el subárbol derecho de hijo izquierdo en el subárbol izquierdo del hijo derecho en el subárbol derecho del hijo derecho Caso 1 Caso 4 COMO ESTÁN LOS INDICADORES DE BALANCE ? Caso 2 Caso 3 COMO ESTÁN LOS INDICADORES DE BALANCE ? En el caso 1 y 4 se habla de un nodo externo causando el desbalanceo. En el caso 2 y 3 de uno interno. Para resolver el caso 1 y 4 de los nodos externos se hace lo que se llama rotación simple (ej pag 513). Static NodoArbol rotHijoIzq(NodoArbol p) { NodoArbol q = p.izq; p.izq = q.der; q.der = p; p.bal = 0; q.bal = 0; return q; } Static NodoArbol rotHijoDer(NodoArbol p) { NodoArbol q = p.der; p.der = q.izq; q.izq = p; p.bal = 0; q.bal = 0; return q; } Para los casos 2 y 3 de los nodos internos se hace la llamada rotación doble (ej pag 515) P R Q Q R D A A B P B C D C Static NodoArbol dobleConIzq(NodoArbol p) { NodoArbol q = p.izq, r = q.der; q.der = r.izq; p.izq = r.der; r.izq = q; r.der = p; if (r.bal > 0) { // B era mas alto que C q.bal = 0; r.bal = 1; p.bal = -1; } } Inserción en AVL. La manera más simple es un algoritmo recursivo: Para insertar un elemento X hacemos un algoritmo recursivo (como el conocido para binarios) que inserta en el lugar preciso. El subárbol formado por el nodo recién creado es 1más alto que el antiguo así que debe informar de esto a su padre para que recalcule el factor de balance. Si al calcularlo el padre ve que se mantiene el balance, está todo en orden pero debe informar a su padre que creció en uno para que este recalcule también. Si en algún momento se ve un desbalance que no se puede tolerar se realiza la rotación necesaria. Desde ese momento no es necesario informar de crecimiento de árbol hacia arriba ya que se volvió la altura original. ARBOLES ROJO-NEGRO Tienen la gracia de que se puede balancear mientras se va hacia abajo (no se necesita el paso de vuelta hacia arriba por lo cual no se necesita hacerlo en forma recursiva). Como resultado la implementación es más simple y rápida que el AVL. Los rojo-negro tienen las siguientes características: 1234- Cada nodo está coloreado rojo o negro La raíz es negra Si un nodo es rojo, los hijos deben ser negros (no se permiten dos rojos seguidos en un path) Cualquier path desde la raíz hasta una referencia null debe contener el mismo número de nodos negros Ver ejemplo en pág 517. Se puede demostrar por inducción que si todo path desde la raíz hasta una referencia null tiene B nodos negros, entonces tiene que haber por lo menos (2**B)-1 nodos negros en el árbol. Más aún, como la raíz es negra y no pueden haber dos nodos consecutivos rojos en el path, la altura de un árbol rojo-negro es a lo más 2*log(N+1) (intercalemos un nodo rojo en alguno de los path cada nodo negro). Consecuentemente, la búsqueda está garantizada de ser logarítmica. Inserción, primera aproximación: botton up Recordemos que todo nodo nuevo se inserta como una hoja en un árbol. Si la coloreamos negra entonces de seguro estaremos violando la propiedad 4 ya que habrá un path a una referencia null con un nodo negra más (en realidad 2). Insertemos la hoja nueva de color rojo. Si el padre era negro está todo bien y hasta aquí llega el problema. Si el padre ya era rojo entonces estamos en problemas, pero con rotaciones y cambios de colores es posible solucionar esto. Hay varios casos a considerar (y sus espejos) si el padre era rojo: Primero consideremos que el tio era negro o null. Si llamamos x al nuevo nodo, p al padre, a al abuelo y t al tio podemos hacer una rotación y un cambio de colores que nos dejará todo en orden de nuevo. Sólo p y x pueden ser rojos en este caso porque si a fuera rojo hubiese una situación no válida ANTES de insertar. Ahora podemos usar la terminología de los árboles AVL y decir que el nodo nuevo x puede ser externo o interno. Si es externo una rotación simple y un cambio de color de p y de a arreglan la cosa: a p p t C D E a A t X X X X X X X X X X X X X X X X X X X X x x B A C B D E Aunque x es una hoja y en ese caso t es null, hemos dibujado un caso más general para usar esto más adelante. Para el caso de una inserción como nodo interno: rotación doble. a x t x B C p a t X X X X X X X X X X A X X X X X X X X X X p A B C D E Antes de continuar tenemos que estar seguros que esto funciona también para el caso general (en que X no es hoja). Primero podemos ver que se mantienen las condiciones de rojo-negro intercalados y también la de los nodos negros en los paths ya que se conservo la cantidad de nodos negros (dos a la derecha, 1 a la izquierda). También se puede ver que las raíces de A,B,C y D deberían haber sido originalmente negras (o null) si todo estaba en orden antes de aparecer X. OK, hasta aquí, pero ¿qué pasa si el tío era rojo? Bueno, se ve que esto también se puede arreglar: a p p t C D E a A t X X X X X X X X X X X X X X X X X X X X x x B A C B D a x t x B p a t X X X X X X X X X X X X X X X X X X X X p A E A B C C D E Aunque todo esto parece funcionar queda una pregunta: ¿qué pasa si el padre de a era originalmente rojo? Bueno, aquí podemos hacer los mismos análisis que hemos hecho hasta ahora. Esta vez sí que tiene sentido tener en cuenta lo del caso general, o sea que estamos tratando con nodos internos. Inserción top-down en árboles rojo-negro Para evitar la necesidad de tener que estar rotando el árbol cuando se vaya hacia arriba de vuelta de haber hecho una inserción podemos usar un truco cuando vayamos hacia abajo de modo de asegurar que cuando se llega al lugar que se debe insertar, el tío nunca será rojo por lo cual bastará con insertar una hoja roja y hacer a lo más una rotación (simple o doble). El procedimiento se basa en lo siguiente: mientras se va hacia abajo, si vemos un nodo x que tiene dos hijos rojos cambiamos x a rojo y hacemos los hijos negros. X X Es claro que el número de nodos negros bajo x permanece inalterado. El problema es que si el padre de x era rojo tenemos dos rojos consecutivos. En teste caso podemos aplicar las rotaciones simple o doble vistas anteriormente, en las cuales había un tío negro, dependiendo de dónde se encuentra x (estas no suben un nodo rojo así que no pará más). Pero qué pasa si el tío es rojo ! ahí si se pasa un nodo rojo hacia arriba !. Esto no puede pasar ya que en el viaje hacia abajo si se encontró que el abuelo tenía dos hijos rojos se cambiaron a negros. Ejemplo: cuadros 18.34 (pág 517) se quiere poner el 45cuando bajamos vemos que el 50 tiene dos hijos rojos. Esto se cambia (ver cuadro 18.39 en pág. 520). Esto produce que el 60 y el 50 queden sucesivamente rojos. Se hace una rotación simple (ver cuadro 18.40 en pagina del lado) y luego se puede poner el 45 sin problemas.