Diseño y Análisis de Algoritmos. (DAA-2009) CAPITULO 4 – Estructuras dinámicas de datos Contenidos: 1. Arbol (Tree) 2. Arbol Búsqueda Binaria(ABB), Heap, 3. AVL, 4. Splay Tree, 5. Red-Black Tree, 6. AA-Tree, 7. B-tree, y otros. Dr. Eric Jeltsch F. Introducción En esta sección se presentan los árboles que son un tipo de dato abstracto más adecuado para el tratamiento de grandes cantidades de información, las aplicaciones de los mismos son muy diversas, así por ejemplo, se usan para el almacenamiento y búsqueda de información ya que pueden establecerse diversas formas en las que el tiempo medio de las operaciones de búsqueda es del orden de log(n), lo que es bastante bueno comparado por ejemplo, con las listas enlazadas en la búsqueda de alguna información. Otros ámbitos de aplicación son en la implementación del sistema de archivos en los sistemas operativos, manejo de archivos en las base de datos. En este segmento veremos una diversidad de estructuras de datos dinámicas y propiedades de los árboles Binario, ABB, árboles AVL, Splay, y Red-Black, entre otros. En donde lo sustantivo será el reconocer las estructuras, manejar sus propiedades de “equilibrio” a través de las “rotaciones”, su performance, implementación y aplicaciones. Además, como ya hemos visto, muchas aplicaciones requieren de un conjunto dinámico que soporte solamente las operaciones de Diccionario, a saber, Insert, Search, y Delete. Consideremos en este contexto el problema de cómo recuperar eficientemente el registro de alumnos. Dada una clave buscada K, que corresponde a un alumno en particular y una tabla T, que consiste de 5.000 alumnos. ¿Cómo organizar T, de manera que la búsqueda de la clave K sea eficiente como sea posible? Una forma: usar un registro implementado a través de un array en orden numérico ascendente sobre las claves y usar posteriormente búsqueda binaria para localizar el registro. a) Búsqueda binaria toma aproximadamente (log2 (5001)-1) –comp. (ó 11.3), sobre el promedio, si cada clave es igualmente probable a ser usada. b) Otra posibilidad es usar o dejar los registros en un AVL, de acuerdo al número del alumno. En este caso, sobre el promedio, la búsqueda en un AVL es (log 25000 + 0.25) – comp. (ó 12.5 ). Sin embargo, usando la técnica hashing doble se puede afirmar que almacenando 5000 alumnos en una tabla T que tiene espacio para 6.000 alumnos reduce el nº promedio de comparaciones necesarias para localizar a algún alumno, realizando efectivamente (2.15)-comp. De manera, que la técnica del Hashing permite recuperar la información casi 4 veces más rápido ó eficiente que las otras estructuras. Sin embargo, no todo es del tipo Diccionario lo que se puede hacer con la información. Arbol Un árbol es una estructura no lineal formada por un conjunto de nodos y un conjunto de ramas o arcos. De un punto de vista más técnico digamos que los árboles se basan en el concepto de nodo, que corresponde a cualquier tipo cuyos elementos son registros formados por un campo de datos y un número dado de punteros, tal como se vieron en las listas enlazadas. _____________________________________________________________________________ Escuela de Ingeniería en Computación, Universidad de La Serena. 1 Diseño y Análisis de Algoritmos. (DAA-2009) Mundo Real Mundo Virtual Dr. Eric Jeltsch F. Mundo de la Fantasía Ahora, si los nodos están rotulados con el fin de almacenar un tipo de información, tenemos una gran variedad de árboles. En general, digamos que en un árbol existe un nodo especial denominado raíz. Así mismo, un nodo del que sale alguna rama, recibe el nombre de nodo de bifurcación o nodo rama y un nodo que no tiene ramas recibe el nombre de nodo terminal o nodo hoja, tal como lo muestra la siguiente figura. De un modo más formal, diremos que un árbol es un conjunto finito de uno o más nodos tales que: a) Existe un nodo especial llamado raíz del árbol, y b) los nodos restantes están agrupados en n > 0 conjuntos disjuntos A1, .., An donde cada uno de los cuales es a su vez un árbol que recibe el nombre de subárbol. Evidentemente, la definición dada es recursiva; es decir, hemos definido un árbol como un conjunto de árboles. De la definición se desprende, que cada nodo de un árbol es la raíz de algún subárbol contenido en la totalidad del mismo. El número de ramas de un nodo recibe el nombre de grado del nodo. El nivel de un nodo respecto al nodo raíz se define diciendo que la raíz tiene nivel 0 y cualquier otro nodo tiene un nivel igual a la distancia de ese nodo al nodo raíz. El máximo de los niveles se denomina profundidad o altura del árbol. Es útil limitar los árboles en el sentido de que cada nodo sea a lo sumo de grado 2. De esta forma cabe distinguir entre subárbol izquierdo y subárbol derecho de un nodo. Los árboles así formados, se denominan árboles binarios. Un árbol binario es un conjunto finito de nodos que consta de un nodo raíz que tiene dos subárboles binarios denominados subárbol izquierdo y subárbol derecho. Para motivar su utilidad digamos que los diagramas de Venn y su anidación de paréntesis (A (B (D ( I ), E, F (J, K )), C (G, H ( L )))), puede ser representada a través de árbol. _____________________________________________________________________________ Escuela de Ingeniería en Computación, Universidad de La Serena. 2 Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Una de las aplicaciones típicas de los árboles binario es la representación de expresiones algebraicas, debido a que los operadores que intervienen son operadores binarios. La figura siguiente nos muestra un árbol que corresponde a la expresión aritmética: (a+b*c)/(d-e/f) Una combinación de los dos anteriores surge al considerar (A1 ((A 2 ) ( A3 ))) (((A4 ) (A 5 ))A6 ), que corresponde a la forma óptima de parentizar un producto de matrices de algún orden el que se puede representar por el árbol binario siguiente El árbol binario es una estructura de datos muy útil cuando el tamaño de la estructura no se conoce, y se necesita acceder a sus elementos ordenadamente, la velocidad de búsqueda es importante o el orden en el que se insertan los elementos es casi aleatorio. En definitiva, un árbol binario es una colección de objetos (nodos del árbol) cada uno de los cuales contiene datos o una referencia a los datos, una referencia a su subárbol izquierdo y una referencia a su subárbol derecho. Según lo expuesto, la estructura de datos representativa de un nodo puede ser de la forma siguiente: Si el número de nodos en un árbol de orden t es N, entonces un árbol completo de altura h contiene: h (1) N t i 1 i 1 th 1 t 1 En particular, un árbol binario (t = 2) contiene N 2 h 1 nodos. Esto nos dice que para un árbol binario de altura h = 3, se tienen 7 nodos. Tal como se ve en la siguiente figura, estamos frente a un árbol binario completo y el otro que no lo es _____________________________________________________________________________ Escuela de Ingeniería en Computación, Universidad de La Serena. 3 Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. En general, un árbol del tipo como la figura también es considerado completo, pero no “full”. Mientras que el de enfrente No es completo. Ya que, el sentido de “completitud”, es considerado que se van completando por niveles de arriba hacia abajo y de izquierda a derecha. Clasificación de los árboles A causa del gran significado que poseen los árboles es que se hace necesaria una clasificación, tanto en la forma, como los datos que son almacenados en los árboles, así como en la forma de buscar un tipo de información, y de recorrer los nodos. En general, los datos o información se encuentra en los nodos, de aquí que consideremos en forma particular los árboles binarios y la forma de cómo disponer su información, generando un tipo de árbol llamado árbol de búsqueda binaria. ABB(árboles de búsqueda binaria) Las claves o datos son dispuestas de la siguiente manera: los datos menores a la izquierda y los mayores a la derecha. En la siguiente figura se puede constatar fácilmente la posición en la cual se encuentra algún dato en particular. 40 30 50 20 11 39 24 37 60 44 40 41 45 62 65 Arboles de Decisión(o árbol de búsqueda o de comparaciones) Este tipo de árboles es aplicado a los algoritmos el cual se obtiene al trazar de principio a fin la acción del algoritmo, representando cada comparación como un vértice del árbol. En los árboles binarios se da la situación de dos comparaciones, las que se pueden codificar por: - 0 : Decisión para el hijo izquierdo - 1 : Decisión para el hijo derecho En el ejemplo, se muestra el árbol de comparaciones para la búsqueda secuencial, en donde debemos resaltar los „cajones„ como nodos externos o especiales y los nodos en donde se encuentra la información, generándose así un nuevo tipo de árbol, llamado árbol extendido. _____________________________________________________________________________ Escuela de Ingeniería en Computación, Universidad de La Serena. 4 Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. En este sentido, el árbol de comparaciones cambia radicalmente su rotulación si incorporamos la comparación <, >, o =, tal como se ve en la siguiente figura, cuando n =10, en la cual se combinan dos comparaciones a fin de obtener una comparación de tres vías para cada paso, así el árbol se ve más compacto, en comparación con el árbol que considera las comparaciones a la izquierda y > a la derecha. Supongamos que deseamos ordenar tres datos A, B y C. La siguiente figura muestra un árbol de decisión posible para resolver este problema. Los nodos internos del árbol representan comparaciones y los nodos externos representan salidas emitidas por el programa. Todo árbol de decisión con H hojas tiene al menos altura log2 H, y la altura del árbol de decisión es igual al número de comparaciones que se efectúan en el peor caso. En un árbol de decisión para ordenar n datos se tiene que H=n!, y por lo tanto se tiene que todo algoritmo que ordene n datos mediante comparaciones entre llaves debe hacer al menos log2 n! comparaciones en el peor caso. Usando la aproximación de Stirling, se puede demostrar que log2 n! = n log2 n + O(n), por lo cual la cota inferior es de O(n log n). Basado en la forma de rotular los nodos, podríamos clasificar los árboles en árboles orientados a los nodos y árboles orientados a las hojas, para distinguirlos digamos que los primeros son árboles en _____________________________________________________________________________ Escuela de Ingeniería en Computación, Universidad de La Serena. 5 Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. donde los datos se encuentran en los nodos del árbol, mientras que los otros son árboles en donde los datos se encuentran solamente en las hojas. Convengamos que, tal como la figura anterior si el árbol es ampliado con nodos especiales, de manera que todos los subárboles están completos, es decir todos los apuntadores sin nodos descendientes apuntan a un nodo especial, se habla de un árbol extendido. En este contexto, se define la longitud de camino X como el número de arcos que deben ser recorridos para llegar desde la raíz al nodo X. por definición la raíz tiene longitud de camino 1, sus descendientes directos 2… Por otra parte, la longitud de camino interno de un árbol, es la suma de todas las longitudes de trayectoria de sus nodos, y se calcula por medio de la siguiente fórmula. h LCI ni * i i 1 donde i = nivel del árbol, h = altura del árbol, ni = número de nodos en el nivel i. Por otra parte, la media de la longitud de camino, se calcula por la formula LCI /N, donde N es el número de nodos. En este grafo el LCI = 1*1 + 2*2 + 5*3 + 4*4 = 36. Otra forma de clasificación es considerar árboles optimal estáticos u optimal dinámicos, los primeros significa que el árbol debe ser construido nuevamente, mientras que el otro se construye durante el ingreso o al agregar los datos. El objetivo final en ambos casos es lograr una razonable estructura de almacenamiento, aunque esta situación en general globalmente no se pueda lograr, pero sí localmente. En ambos casos, se evitan árboles degenerados, que son los árboles que degeneran en listas lineales o ramas, de ahí que surjan las “rotaciones” como una forma de evitar este tipo de situaciones. Los árboles sirven también para representar una jerarquía, tal como lo muestra el siguiente ejemplo, respecto de la representación de expresiones aritméticas. Por ejemplo, para la expresión (A B / C) ( D E F) se puede representar por el siguiente árbol: * + - A / B D C * E F Relación de Orden y Representación En cada nodo, se da una situación de orientación y jerarquerización en los árboles binarios: Toda clave al lado derecho(izq) de los subarboles son mayores(menores) a la clave del nodo. Con ayuda _____________________________________________________________________________ Escuela de Ingeniería en Computación, Universidad de La Serena. 6 Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. de esta relación de orden se generan árbol que sirven para buscar, borrar o hallar algún elemento en particular. La búsqueda de un elemento se realiza desde la raíz hasta el nodo en donde se encuentra la clave, en caso de existir, en caso contrario no existe el dato. 1. Las dos claves son iguales, la que se busca y la que tiene el nodo: el elemento es encontrado. 2. La clave buscada es pequeña: el elemento buscado se encuentra solamente en los subarboles izquierdo. 3. La clave buscada es mayor: el elemento buscado se encuentra solamente en los subarboles derecho. Este proceso se realiza hasta que la clave es encontrada. Notar que la estructura y crecimiento de los árboles binarios son a través de una relación de orden, por tal motivo se generan varios árboles con rotulaciones distintas. Por ejemplo, se dan las 3 claves 1, 2 y 3, tan solo con estas podemos generar distintos árboles binarios respetuosos del orden antes descrito, por ejemplo existiendo 6 distintas formas de rotularlos y por ende 6 árboles binarios distintos. En general, se demuestra que n-elementos generan n! formas. _____________________________________________________________________________ Escuela de Ingeniería en Computación, Universidad de La Serena. 7 Diseño y Análisis de Algoritmos. (DAA-2009) Operaciones Dr. Eric Jeltsch F. Veamos como generar un árbol de búsqueda binaria a partir del árbol vacío, al cual se le agregan los datos (12, 7, 15, 5, 13). Inserción en un ABB 1) comparar la clave a insertar con la raíz del árbol. Si es mayor, debe avanzar hacia el subarbol derecho. Si es menor, debe avanzar hacia el subarbol izquierdo. Repetir sucesivamente 1), hasta que se cumpla alguna de las siguientes condiciones: a) el subarbol derecho es vacío, o el subarbol izquierdo es vacío; en cuyo caso se procede a insertar el elemento en el lugar que le corresponda. b) la clave que se quiere insertar es igual a la raíz del árbol; en cuyo caso no se realiza la inserción. Por ejemplo, insertar 120, 87 y 130, en ese orden a partir del árbol vacío. Eliminación en un ABB Se refiere a eliminar un Nodo con una determinada clave, suponiendo que el elemento ha sido encontrado. Existen varias situaciones, entre ellas están: a) Si el elemento a borrar es terminal u hoja, simplemente se elimina. b) Si el elemento a borrar tiene un solo descendiente, entonces tiene que sustituirse por ese descendiente. _____________________________________________________________________________ Escuela de Ingeniería en Computación, Universidad de La Serena. 8 Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. c) Si el elemento a borrar tiene los 2 descendientes, entonces se tiene que sustituir por el nodo que se encuentra más a la izquierda en el subárbol derecho o por el nodo que se encuentra más a la derecha en el subárbol izquierdo. Ejemplo: Veamos como va quedando el ABB, luego de realizar una serie de eliminaciones. Para cuando se eliminen los valores 2 y 6. Se tiene (caso a) _____________________________________________________________________________ Escuela de Ingeniería en Computación, Universidad de La Serena. 9 Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. a) 13(caso b) d) 15 (caso b) e) 5 (caso a) f) 12 (caso c) Dado el árbol búsqueda binaria. Se desea eliminar el 12, ¿como queda el ABB.? _____________________________________________________________________________ 10 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Recorrido y orden en Arboles Binario El principio de recorrer un árbol binario, determina un orden sobre el conjunto de nodos. Existen 3 posibilidades o principios de como recorrer un árbol binario, ellos son la lectura en Inorden, Preorden y Posorden. Inorden Orden IRD (1) Recorrer el subárbol izquierdo en INORDEN (2) Visitar la raíz (3) Recorrer el subárbol derecho en INORDEN Orden DRI (1) Recorrer el subárbol derecho en INORDEN (2) Visitar la raíz (3) Recorrer los subárbol derecho en INORDEN Los orden IRD y DRI son uno inverso del otro. El orden IRD, se llama orden simétrico Preorden Orden RID (1) Visitar la raíz (2) Recorrer el subárbol izquierdo en PREORDEN (3) Recorrer el subárbol derecho en PREORDEN Orden RDI (1) Visitar la raíz (2) Recorrer el subárbol derecho en PREORDEN (3) Recorrer el subárbol izquierdo en PREORDEN En general se visita la raíz antes de los dos subárboles. Posorden Orden IDR (1) Recorrer el subárbol izquierdo en POSTORDEN (2) Recorrer el subárbol derecho en POSTORDEN _____________________________________________________________________________ 11 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) (3) Visitar la raíz Dr. Eric Jeltsch F. Primero se visitan los subárboles y luego la raíz. Orden DIR (1) Recorrer el subárbol derecho en POSTORDEN (2) Recorrer el subárbol izquierdo en POSTORDEN (3) Visitar la raíz Primero se visitan los subárboles y luego la raíz. Por ejemplo, se dan una serie de árboles binarios, y la idea es describir su recorrido en las formas antes definidas. 1.- 2. Este ejemplo nos muestra una estructura de árbol (representación de una estructura jerarquica de una expresión aritmética). Esta representación „arbórea“ es en particular muy útil para la traducción de una expresión en lenguaje de máquina. Desde la estructura anterios se pueden representar fácilmente las distintas formas de una expresión aritmética. Entregando de esta manera el recorrido en "Posorden" como la notación Postfija, y en "Preorden" la notación Prefija". 3. _____________________________________________________________________________ 12 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. 4. Aplicaciones de los recorridos Con ayuda de los recorridos antes descritos se pueden determinar algunas otras operaciones sobre los árboles. Por ejemplo, determinar el número de hojas en el árbol, entregar la altura del árbol, copiar el árbol, borrar el árbol, descripción gráfica de un árbol de búsqueda binaria. En un árbol de búsqueda binario se puede dar la siguiente situación, la que se interpreta como un árbol de búsqueda binario que degenero en una lista lineal, derivando en que la búsqueda de algún elemento en particular resulta tan costoso como buscarlo en forma exhaustiva, de aquí que es importante evitar que se genere una situación de este tipo. Para ello, están los „árboles perfectamente balanceados“ que evitan que se de una situación como la descrita a continuación, de manera de obtener una forma de balanceo que en definitiva facilita la búsqueda de algún elemento, pues no se encuentra a una profundidad tan alejado de la raíz. Se verifica que el árbol de la derecha es árbol AVL, que luego lo veremos en detalle. A continuación se muestra un simple algoritmo que genera un árbol con las condiciones de balanceo. (1) ordenar las claves en una sucesión ordenada en forma creciente (2) es conocido el número de Objetos (claves) que se deben tener. _____________________________________________________________________________ 13 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Se consideran dos tipos de recorrido: recorrido en profundidad y recorrido en anchura o a nivel. Puesto que los árboles no son secuenciales como las listas, hay que buscar estrategias alternativas para visitar todos los nodos. Dada la siguiente figura: Figura 1 - Recorridos en profundidad: Recorrido en preorden: consiste en visitar el nodo actual (visitar puede ser simplemente mostrar la clave del nodo por pantalla), y después visitar el subárbol izquierdo y una vez visitado, visitar el subárbol derecho. Es un proceso recursivo por naturaleza. Si se hace el recorrido en preorden del árbol de la figura 1 las visitas serían en el orden siguiente: a,b,d,c,e,f. Recorrido en inorden u orden central: se visita el subárbol izquierdo, el nodo actual, y después se visita el subárbol derecho. En el ejemplo de la figura 1 las visitas serían en este orden: b,d,a,e,c,f. private void printTree(Nodo_ABB b) { if(b != null) { printTree(b.izq); System.out.print(b.dat); System.out.print(' '); printTree( b.der ); } } Recorrido en postorden: se visitan primero el subárbol izquierdo, después el subárbol derecho, y por último el nodo actual. En el ejemplo de la figura 1 el recorrido quedaría así: d,b,e,f,c,a. La ventaja del recorrido en postorden es que permite borrar el árbol de forma consistente. Es decir, si visitar se traduce por borrar el nodo actual, al ejecutar este recorrido se borrará el árbol o subárbol que se pasa como parámetro. La razón para hacer esto es que no se debe borrar un nodo y después sus subárboles, porque al borrarlo se pueden perder los enlaces, y aunque no se perdieran se rompe con la regla de manipular una estructura de datos inexistente. Una alternativa es utilizar una variable auxiliar, pero es innecesario aplicando este recorrido. - Recorrido en amplitud: _____________________________________________________________________________ 14 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Consiste en ir visitando el árbol por niveles. Primero se visitan los nodos de nivel 1 (como mucho hay uno, la raíz), después los nodos de nivel 2, así hasta que ya no queden más. Si se hace el recorrido en amplitud del árbol de la figura una visitaría los nodos en este orden: a,b,c,d,e,f. Construcción de un árbol binario Hasta el momento se ha visto la declaración y recorrido de un árbol binario. Sin embargo no se ha estudiado ningún método para crearlos. A continuación se estudia un método para crear un árbol binario que no tenga claves repetidas partiendo de su recorrido en preorden e inorden, almacenados en sendos arrays. Antes de explicarlo se recomienda al lector que lo intente hacer por su cuenta, es sencillo cuando uno es capaz de construir el árbol viendo sus recorridos pero sin haber visto el árbol terminado. Partiendo de los recorridos preorden e inorden del árbol de la figura 1 puede determinarse que la raíz es el primer elemento del recorrido en preorden. Ese elemento se busca en el array inorden. Los elementos en el array inorden entre izq y la raíz forman el subárbol izquierdo. Asimismo los elementos entre der y la raíz forman el subárbol derecho. Por tanto se tiene este árbol: A continuación comienza un proceso recursivo. Se procede a crear el subárbol izquierdo, cuyo tamaño está limitado por los índices izq y der. La siguiente posición en el recorrido en preorden es la raíz de este subárbol. Queda esto: El subárbol b tiene un subárbol derecho, que no tiene ningún descendiente, tal y como indican los índices izq y der. Se ha obtenido el subárbol izquierdo completo de la raíz a, puesto que b no tiene subárbol izquierdo: _____________________________________________________________________________ 15 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Después seguirá construyéndose el subárbol derecho a partir de la raíz a. Aplicación: Se tiene un archivo de texto ASCII. Para este propósito puede servir cualquier libro electrónico de la librería Gutenberg o Cervantes, que suelen tener varios cientos de miles de palabras. El objetivo es clasificar todas las palabras, es decir, determinar que palabras aparecen, y cuantas veces aparece cada una. Palabras como 'niño'-'niña', 'vengo'-'vienes' etc, se consideran diferentes para simplificar el problema. Se pide, escribir un programa, que recibiendo como entrada un texto, realice la clasificación descrita anteriormente. Nótese que el empleo de una lista enlazada ordenada no es una buena solución. Si se obtienen hasta 20.000 palabras diferentes, por decir un número, localizar una palabra cualquiera puede ser, y en general lo será, muy costoso en tiempo. Se puede hacer una implementación por pura curiosidad para evaluar el tiempo de ejecución, pero no merece la pena. La solución pasa por emplear un árbol binario de búsqueda para insertar las claves. El valor de log(20.000) es aproximadamente de 14. Eso quiere decir que localizar una palabra entre 20.000 llevaría en el peor caso unos 14 accesos. El contraste con el empleo de una lista es simplemente abismal. Por supuesto, como se ha comentado anteriormente el árbol no va a estar perfectamente equilibrado, pero nadie escribe novelas manteniendo el orden lexicográfico (como un diccionario) entre las palabras, asi que no se obtendrá nunca un árbol muy degenerado. Lo que está claro es que cualquier evolución del árbol siempre será mejor que el empleo de una lista. Implementación en Java Por último, veamos un ejemplo o propuesta de implementación de los árboles ABB. Se muestra una implementación (No genérica, es decir no usa “Generics”), pero si usa la interface Comparable y otras que vimos en programación orientada a objetos. La propuesta se muestra en dos archivos, uno de ellos es Arbol_ABB.java y el otro Arbol_ABBTest.java. class Nodo_ABB { // Instancia (variables) protected Nodo_ABB izq; protected Nodo_ABB der; public Comparable dat; // sub arbol izq // sub arbol derecho // datos de los nodos // Constructor public Nodo_ABB(Comparable datElement) _____________________________________________________________________________ 16 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. { this(datElement, null, null ); } public Nodo_ABB(Comparable datElement, Nodo_ABB i, Nodo_ABB d) { dat = datElement; izq = i; der = d; } public Nodo_ABB getIzq() { return izq; } public Nodo_ABB getDer() { return der; } } public class Arbol_ABB { /* la raíz del árbol */ private Nodo_ABB root; /* * Constructor. */ public Arbol_ABB() { root = null; } /* * agregar un elemento en el ABB * los valores duplicados son ignorados */ public void insert( Comparable x ) { root = insert(x, root); } /* * eliminar de un ABB. En caso que no este, no pasa nada */ public void remove( Comparable x ) { root = remove(x, root); } /* * hallar el menor elemento del ABB */ public Comparable findMin() { return elementAt(findMin(root)); } /* * hallar el más grande elemento en el ABB */ public Comparable findMax() { return elementAt(findMax(root)); } /* * hallar un elemento (dato) en el ABB */ _____________________________________________________________________________ 17 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. public Comparable find(Comparable x) { return elementAt(find(x, root)); } /* * Hacer el ABB vacío */ public void makeEmpty() { root = null; } /* * Test, para saber si el ABB esta vacio */ public boolean isEmpty() { return root == null; } /* * salida de los elementos en orden */ public void printTree() { if( isEmpty( ) ) System.out.println( "ABB esta vacio" ); else printTree( root ); } /* * salida de los elementos, pero deben interpretarse (90 Grad rotados) */ public void salidaABB() { if( isEmpty() ) System.out.println( "ABB vacio" ); else salidaABB( root,0 ); } private Comparable elementAt( Nodo_ABB b ) { return b == null ? null : b.dat; } //insertar private Nodo_ABB insert(Comparable x, Nodo_ABB b) { if( b == null ) b = new Nodo_ABB( x, null, null ); else if( x.compareTo( b.dat ) < 0 ) b.izq = insert( x, b.izq ); else if( x.compareTo( b.dat ) > 0 ) b.der = insert( x, b.der ); else ; // Duplicado, no hace nada return b; } //eliminar private Nodo_ABB remove(Comparable x, Nodo_ABB b) { if( b == null ) return b; // no encontrado, no hace nada. if( x.compareTo(b.dat) < 0 ) b.izq = remove(x, b.izq ); else if( x.compareTo(b.dat) > 0 ) _____________________________________________________________________________ 18 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. b.der = remove( x, b.der ); else if( b.izq != null && b.der != null ) // dos hijos { b.dat = findMin(b.der).dat; b.der = remove(b.dat, b.der); } else b = ( b.izq != null ) ? b.izq : b.der; return b; } //hallar el min private Nodo_ABB findMin(Nodo_ABB b) { if (b == null) return null; else if( b.izq == null) return b; return findMin(b.izq ); } //hallar el max private Nodo_ABB findMax( Nodo_ABB b) { if( b != null ) while( b.der != null ) b = b.der; return b; } //buscar un elemento private Nodo_ABB find(Comparable x, Nodo_ABB b) { if(b == null) return null; if( x.compareTo(b.dat ) < 0) return find(x, b.izq); else if( x.compareTo(b.dat) > 0) return find(x, b.der); else return b; // hallado! } //imprimir de una forma .. inorden private void printTree(Nodo_ABB b) { if(b != null) { printTree(b.izq); System.out.print(b.dat); System.out.print(' '); printTree( b.der ); } } /* * salida, pero el arbol esta rotado en 90 Grad.(puede mejorarse) */ private void salidaABB(Nodo_ABB b, int paso) { if (b != null) { salidaABB(b.izq, paso + 1); for (int i = 0; i < paso; i++) { System.out.print(' '); } _____________________________________________________________________________ 19 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. System.out.println(b.dat); salidaABB(b.der, paso + 1); } } } public class Arbol_ABB_Test { // Test o pruebas public static void main( String [] args ) { // Test Nr. 1 Arbol_ABB b = new Arbol_ABB(); final int NUMERO = 4000; final int ESPACIO = 37; System.out.println( "Probando...."); for( int i = ESPACIO; i != 0; i = (i + ESPACIO) % NUMERO) b.insert( new Integer( i ) ); for(int i = 1; i < NUMERO; i+= 2 ) b.remove( new Integer( i ) ); if (NUMERO < 40) b.printTree( ); if( ((Integer)(b.findMin( ))).intValue( ) != 2 || ((Integer)(b.findMax( ))).intValue( ) != NUMERO - 2 ) System.out.println( "fallas en FindMin o FindMax !" ); for( int i = 2; i < NUMERO; i+=2 ) if( ((Integer)(b.find( new Integer( i ) ))).intValue( ) != i ) System.out.println( "hallar fallas!" ); for( int i = 1; i < NUMERO; i+=2 ) { if( b.find( new Integer( i ) ) != null ) System.out.println( "encontrar error!" ); } // Test Nr.2 Arbol_ABB b1 = new Arbol_ABB(); for (int i = 0; i < 10; i++) { // genera nº entre 0 y 100 Integer r = new Integer((int)(Math.random()*100)); b1.insert(r); } System.out.println("recorrido en Inorden"); b1.printTree(); System.out.println(); System.out.println("representacion del arbol, rotar en 90 Grados"); b1.salidaABB(); System.out.print("el menor valor: "); System.out.print(((Integer)(b1.findMin())).intValue()); System.out.println(); System.out.print("el mayor valor: "); System.out.print(((Integer)(b1.findMax())).intValue()); System.out.println(); for (int i = 0; i < 10; i++) { // genera nº entre 0 y 100 Integer r = new Integer((int)(Math.random()*100)); if ( b1.find(r) != null ) { b1.remove( r ); } // else System.out.println(r.intValue() + " no encontrado."); _____________________________________________________________________________ 20 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. } b1.salidaABB(); // Test Nr. 3 Arbol_ABB b2 = new Arbol_ABB(); for (int i = 0; i < 20; i++) { String s = "nº random " + (int)(Math.random() * 100); b2.insert(s); } b2.printTree(); // aparecen ordenados } } Arboles AVL Introducción: Una de las primeras estructuras de datos de árboles de búsqueda “equilibrados” que consideraremos son los árboles AVL ( que llevan el nombre de sus autores Adelson-VelskiiLandis), existen otros tipos de estructuras similares, tales como los árboles Red-Black y otros. En este informe se presenta la definición de los árboles AVL, con ejemplos. Se hace una estimación del número de nodos en el peor árbol AVL de altura h. Se revisa, en particular, el método de Insertar, con ejemplos. Además se muestra la implementación en Java de cada uno de los componentes, tales como Nodo, Arbol AVL y las Rotaciones, como el medio para lograr la implementación de Inserción en los árboles AVL. Finalmente, se hace una simulación de la implementación y se interpreta la salida, junto con mencionar algunas mejoras, u otras especificaciones que se le podrían realizar, como por ejemplo considerar que se ingresan string y no enteros, que son los datos en que se basa la presente implementación. Lo substancial de este informe se centra en poder superar el problema generado por la inserción en los ABB que podían degenerar en una lista proporcional a los datos ingresados y por otra reforzar los conceptos adquiridos en Estructuras de Datos. Definición: Un árbol AVL es un árbol binario de búsqueda en el que las alturas de los subarboles izquierdos y derecho de cualquier nodo difieren a lo sumo en 1. Esta restricción impuesta sobre la altura de los subarboles de un árbol AVL se le conoce como “propiedad de los árboles AVL”, y debe ser cumplida por todos y cada uno de los nodos del árbol. _____________________________________________________________________________ 21 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Ejemplo: Los tres árboles que se muestran son del tipo AVL, mientras que los de enfrente no lo son. Ejemplo: Basado en que los árboles AVL son árboles binarios es que a partir de un árbol vacío se han insertado los datos originando el árbol de la Fig. 1, el cual deja de tener la propiedad AVL. Pues, aunque los nodos 2, 4, 10, la posean considerando sus respectivos sub-arboles, notamos que en este caso la raíz, es decir el nodo 8 no posee la propiedad de los árboles AVL pues la altura del subárbol izquierdo no difiere en a lo sumo 1 con el subárbol derecho, (en particular el subárbol izquierdo tiene altura 3, mientras que el subárbol derecho tiene altura 1. ( De allí la marca que se considera como –2 bajo el nodo 8 y –1 bajo el nodo 4. Los valores –2 y –1 se conocen como "Balance" de los nodos 8 y 4 respectivamente. H registra los valores de las alturas de los subárboles, los que deben ser -1 (balance_izq), 0 (balanceado) y +1 (balance_der). En este otro caso, todos los nodos satisfacen la condición de balance, de manera que es un árbol AVL, como muestra Fig. 2 Sea T(h) cualquier árbol AVL que contenga N(h) nodos, con h>=2. Como T(h) es un árbol AVL, y supongamos que tenga el menor número de nodos, entonces uno de los subárboles de la raíz debe tener altura h-1 y el otro deberá tener altura h-2, de manera que el número de nodos de un árbol AVL en el caso peor de altura h viene dado por: (Ver (2), para mayor información) _____________________________________________________________________________ 22 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. N(h) = 1 + N(h-1) + N(h-2). Si suponemos que N(h) = 0 cuando h <0, entonces los primeros valores de esta relación de recurrencia son: 1, 2, 4, 7, 12, 20, ...etc. En general, N(h) = F(h+3) – 1, donde F(i) es el i-ésimo número de Fibonacci. (Ver (4), pág.36-37). Tomando logaritmo en base (1 + 5)/2), se tiene que h = log (1 + 5)/2) N(h) –3. Como N(h) es el número de nodos en el peor árbol AVL de altura h, se desprende del análisi anteriorque la altura de cualquier árbol AVL de n-nodos es O(log(n)). Métodos de Consulta Al igual que como se hizo para los árboles de búsqueda binaria podemos realizar consultas en un árbol AVL. A saber, Insertar y Eliminar, y otros métodos que no resultan tan directos como en los árboles de búsqueda binaria, por el problema de balanceo que se genera. Ejemplos: Dado el siguiente árbol T de búsqueda binaria 1) En este árbol, son insertados los nodos con claves 9 y 11. Generándose el árbol T1 : 2) Basado en T, son insertados los nodos con claves 1, 3, 5 y 7. Generándose el árbol T2: _____________________________________________________________________________ 23 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Ya al insertar la clave „1“ el árbol pierde la propiedad de AVL.. De aquí el aplicar una doble rotación. Arbol obtenido luego de insertar las claves 3, 5 y 7. La propiedad de AVL se pierde si H = +2 resp. -2. Esto se remedia mediante rotaciones. Rotaciones a) Los árboles de Fig. 7 contienen los mismos elementos y son ambos árboles de búsqueda binaria. Primero, en ambos casos k1 < k2, segundo, todos los elementos en los subárboles X son menores que k1 en ambos árboles, tercero, todos los elementos en el subárbol Z son mayores que k2. Finalmente todos los elementos en el subárbol Y están entre k1 y k2. La conversión de uno de ellos al otro se conoce como „Rotación simple“, que significa en lo substancial cambiar la estructura del árbol. La figura 7 muestra las variantes simétricas y la doble rotación. Fig. 7 Representación en Java Un árbol AVL se representa de la misma manera que un árbol binario de búsqueda, esto es con nodos que contienen punteros a su padre y a sus hijos izquierdo y derecho, sin embargo, un nodo ahora debe almacenar un campo adicional que indica la altura o balance del nodo. // Descripción de un nodo para un árbol AVL class Nodo_Avl { // Instancias _____________________________________________________________________________ 24 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) protected Nodo_Avl izq; protected Nodo_Avl der; protected int altura; public Comparable datos; Dr. Eric Jeltsch F. // hijo Izquierdo // hijo derecho // altura // elementos // Constructores public Nodo_Avl(Comparable datElem) { this(datElem, null, null ); } public Nodo_Avl( Comparable datElem, Nodo_Avl ib, Nodo_Avl db ) { datos = datElem; izq = ib; der = db; balance = 0; } } /* Este método puede ser llamado solamente si k2 tiene un hijo izquierdo, realizando una rotación entre el nodo k2, tal como lo muestra la figura 7. Además, actualiza la altura, asignando la nueva raíz a k2. */ private static Nodo_Avl RotacionSimpleIzq(Nodo_Avl k2) { Nodo_Avl k1 = k2.izq; k2.izq = k1.der; k1.der = k2; k2.altura = max( altura( k2.izq ), altura( k2.der ) ) + 1; k1.altura = max( altura( k1.izq ), k2.altura ) + 1; return k1; } b) Existen situaciones en donde el desbalanceo es generado por un nodo que es insertado en el árbol que está contenido en el subárbol de el medio( es decir Y) y que al mismo tiempo como los otros arboles tienen idéntica altura. El caso es fácil de chequear y la solución es llamada “Rotación Doble”, la cual es muy similar a la rotación simple salvo que ahora se ven involucrados 4 subárboles en vez de 3. Fig.8: Rotación Izq-Der, Rotación Doble y en forma similar Rotación Der-Izq, Rotación Doble. /* Rotación Doble, basada en Fig. 8: Este método solo puede ser usado si k3 tiene hijo izquierdo y los hijos de k3 tienen hijo derecho. Esta rotación se conoce como rotación izq-der. Actualiza la altura, y su raíz. */ private static Nodo_Avl DobleRotacionIzq_Der(Nodo_Avl k3) { /* Rotación entre k1 y k2*/ k3.izq = RotationSimpleIzq( k3.izq); return RotationSimpleDer( k3 ); } _____________________________________________________________________________ 25 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Ejemplo: Dr. Eric Jeltsch F. Rot.der-izq En este último árbol se inserta la clave 13, quedando Rot.der-izq Ud. podrá verificar que cualquier desbalanceo causado por una inserción en un árbol AVL puede ser realizada por una Rotación Doble o Simple, (Ver (1)). Ahora, respecto a la eficiencia de esta TDA mencionemos que almacenar la información de la altura, que en este caso son suficientes con +1, 0 y –1, es de gran utilidad /* Método para calcular la altura de un nodo en un árbol AVL. */ private static int altura( Nodo_Avl b) { return b == null ? -1 : b.altura; } Entonces recordemos que para Insertar un nodo con la clave „x“ en un árbol AVL, el valor „x“ se inserta recursivamente en el subarbol correspondiente, tal como en los árboles de búsqueda binario. En el caso que la altura del subárbol no cambie, la inserción concluye. En caso contrario es necesario utilizar según sea el caso, Rotación Simple o Rotación Doble. Implementación de los árboles AVL //En archivo Arbol_AvlTest.java public class Arbol_AvlTest { // Programa Test public static void main(String [] args) _____________________________________________________________________________ 26 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. { Arbol_Avl b1 = new Arbol_Avl(); Arbol_Avl b2 = new Arbol_Avl(); for (int i = 0; i < 7; i++) // { Integer r = new Integer(i); b1.insertar(r); } System.out.println("Arbol girado en 90 grados"); b1.salidaArbolBinario(); for (int i = 0; i < 10; i++) { // Genera un número entre 0 y 100 Integer r = new Integer((int)(Math.random()*100)); b2.insertar(r); } System.out.println("Arbol girado en 90 grados"); b2.salidaArbolBinario(); System.out.println("Travesia en Inorden(Izq-Raiz-Der)"); b2.printArbol(); } } //En archivo Arbol_Avl.java /* * Comparaciones se basan en el método compareTo.(REPASAR lo visto en POO) */ public class Arbol_Avl { /* Raiz del Arbol */ private Nodo_Avl raiz; /* * Constructor por defecto */ public Arbol_Avl( ) { raiz = null; } /* * Insertar: Duplicados son ignorados. * x es el dato a ser insertado. */ public void insertar(Comparable x ) { raiz = insertar( x, raiz ); } /* * Eliminar un nodo del Arbol. Caso que x no este, * nada ocurre. * Si x esta, es eliminado. */ //no esta la implementación......(Tarea) /* * Determinar el elemento más pequeño en el arbol.. * Devuelve: el dato más pequeño o null, * en el caso que el arbol este vacio. * Analogamente se podría determinar el más grande elemento en el arbol */ _____________________________________________________________________________ 27 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. //no esta implementado.....(Tarea) /* * Eliminar el arbol. */ //no esta implementado....(Tarea) /* * Test, si el arbol esta vacio o no. * devuelve true, caso de vacio; sino false. */ public boolean esVacio( ) { return raiz == null; } /* * Entregar el contenido del árbol en una sucesion ordenada. */ public void printArbol( ) { if( esVacio( ) ) System.out.println( "Arbol vacio" ); else printArbol( raiz ); } /* * Salida de los elementos del arbol binario rotados en 90 grados */ public void salidaArbolBinario() { if( esVacio() ) System.out.println( "Arbol vacio" ); else salidaArbolBinario(raiz,0); } /* * Metodo interno para tomar un nodo del arbol. * Parametro b referencia al nodo del arbol. * Devuelve los elementos o null, * caso de b sea null. */ private Comparable elementAt(Nodo_Avl b ) { return b == null ? null : b.datos; } /* * Metodo Interno para agregar o insertar un nodo en un subarbol. * x es el elemento a agregar. * b es el correspondiente nodo raiz. * Devuelve la nueva raiz del respectivo subarbol. */ private Nodo_Avl insertar(Comparable x, Nodo_Avl b) { if( b == null ) b = new Nodo_Avl(x, null, null); else if (x.compareTo( b.datos) < 0 ) { b.izq = insertar(x, b.izq ); if (altura( b.izq ) - altura( b.der ) == 2 ) if (x.compareTo( b.izq.datos ) < 0 ) b = RotacionSimpleIzq(b); else _____________________________________________________________________________ 28 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. b = RotacionDobleIzq_Der(b); } else if (x.compareTo( b.datos ) > 0 ) { b.der = insertar(x, b.der); if( altura(b.der) - altura(b.izq) == 2) if( x.compareTo(b.der.datos) > 0 ) b = RotacionSimpleDer(b); else b = RotacionDobleDer_Izq(b); } else ; // Duplicados; no hace nada b.altura = max( altura( b.izq ), altura( b.der ) ) + 1; return b; } /* * Metodo Interno para determinar el dato más pequeño. * b es la raiz. * Devuelve: Nodo con el elemento mas pequeño. */ private Nodo_Avl hallarMin(Nodo_Avl b) { if (b == null) return b; while(b.izq != null ) b = b.izq; return b; } /* * Analogamente al anterior pero el más grande. */ private Nodo_Avl hallarMax(Nodo_Avl b ) { if (b == null) return b; while (b.der != null) b = b.der; return b; } /* * Metodo interno para determinar un dato. * x es el dato buscado * b es la raiz * Devuelve: Nodo con el correspondiente dato. */ private Nodo_Avl hallar(Comparable x, Nodo_Avl b) { while( b != null ) if (x.compareTo( b.datos) < 0 ) b = b.izq; else if( x.compareTo( b.datos ) > 0 ) b = b.der; else return b; // paso return null; // no paso nada } // recorrido en Inorden _____________________________________________________________________________ 29 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. private void printArbol(Nodo_Avl b) { if( b != null ) { printArbol( b.izq ); System.out.println( b.datos ); printArbol( b.der ); } } /* * salida del arbol binario rotado en 90 Grados */ private void salidaArbolBinario(Nodo_Avl b, int nivel) { if (b != null) { salidaArbolBinario(b.izq, nivel + 1); for (int i = 0; i < nivel; i++) { System.out.print(' '); } System.out.println(b.datos); salidaArbolBinario(b.der, nivel + 1); } } /* * Salida: altura de los nodos, o -1, en el caso null. */ private static int altura(Nodo_Avl b) { return b == null ? -1 : b.altura; } /* * Salida: Maximum entre lhs y rhs. */ private static int max( int lhs, int rhs ) { return lhs > rhs ? lhs : rhs; } /* * Rotacion Simple Izquierda(simetrica a Rotacion Simple Derecha). * Para los arboles AVL, esta es una de las simples rotaciones. * Actualiza la altura, devuelve la nueva raiz. */ private static Nodo_Avl RotacionSimpleIzq(Nodo_Avl k2) { Nodo_Avl k1 = k2.izq; k2.izq = k1.der; k1.der = k2; k2.altura = max( altura( k2.izq ), altura( k2.der ) ) + 1; k1.altura = max( altura( k1.izq ), k2.altura ) + 1; return k1; } /* * Rotación Simple Derecha. */ private static Nodo_Avl RotacionSimpleDer(Nodo_Avl k1) { Nodo_Avl k2 = k1.der; k1.der = k2.izq; k2.izq = k1; k1.altura = max( altura( k1.izq ), altura( k1.der ) ) + 1; k2.altura = max( altura( k2.der ), k1.altura ) + 1; _____________________________________________________________________________ 30 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. return k2; } /* * * * * Rotacion doble: primero hijo izquierdo con su hijo derecho entonces nodo k3 con el nuevo hijo izquierdo. para los arboles AVL, esta es una doble rotación actualiza alturas, entrega nueva raiz. */ private static Nodo_Avl RotacionDobleIzq_Der(Nodo_Avl k3) { k3.izq = RotacionSimpleDer( k3.izq ); return RotacionSimpleIzq( k3 ); } /* * rotacion doble: primero hijo derecho * con su hijo izquierdo; luego nodo k1 con nuevo hijo derecho. * Para los AVL, esta es una doble rotación. * actualiza alturas, entrega nueva raiz. */ private static Nodo_Avl RotacionDobleDer_Izq(Nodo_Avl k1) { k1.der = RotacionSimpleIzq(k1.der); return RotacionSimpleDer(k1); } } //En archivo Nodo_Avl.java // Declaración de la clase Nodos para los elementos en los arbol AVL. class Nodo_Avl { // Instancias protected Nodo_Avl izq; protected Nodo_Avl der; protected int altura; public Comparable datos; // // // // hijo izquierdo hijo derecho altura los datos como elementos del arbol avl // Constructores public Nodo_Avl(Comparable datElem) { this(datElem, null, null ); } public Nodo_Avl( Comparable datElem, Nodo_Avl ib, Nodo_Avl db ) { datos = datElem; izq = ib; der = db; altura = 0; } } _____________________________________________________________________________ 31 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. La implementación de los árboles AVL, así como su salida están basados en los ejemplos dados. Para interpretar la salida considerar que se debe girar el árbol resultante de los datos 0, 1, 2, 3, 4, 5, 6, ya que los otros son al azar. Propuestas de mejora: Una mejora factible de considerar en la implementación del método insertar es considerar que los elementos a ingresar son String o caracteres, además de considerar el factor de balance y la nueva raíz que se obtiene. Como se muestra en el ejemplo siguiente. carácter que desea insertar al arbol-AVL (Borrar: \n): a a insertado AVL con balanceo: a(0) carácter que desea insertar al arbol-AVL (Borrar: \n): b b insertado AVL con balanceo: b(0) a(1) c insertado AVL con balanceo: c(0) _____________________________________________________________________________ 32 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) b(0) a(0) Dr. Eric Jeltsch F. Arboles Splay Introducción: Como una forma de introducir el tema, digamos que se desea implementar un sistema de software posible de llevar la atención a los pacientes de un hospital o consultorio, en el cual (dependiendo de su dolencia) se supone que cualquier paciente no tiene igual probabilidad de ser atendido. Por otra parte, cuando un paciente deja el hospital, es posible que su ficha clínica se vuelva menos activa, aunque es posible que eventualmente se consulte ella para hacer un historial clínico del paciente. Por otra parte, si después de un tiempo se vuelve a atender, se volverá activo de nuevo. Y finalmente, no es posible archivarlos cuando no están activos, pues pueden ocurrir nuevas atenciones, especialmente en pacientes crónicos, cuyas consultas pueden ser muy frecuentes en alguna época del año. La pregunta que surge es entonces que tipo de estructura de dato es la más adecuada en este tipo de situaciones. Digamos que si usamos ABB o AVL, los registros más antiguos irían a las hojas del árbol y por consiguiente sería más costoso su acceso. La pregunta es como traerlos “de abajo” sin grandes costos “hacia arriba”. Recordar que “más arriba “, significa que los accesos al disco son menos costosos que si los datos se encontraran en las hojas en un árbol de gran profundidad. En todo caso cualquier esfuerzo que se haga para traerlos repercutirá en el total. Lo sustantivo de esta nueva estructura de dato es reducir el tiempo total consumido accediendo a los elementos, en donde son trasladados los que son accedidos más frecuentemente hacia la raíz del árbol, donde pueden ser encontrados más rápidamente. En un sentido más técnico, los árboles ABB, AVL y Red-Black, la memoria de accesos a los elementos no es utilizada explícitamente; el objetivo es, en cambio, asegurar que el árbol nunca se desequilibre, garantizando de este modo que cualquier acceso nunca requiere más tiempo que O(logn). Con los árboles Splay no nos preocupamos tanto del costo de un acceso individual, que puede ser tan malo como O(n); en su lugar queremos asegurar que este tipo de comportamiento no puede producirse repetidamente. Este informe se centra en poder reconocer los árboles Splay y por otra reforzar los conceptos adquiridos en Estructuras de Datos, como al mismo tiempo practicar su implementación y aplicación con la ayuda del lenguaje de programación Java. Definición: Un árbol Splay es un árbol binario de búsqueda en el que todas las operaciones sobre el conjunto dinámico se implementan utilizando un esquema de reestructuración llamado biselación. La forma en la que un nodo es “rotado” hasta la raíz debe ser considerado dentro de los siguientes esquemas o patrones. Supongamos que se está realizando una biselación en el nodo v, cuyos nodos padre y abuelo (si existen) están etiquetados por p y g respectivamente. Los 3 casos siguientes poseen una variante simétrica. Caso1(zig): Si p es la raíz del árbol y v es su hijo izquierdo, realizar una operación rotar-derecha(p) tal como se muestra en la siguiente figura. En donde p es la raíz. Este proceso concluye cuando v haya alcanzado la raíz. Caso2(zig-zig): Si p no es la raíz del árbol, sino que tanto v como p son hijos izquierdos, realizar la operación rotar-derecha(g) seguido de rotar-derecha(p), tal como se muestra en la siguiente figura. En donde p es la raíz. Este proceso concluye cuando v haya alcanzado la raíz. _____________________________________________________________________________ 33 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Caso3(zig-zag): Si p no es la raíz del árbol, sino que p es un hijo izquierdo y v es un hijo derecho, realizar la operación rotar-izquierda(p) seguido de rotarderecha(g), tal como se muestra en la siguiente figura. Este proceso concluye cuando v haya alcanzado la raíz. Ejemplo: Consideremos que sucede después de haber accesado k1 sobre el árbol dado. El acceso al nodo k1 es a través de la línea punteada. Primeramente realizamos una rotación simple entre k1 y su padre (k2), obteniendo el siguiente árbol, Luego rotamos entre k1 y k3, obteniendo el siguiente árbol _____________________________________________________________________________ 34 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Finalmente debemos realizar dos rotaciones más, para obtener que k1 alcance la raíz. Ejemplo: Se muestra un ejemplo respecto de la búsqueda de una clave y las distintas operaciones a realizar. _____________________________________________________________________________ 35 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Ejemplo: En este caso se busca el 2. Splay(2) es genérico en término que intervienen una serie de operaciones. ¿Cuales son ellas?. Desgraciadamente, se puede apreciar que a través de las rotaciones el nodo k3 quedo situado en los niveles más inferiores, si lo comparamos con la posición que tenía antes. Notar que si supiéramos las frecuencias de acceso a cada elemento, podríamos armar un árbol en el que los elementos más accedidos estén más cerca de la raíz. De allí entonces el considerar este tipo de estructura árbol Splay, como una buena opción para cuando se tenga una distribución de probabilidades. Buscar: Supongamos que estamos buscando un elemento con clave k. Dado que un árbol biselado satisface la propiedad de los ABB para buscar un elemento es que se puede aplicar la misma técnica. Si se encuentra un nodo v conteniendo un elemento con clave k, el árbol es biselado en v y se devuelve el elemento. Si, la búsqueda es fallida, entonces el último nodo examinado antes de alcanzar un puntero nulo es biselado y se devuelve el valor nulo. Insertar: Se usa el método estándar de los ABB. Esto es, comenzamos buscando el punto de inserción; si se encuentra un puntero nulo se añade el nuevo nodo al árbol en este punto. A continuación el nuevo nodo es biselado. No obstante, si ya existe en el árbol un elemento con la clave que estamos buscando se bisela ese nodo. Eliminar: Esta es la operación mas complicada dado que involucra dos biselaciones. Primero, el nodo que queremos eliminar debe ser encontrado (si no se puede encontrar, entonces es biselado el último nodo encontrado). Asumamos que la clave que estamos buscando se encuentra en el nodo v. Entonces v es biselado a la raíz y luego eliminado, tal como lo muestra la figura siguiente. Esto nos deja con dos árboles biselados separados. La propiedad de los ABB asegura que toda clave de I es menor que toda clave de D. Para unir estos dos árboles de nuevo reorganizamos uno de los árboles, sea I, en donde se ejecuta la operación Máximo sobre él. Esto causa que el predecesor en inorden de v, denotado v‘, sea llevado a la raíz de I. Ahora como v’ contiene la clave máxima de I _____________________________________________________________________________ 36 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. no tendrá hijo derecho y la operación se completa haciendo que la raíz de D sea hijo derecho de v’, tal como lo muestra la figura siguiente. Si se desea en cambio reorganizar D, debería ejecutarse Mínimo sobre D antes de añadirle I. Implementación de los árboles Splay class NodoArbolBinario { // variables Instancias protected NodoArbolBinario izq; protected NodoArbolBinario der; public Comparable datos; // subarbol izquierdo // subarbol derecho // datos contenidos en el Nodo // Constructor public NodoArbolBinario(Comparable datosElement) { this(datosElement, null, null ); } public NodoArbolBinario(Comparable datosElement, NodoArbolBinario iz, NodoArbolBinario de) { datos = datosElement; izq = iz; der = de; } public void insertar (Comparable x) { if (x.compareTo(datos) > 0) // luego a la derecha { if (der == null) der = new NodoArbolBinario(x); else der.insertar(x); } else // sino izquierda { if (izq == null) izq = new NodoArbolBinario(x); else izq.insertar(x); } } public NodoArbolBinario getIzq() { return izq; } public NodoArbolBinario getDer() { return der; } } /* * Implementacion de un proceso top-down sobre los arboles Splay. _____________________________________________________________________________ 37 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. * Las comparaciones utilizan el método compareTo(). */ public class ArbolSplay { private NodoArbolBinario raiz; private static NodoArbolBinario Nodonull; { Nodonull = new NodoArbolBinario( null ); Nodonull.izq = Nodonull.der = Nodonull; } private static NodoArbolBinario nuevoNodo = null; // en diversos formas de inserción sera utilizado private static NodoArbolBinario ini = new NodoArbolBinario(null); /* * Constructor. */ public ArbolSplay( ) { raiz = Nodonull; } /* * manipulando la raiz */ public NodoArbolBinario buscarRaiz() { return raiz; } /* * Insertar. * Parametro x es el elemento a agregar. */ public void insertar( Comparable x ) { if( nuevoNodo == null ) nuevoNodo = new NodoArbolBinario( null ); nuevoNodo.datos = x; if( raiz == Nodonull ) { nuevoNodo.izq = nuevoNodo.der = Nodonull; raiz = nuevoNodo; } else { raiz = splay( x, raiz ); if( x.compareTo( raiz.datos ) < 0 ) { nuevoNodo.izq = raiz.izq; nuevoNodo.der = raiz; raiz.izq = Nodonull; raiz = nuevoNodo; } else if( x.compareTo( raiz.datos ) > 0 ) { nuevoNodo.der = raiz.der; nuevoNodo.izq = raiz; raiz.der = Nodonull; raiz = nuevoNodo; } _____________________________________________________________________________ 38 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. else return; } nuevoNodo = null; // } /* * Remover. * Parametro x es el elemento a eliminar. */ public void remover( Comparable x ) { NodoArbolBinario nuevoArbol; // Caso que x es encontrado, deja x en la raíz raiz = splay( x, raiz ); if( raiz.datos.compareTo( x ) != 0 ) return; // Elemento no encontrado; no hace nada if( raiz.izq == Nodonull ) nuevoArbol = raiz.der; else { // Hallar el máximmo en el subarbol izquierdo nuevoArbol = raiz.izq; nuevoArbol = splay( x, nuevoArbol ); nuevoArbol.der = raiz.der; } raiz = nuevoArbol; } /* * Determinar el elemento más pequeño del árbol. * Devuelve: el elemento más pequeño, o null caso vacío. */ public Comparable hallarMin( ) { if( esVacio( ) ) return null; NodoArbolBinario ptr = raiz; while( ptr.izq != Nodonull ) ptr = ptr.izq; raiz = splay( ptr.datos, raiz ); return ptr.datos; } /* * Determinar el elemento más grande del árbol. * Devuelve: el elemento más grande, o null, caso vacío. */ public Comparable hallarMax( ) { if (esVacio( )) return null; NodoArbolBinario ptr = raiz; while( ptr.der != Nodonull ) ptr = ptr.der; raiz = splay( ptr.datos, raiz ); return ptr.datos; } /* * Determina un elemento en el árbol. * Parametro x contiene el elemento buscado. * Devuelve: El elemento adecuado o null, caso vacío */ public Comparable hallar( Comparable x ) { raiz = splay( x, raiz ); if (raiz.datos.compareTo( x ) != 0) return null; return raiz.datos; } _____________________________________________________________________________ 39 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. /* * Hacer el arbol vacío. */ public void hacerVacio( ) { raiz = Nodonull; } /* * Comprobar si el árbol es vacío * Devuelve true caso vacío, en otro caso false. */ public boolean esVacio( ) { return raiz == Nodonull; } /* * Entrega el contenido del árbol según un cierto formato u orden. */ public void printArbol( ) { if (esVacio( )) System.out.println( "Arbol vacio" ); else printArbol( raiz ); } /* * Salida del arbol binario rotado en 90 Grados */ public void salidaArbolBinario(NodoArbolBinario b, int nivel) { if (b != b.izq) { salidaArbolBinario(b.izq, nivel + 3); for (int i = 0; i < nivel; i++) { System.out.print(' '); } System.out.println(b.datos.toString()); salidaArbolBinario(b.der,nivel + 3); } } /* * Metodo Interno para llevar a cabo un "top down" para splay. * El último Nodo encontrado en el árbol es la nueva raíz. * Parametro x es elemento objetivo, para el proceso Splaying o "biselado". * Parameter b es la raíz del subárbol, luego de llevado a cabo el Splaying. * Devuelve de subarbol. */ private NodoArbolBinario splay( Comparable x, NodoArbolBinario t ) { NodoArbolBinario izqArbolMax, derArbolMin; ini.izq = ini.der = Nodonull; izqArbolMax = derArbolMin = ini; Nodonull.datos = x; // garantiza coincidencia for( ; ; ) if( x.compareTo( t.datos ) < 0 ) { if( x.compareTo( t.izq.datos ) < 0 ) t = rotacion_con_HijoIzq( t ); if( t.izq == Nodonull ) break; // a la derecha derArbolMin.izq = t; derArbolMin = t; _____________________________________________________________________________ 40 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. t = t.izq; } else if( x.compareTo( t.datos ) > 0 ) { if( x.compareTo( t.der.datos ) > 0 ) t = rotacion_con_HijoDer( t ); if( t.der == Nodonull ) break; // a la izquierda izqArbolMax.der = t; izqArbolMax = t; t = t.der; } else break; izqArbolMax.der = t.izq; derArbolMin.izq = t.der; t.izq = ini.der; t.der = ini.izq; return t; } /* * Rotacion NodoArbolBinario con descendiente a la izquierda. */ static NodoArbolBinario rotacion_con_HijoIzq(NodoArbolBinario k2) { NodoArbolBinario k1 = k2.izq; k2.izq = k1.der; k1.der = k2; return k1; } /* * Rotacion NodoArbolBinario con descendiente a la derecha. */ static NodoArbolBinario rotacion_con_HijoDer(NodoArbolBinario k1) { NodoArbolBinario k2 = k1.der; k1.der = k2.izq; k2.izq = k1; return k2; } private void printArbol( NodoArbolBinario b ) { if( b != b.izq ) { printArbol(b.izq); System.out.println(b.datos.toString( )); printArbol(b.der); } } } import java.io.*; public class ArbolSplayTest { public static void main( String [ ] args ) { ArbolSplay b = new ArbolSplay(); String lineaEntrada = null; System.out.println("Ingrese Datos"); BufferedReader entrada = null; entrada = new BufferedReader( new InputStreamReader(System.in)); try { _____________________________________________________________________________ 41 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. int numero; do { System.out.println("Numero ? "); lineaEntrada = entrada.readLine(); try { numero = Integer.parseInt(lineaEntrada); b.insertar(new Integer(numero)); b.salidaArbolBinario(b.buscarRaiz(),2); } catch (NumberFormatException ne) { break; } } while (lineaEntrada != ""); } catch (IOException ioe) { System.out.println("Entrada en main()"); } System.out.println("Hallar el elemento mas pequeño"); b.hallarMin(); b.salidaArbolBinario(b.buscarRaiz(),2); System.out.println("Hallar el elemento más grande"); b.hallarMax(); b.salidaArbolBinario(b.buscarRaiz(),2); } } Una forma de visualizar su funcionamiento le ingresamos las claves 1, 2, 3, 4, 5. cuya salida debería interpretarse como el árbol que se adjunta. Más aún si ahora, en este momento pulsa <enter> podrá obtener en la raíz el más grande y el más pequeño de los elementos. _____________________________________________________________________________ 42 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. cuyos árboles Splay son los que se muestran. Arbol Red-Black Introducción: Supongamos que se desea tener un Buscador de Direcciones de Email, el cual almacena nombres y direcciones de e-mail en un archivo map basado en la clase java.util.TreeMap definida en el JDK 1.2. La clase TreeMap crea una estructura de datos llamada "red-black tree", en el cual, los datos son almacenados con una clave y un valor, es decir, el nombre es la clave y la dirección e-mail el valor. Cuando añadimos una entrada al archivo map, introducimos tanto un nombre (la clave) como una dirección e-mail (el valor). De manera que, podemos buscar o borrar una dirección e-mail introduciendo sólo un nombre. El nombre no puede ser null porque es una clave. Si un usuario intenta introducir un nombre null, la aplicación lanza una excepción y muestra una página de error. Esta es una aplicación en donde se utilizan la estructura de datos Red-Black para organizar los datos. Un árbol rojo-negro es un árbol binario extendido, en donde los nodos pueden ser ramas u hojas. Los nodos hojas son los nodos que hay al final de una línea, mientras que los nodos ramas son los nodos más grandes que conectan con dos o más líneas. Los nodos se almacenan en una estructura compensada en el árbol, usando las siguientes condiciones: _____________________________________________________________________________ 43 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. 1. Cada nodo tiene dos hijos o es una hoja. 2. Cada nodo está coloreado en rojo en negro. 3. Cada nodo hoja está coloreado en negro. 4. Si un nodo es rojo, sus dos hijos son negros. 5. Cada camino desde el raíz hasta una hoja contiene el mismo número de nodos negros. Ejemplo Aquí vemos un Red-Black válido, y otro que no lo es. Todo nodo es ya sea red (circulo) ó black (cuadrado) • La raíz es black, • Toda hoja (leaf ) es black, a pesar que no se vean, • Si un nodo es red, entonces ambos hijos son black - dos nodos red pueden no ser adyacentes. - Pero si un nodo es black, sus hijos pueden ser red o black, • Para todo nodo, todos los caminos (paths) desde él a sus descendientes contienen el mismo nº de nodos black. Observar que todo AVL tree es también un Red-Black tree, sin embargo no todo Red-Black tree es necesariamente un AVL. Notar que una de la diferencia fundamental es que los AVL tree tienen altura de log(n), mientras que los Red-Black tree tienen altura menor o igual a 2log(n+1). La ventaja de un árbol Red_Black en el contexto de Buscador de Direcciones de E-mail es que podemos crear un archivo map que almacena datos en orden ascendente (ordenados por claves) y que tiene tiempos de búsqueda rápidos. Una relación interesante de los Red-Black con otro tipo de árbol es con los árbol 2-3-4, los que pueden representarse como árboles binarios. La idea comprende, representar 3-nodos y 4-nodos como árbol binario, los que a través de arcos „rojos“ pueden ser unidos tal como lo muestra la figura, _____________________________________________________________________________ 44 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Ejemplo: Transformar el árbol siguiente en un Red-Black Desde un punto de vista más técnico, digamos que la librería Collection posee un “map”, que corresponde a una colección de datos, cuya naturaleza consta de dos partes: – una clave única, la que “maps” al valor correspondiente – un valor, el cual puede no ser única. Un “map” de alumnos, en donde la clave, que es única (nº ID del alumno) y el resto es el valor del alumno (ficha asociada a él, o el promedio de notas de un curso). Llamaremos altura negra ( black-height ó bh(x) ) de un nodo x, al número de nodos negros desde x a una hoja, no incluyendo a x. Se definirá como altura negra de un RB-Tree a la altura negra de su raíz. Se asumirá que la raíz de un RB-Tree es negra. La altura negra del RB-Tree de la Figura 1, es 3. _____________________________________________________________________________ 45 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) = red Dr. Eric Jeltsch F. = black 40 3 0 20 100 2 3 10 30 60 120 1 1 2 2 0 0 0 0 50 80 110 1 2 1 0 0 70 90 1 1 0 0 0 140 2 0 160 130 1 0 0 1 0 150 170 1 0 1 0 0 0 Algunos números de interés. a) Cualquier árbol Red-Black, con raíz x, tiene al menos n = 2bh(x) – 1 nodos internos, donde bh(x) es la altura negra del nodo x. b) En un árbol Red-Black, al menos la mitad de los nodos sobre cualquier camino desde la raíz a la hoja debe ser negro. Con esto se puede probar que un árbol Red-Black con n-nodos internos tiene altura h 2log(n -1). En efecto, basado en b), podemos inferir que si h es la altura de un árbol Red-Black, entonces bh(x) h/2, luego n 2h/2 – 1, n –1 2h/2, log(n-1) h/2 y finalmente 2 log(n-1) h. En consecuencia este resultado establece que la altura de los árboles Red-Black es O(log(n)). De donde se tiene que el costo de inserción es O(log(n)) para descender al punto de inserción, O(1), para hacer la inserción y O(log(n)) para ascender. Así como también se puede afirmar que la búsqueda esta garantizada que sea bajo logN. INSERTAR La inserción es igual que el insertado en un ABB, pero con la propiedad de que el nodo insertado siempre va ser rojo. Luego de tener insertado el nodo en el árbol se rompe la propiedad Red-Black si y solo si el padre es rojo, si el padre es negro no existen problemas. Sin embargo, existen una serie de casos a estudiar. Por lo pronto digamos que insertar en un arbol RED-BLACK es de costo Ο(log n). Ejemplo A continuación se muestra una sucesión de inserciones a partir de un Red-Black inicialmente vacío. Los datos a insertar son 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15. , , , , _____________________________________________________________________________ 46 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. , , Y a continuación se muestra la salida para una propuesta de implementación que usa la interface Comparable y la interpretación que se le debe dar a la salida para relacionarlo con los árboles RedBlack, es decir, es un par ordenado (a, b) en donde la primera componente es el valor ingresado y la segunda componente es ya sea 1(black) o 0(red). , , _____________________________________________________________________________ 47 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. , , , , , . _____________________________________________________________________________ 48 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. /* * Implementación de un Arbol_RB. * Las comparaciones están basadas en el método compareTo. */ public class Arbol_RB { private Nodo_RB base; // nodo básico private static Nodo_RB nullNode; { nullNode = new Nodo_RB(null); nullNode.izq = nullNode.der = nullNode; } static final int BLACK = 1; // Black debe ser 1 static final int RED = 0; // Para "insertar" se necesitan private static Nodo_RB actual; private static Nodo_RB padre; private static Nodo_RB relac1; //relac1 es la relación entre los nodos _____________________________________________________________________________ 49 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. private static Nodo_RB relac2;//relac1 es la relación entre los nodos /* * Construcción del Arbol. * negInf es un valor, el que es menor o igual a todos los otros valores. */ public Arbol_RB(Comparable negInf) { base = new Nodo_RB(negInf); base.izq = base.der = nullNode; } /* * agregar en el RB. Duplicados no serán considerados. * "item" es el dato que se agrega. */ public void insert(Comparable item) { actual = padre = relac1 = base; nullNode.dat = item; while( actual.dat.compareTo( item ) != 0 ) { relac2 = relac1; relac1 = padre; padre = actual; actual = item.compareTo(actual.dat ) < 0 ? actual.izq : actual.der; // Probar si dos hijos R; en caso de si, fijarlo if( actual.izq.color == RED && actual.der.color == RED ) reOrientar( item ); } // campo para agregar, en caso de if( actual != nullNode ) return; actual = new Nodo_RB( item, nullNode, nullNode ); // colgar de su padre if( item.compareTo( padre.dat) < 0 ) padre.izq = actual; else padre.der = actual; reOrientar( item ); } /* * eliminar no esta implementado * x es el dato a eliminar. */ public void remove( Comparable x ) { System.out.println("Eliminar no esta implementado."); } /* * hallar el min elemento en el RB. * devuelve el elemento más pequeño o null, en caso vacío. */ public Comparable findMin( ) { if (isEmpty( )) return null; Nodo_RB itr = base.der; while( itr.izq != nullNode ) itr = itr.izq; return itr.dat; } /* * hallar el elemento más grande en el RB. _____________________________________________________________________________ 50 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. * devuelve el elemento más grande, o null, en caso vacío. */ public Comparable findMax( ) { if (isEmpty( )) return null; Nodo_RB itr = base.der; while( itr.der != nullNode ) itr = itr.der; return itr.dat; } /* * hallar un elemento en el RB. * x es el elemento buscado. * devuelve el elemento respectivo o null,en el caso que no sea encontrado. */ public Comparable find(Comparable x) { nullNode.dat = x; actual = base.der; for( ; ; ) { if( x.compareTo( actual.dat ) < 0 ) actual = actual.izq; else if( x.compareTo( actual.dat ) > 0 ) actual = actual.der; else if( actual != nullNode ) return actual.dat; else return null; } } /* * hacer el RB vacío. */ public void makeEmpty( ) { base.der = nullNode; } /* * Test, o si el RB esta vacío o no. * devuelve true, en el caso vacío; en otro caso false. */ public boolean isEmpty( ) { return base.der == nullNode; } /* * salida del RB en orden. */ public void printTree( ) { if( isEmpty() ) System.out.println("Arbol vacío"); else printTree( base.der ); } /* * uno de los métodos de lectura inorden. * b es la raíz del RB. */ private void printTree(Nodo_RB b) { _____________________________________________________________________________ 51 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. if (b != nullNode ) { printTree(b.izq); System.out.println(b.dat ); printTree(b.der); } } /* * salida del RB con una rotacion de 90 Grados. */ public void salidaArbol_RB() { salidaArbol_RB(base.der,0); } private void salidaArbol_RB(Nodo_RB b, int nSpace) { if (b != nullNode) { salidaArbol_RB(b.izq,nSpace += 6); for (int i = 0; i < nSpace; i++) System.out.print(" "); System.out.println(b.dat + " " + b.color); salidaArbol_RB(b.der, nSpace); } } /* * rutina que permite en el caso de que un nodo tenga 2 hijos * los lleva a cambiar de color y rotar. * item contiene el dato a incorporar. */ private void reOrientar(Comparable item) { // cambiar color actual.color = RED; actual.izq.color = BLACK; actual.der.color = BLACK; if (padre.color == RED) // Rotación es necesaria { relac1.color = RED; if ( (item.compareTo( relac1.dat) < 0 ) != (item.compareTo( padre.dat) < 0 ) ) padre = rotacion(item, relac1); // parte la doble rotación actual = rotacion(item, relac2); actual.color = BLACK; } base.der.color = BLACK; // hace la raíz negra } /* * rutina para una simple o doble rotación. * "item" es el dato a reOrientar. * "padre" es el "padre" de la raíz de los sub arboles rotados. * devuelve: la raíz de los sub arboles rotados. */ private Nodo_RB rotacion(Comparable item, Nodo_RB padre) { if (item.compareTo(padre.dat) < 0) return padre.izq = item.compareTo( padre.izq.dat) < 0 ? rotacionCon_descen_izq(padre.izq) : // LL rotacionCon_descen_der(padre.izq) ; // LR else return padre.der = item.compareTo(padre.der.dat) < 0 ? rotacionCon_descen_izq(padre.der) : // RL rotacionCon_descen_der(padre.der); // RR _____________________________________________________________________________ 52 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. } /* * Rotación Nodo_AB con descendiente a la izq. */ static Nodo_RB rotacionCon_descen_izq( Nodo_RB k2 ) { Nodo_RB k1 = k2.izq; k2.izq = k1.der; k1.der = k2; return k1; } /* * Rotación Nodo_AB con descendiente a la der. */ static Nodo_RB rotacionCon_descen_der(Nodo_RB k1) { Nodo_RB k2 = k1.der; k1.der = k2.izq; k2.izq = k1; return k2; } } import java.io.*; public class Arbol_RBTest { public static void main(String[ ] args) { InputStreamReader isr = new InputStreamReader(System.in); BufferedReader ein = new BufferedReader(isr); String entrada_datos = null; Arbol_RB b = new Arbol_RB(new Integer(Integer.MIN_VALUE)); System.out.println("Ingrese los datos: "); while (true) { try { entrada_datos = ein.readLine(); int a = Integer.parseInt(entrada_datos); if (a == 0) break;//no admite al cero como entrada b.insert(new Integer(a)); b.salidaArbol_RB(); } catch(IOException e) { System.out.println(e.toString()); System.exit(0); } } b.printTree(); b.salidaArbol_RB(); } } Ejemplo: Veamos como se relacionan las claves "10 85 15", con la propuesta de implementación de árbol red-black. _____________________________________________________________________________ 53 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Estado de inicio Dr. Eric Jeltsch F. public Arbol_RB(Comparable negInf) { base = new Nodo_RB(negInf); base.izq = base.der = nullNode; } Veamos que pasa con el árbol RB luego de la declaración b.insert(new Integer(10)). Prosigamos con la declaración b.insert(new Integer(85)). _____________________________________________________________________________ 54 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Y, veamos que pasa con b.insert(new Integer(15)) Dr. Eric Jeltsch F. Notar que „Rotación“ es necesaria! Sobre el padre, se tiene padre = rotacion(item,relac1), de donde padre.der = rotacionCon_descen_izq(padre.der). Luego de esta rotación continúa otra rotación y esta vez sobre, actual = rotacion(item, relac2) y padre.der = rotacionCon_descen_der(padre.der) _____________________________________________________________________________ 55 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Estado Final Dr. Eric Jeltsch F. Eliminación: La eliminación de un dato en un árbol RED-BLACK consta de dos pasos, primero se procede con el mismo método de eliminación en un ABB normal, es decir buscar el elemento a eliminar y remplazarlo por el mayor de sus descendientes reduciendo el problema a la eliminación de una hoja; luego se procede a restablecer las propiedades para el balanceo de un RED-BLACK. Sin embargo, existen una serie de casos a estudiar. Arboles AA Introducción: La implementación de los árboles Red-Black es demasiado extenso, en particular la gran cantidad de casos que se dan en el borrado. Los AA-tree permiten limitar en cierto modo lo realizado por los Red-Black. Ejemplo: A partir de un árbol AA, primitivamente vacío se deben agregar los siguientes elementos: 10, 85 15, 70, 20, 60, 30, 50, 65, 80, 90, 40, 5, 55, 35, y en este sentido ver su resultado. _____________________________________________________________________________ 56 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. _____________________________________________________________________________ 57 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. _____________________________________________________________________________ 58 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. que corresponde a la siguiente salida según la implementación siguiente class Nodo_AA { public Comparable dat; protected Nodo_AA izq; protected Nodo_AA der; protected int nivel; // Constructores Nodo_AA(Comparable datElement) { this(datElement, null, null); } Nodo_AA(Comparable datElement, Nodo_AA i, Nodo_AA d) { dat = datElement; izq = i; der = d; nivel = 1; } } public class Arbol_AA { private Nodo_AA raiz; private static Nodo_AA nullNode; { nullNode = new Nodo_AA(null); _____________________________________________________________________________ 59 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. nullNode.izq = nullNode.der = nullNode; nullNode.nivel = 0; } private static Nodo_AA borrarNodo; private static Nodo_AA ultNodo; /* * Constructor. */ public Arbol_AA( ) { raiz = nullNode; } public void insert(Comparable x) { raiz = insert(x, raiz); } public void remove( Comparable x ) { borrarNodo = nullNode; raiz = remove(x, raiz); } public Comparable findMin( ) { if (isEmpty( )) return null; Nodo_AA zgr = raiz; while (zgr.izq != nullNode ) zgr = zgr.izq; return zgr.dat; } public Comparable findMax( ) { if (isEmpty( )) return null; Nodo_AA zgr = raiz; while (zgr.der != nullNode) zgr = zgr.der; return zgr.dat; } public Comparable find(Comparable x ) { Nodo_AA actual = raiz; nullNode.dat = x; for( ; ; ) { if (x.compareTo(actual.dat) < 0 ) actual = actual.izq; else if (x.compareTo(actual.dat) > 0 ) actual = actual.der; else if (actual != nullNode) return actual.dat; else return null; } } _____________________________________________________________________________ 60 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. public void makeEmpty() { raiz = nullNode; } public boolean isEmpty( ) { return raiz == nullNode; } public void printTree() { if (isEmpty( )) System.out.println("Arbol vacio"); else printTree(raiz); } public void salidaArbol_AA() { salidaArbol_AA(raiz,0); } private Nodo_AA insert(Comparable x, Nodo_AA b) { if (b == nullNode) b = new Nodo_AA(x, nullNode, nullNode); else if (x.compareTo(b.dat) < 0 ) b.izq = insert(x, b.izq); else if (x.compareTo(b.dat) > 0 ) b.der = insert(x, b.der); else return b; b = skew(b); b = split(b); return b; } private Nodo_AA remove(Comparable x, Nodo_AA b) { if( b != nullNode ) { // paso 1: buscar, ubicar ultNodo y borrarNodo ultNodo = b; if( x.compareTo( b.dat ) < 0 ) b.izq = remove( x, b.izq ); else { borrarNodo = b; b.der = remove( x, b.der ); } // Paso 2: si x es el actual, es eliminado if( b == ultNodo ) { if( borrarNodo == nullNode || x.compareTo( borrarNodo.dat ) != 0 ) return b; // si no es encontrado, no pasa nada. borrarNodo.dat = b.dat; b = b.der; } // Paso 3: En otro caso, Rebalanciar else _____________________________________________________________________________ 61 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. if( b.izq.nivel < b.nivel - 1 || b.der.nivel < b.nivel - 1 ) { if( b.der.nivel > --b.nivel ) b.der.nivel = b.nivel; b = skew( b ); b.der = skew( b.der ); b.der.der = skew( b.der.der ); b = split( b ); b.der = split( b.der ); } } return b; } private void printTree(Nodo_AA b) { if (b != b.izq) { printTree( b.izq ); System.out.println( b.dat.toString( ) ); printTree(b.der ); } } private void salidaArbol_AA(Nodo_AA b, int nSpace) { if (b != b.izq) { salidaArbol_AA(b.izq,nSpace += 6); for (int i = 0; i < nSpace; i++) System.out.print(" "); System.out.println(b.dat + ", " + b.nivel); salidaArbol_AA(b.der, nSpace); } } private Nodo_AA skew(Nodo_AA b) { if (b.izq.nivel == b.nivel ) b = rotacionCon_descen_izq(b); return b; } private Nodo_AA split(Nodo_AA b) { if (b.der.der.nivel == b.nivel ) { b = rotacionCon_descen_der(b); b.nivel++; } return b; } static Nodo_AA rotacionCon_descen_izq(Nodo_AA k2) { Nodo_AA k1 = k2.izq; k2.izq = k1.der; k1.der = k2; return k1; } static Nodo_AA rotacionCon_descen_der(Nodo_AA k1) { _____________________________________________________________________________ 62 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Nodo_AA k2 = k1.der; k1.der = k2.izq; k2.izq = k1; return k2; } } import java.io.*; public class Arbol_AATest { public static void main(String[ ] args) { InputStreamReader isr = new InputStreamReader(System.in); BufferedReader ein = new BufferedReader(isr); String entrada_datos = null; Arbol_AA b = new Arbol_AA(); System.out.println("Ingrese los datos: "); while (true) { try { entrada_datos = ein.readLine(); int a = Integer.parseInt(entrada_datos); if (a == 0) break; b.insert(new Integer(a)); b.salidaArbol_AA(); } catch(IOException e) { System.out.println(e.toString()); // System.exit(0); } } // b.printTree(); b.salidaArbol_AA(); } } Ejemplo Dado el árbol AA, Se pide, a) Agregar la clave 2 b) Agregar la clave 45 _____________________________________________________________________________ 63 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. - Punteros a la izquierda son eliminados a través de una rotación derecha, tal como se ve en la figura (llamada “skew”) Por ejemplo, - punteros consecutivos a la derecha se eliminan a través de una rotación izquierda simple, (llamada, “split”) Por ejemplo, agregar la clave 45. „split“ en el nodo con clave 35 _____________________________________________________________________________ 64 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) „skew“ en el nodo con clave 50 Dr. Eric Jeltsch F. „split“ en el nodo con clave 40 Finalmente, se obtiene el siguiente árbol, luego de realizar „skew“ en el nodo 70 y „split“ en el nodo 30 Arboles B-Tree Introducción: Pensar en establecer una suerte de balanceo resulta a la postre un costo muy superior al de los árboles binarios, sin embargo, los árboles Red-Black proponen una buena forma de optimizar en términos de ahorro en las Rotaciones, sin embargo, para aquellos árboles de grado mayor que 2, resulta un tanto más complicado buscar la forma de optimizarlos, de allí entonces que la opción de usar los árbol AA resultan una buena alternativa. Otro tipo de árboles, llamados árboles multicaminos o M-Way son también muy utilizados en la construcción y mantenimiento de árboles de búsqueda con gran cantidad de datos o nodos, y por tanto, usualmente almacenados en memoria secundaria, en donde en este caso el acceso al disco, así como la rapidez para encontrar la información, es un problema no menor. Como ya se menciono, en los almacenamientos en memoria secundaria el costo más significativo viene dado por el acceso a este tipo de memoria, muy superior al tiempo necesario para acceder a cualquier posición de memoria principal. Entonces al tratarse de un dispositivo periférico, y por tanto, de acceso lento, resulta primordial minimizar en lo posible el número de accesos. Por ejemplo, para almacenar un millón de elementos en memoria secundaria, con un árbol binario, el _____________________________________________________________________________ 65 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. 6 número promedio de accesos es del orden log 210 aprox. 20 accesos, en donde AVL o Red-Black no mejoran substancialmente este record. Sin embargo los árboles multicaminos mejoran este número de accesos, pues se organizan de tal manera que un árbol se subdivide en subárboles y éstos se representan como unidades a las que se accede simultáneamente, estos subárboles se les llama páginas, cada acceso a página requiere un único acceso a memoria secundaria. De manera que si almacenamos 100 nodos por página se necesitarán en promedio log 100106 aprox. 3 accesos a memoria secundaria para buscar en un millón de elementos. No obstante esta forma de distribuir la información no garantiza que el árbol se degenere, es decir en el peor de los casos, los accesos serán 104. Nuevamente se hace imprescindible un criterio de crecimiento armónico que garantice cierto equilibrio, que se logra en las aplicaciones en Base de Datos. Los datos como ya mencionamos son almacenados sobre el disco en bloques, (blocks) con un número fijo de bytes. Todo bloque por otro lado puede tener muchos registros (record). Un registro corresponde al dato almacenado en el nodo exterior o interior, un nodo por registro. Ahora, el tiempo requerido para acceder al bloque es relativamente largo, pues el cabezal debe desplazarse sobre el cilindro apropiado y el bloque debe entonces rotar con alguna velocidad para posicionarse bajo el cabezal. Una vez que el disco esta posicionado sobre el bloque, el bloque entero es accesado (leer o escribir). El dato más pequeño que puede ser accesado sobre el disco es un bloque. Veamos a continuación una breve introducción de los datos más relevantes en la organización y acceso a la información guardada en una base de datos. Organización Física de los Sistemas de Base de Datos. La organización física de una base de datos es un tópico extenso y se aborda en detalle, principalmente en la asignatura Base de Datos, y digo principalmente pues también se trata en sistema operativo y sistemas de software. Sin embargo, el rendimiento general de un sistema de base de datos se determina en gran medida por las estructuras de datos físicas usadas y por la eficiencia con la cual el sistema trabaja sobre las mismas. Aunque los usuarios no siempre deban tener conocimiento de los detalles del diseño físico de la base de datos, sin embargo Ud. deberían saber que éstos afectan al rendimiento, un factor de gran importancia en la satisfacción del usuario con el sistema de base de datos. Una pregunta nos asalta, ¿Podría el usuario obtener la información deseada en el formato apropiado y en un tiempo conveniente?. Esta última frase, "tiempo conveniente", puede expresarse generalmente como tiempo de respuesta aceptable. La "información deseada" y el "formato apropiado" no se afectan mucho por la organización física de la base de datos, pero el tiempo de respuesta sí. El tiempo de respuesta es el tiempo transcurrido entre la iniciación de una operación sobre la base de datos y la disponibilidad del resultado. Un tiempo de respuesta lento es la queja más frecuente que expresan los usuarios de los sistemas de bases de datos, posiblemente debido a que es lo que se observa más fácilmente. Un buen diseño físico de la base de datos almacenaría datos de forma que puedan recuperarse, actualizarse y manipularse en el mínimo tiempo posible. En este segmento me interesa abordar aspectos de la organización física de la base de datos que soportan la eficiencia de las operaciones en las mismas. Acceso físico a la base de datos _____________________________________________________________________________ 66 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. En la figura anterior, se muestra el sistema para el acceso físico a la base de datos. Se puede ver la interacción del usuario con el sistema de base de datos al iniciar una consulta o demanda. El selector de estrategia (usualmente el software que transforma una consulta del usuario en una forma efectiva para su posterior ejecución) traduce la orden del usuario a su forma más eficiente para su ejecución. La orden traducida activa entonces al administrador de buffer, que controla el movimiento de datos entre la memoria principal y el almacenamiento en disco. El administrador de archivos da soporte al administrador de buffer administrando la reserva de localizaciones de almacenamiento en disco y las estructuras de datos asociadas. Además de los datos del usuario, el disco contiene el diccionario de datos, que define la estructura de los datos del usuario y cómo éstos pueden usarse. Los datos del usuario se almacenan como una base de datos física o colección de registros. Formas de almacenamiento físico La memoria principal es el almacenamiento intermedio usado por los datos que están disponibles para las operaciones del usuario. Aquí es donde reside la ejecución del programa y como los datos se necesitan por el programa para ejecutar sus funciones, se transmiten estos desde el almacenamiento secundario a la memoria principal. Aunque la memoria principal puede ser capaz de almacenar varios megabytes de datos, es normalmente muy pequeña para almacenar la base de datos completa, por lo que es necesario el almacenamiento secundario. El almacenamiento secundario para los sistemas, de base de datos está compuesto generalmente por el almacenamiento en disco y el almacenamiento en cinta magnética. Casi siempre, la base de datos completa se almacena en disco y porciones de ésta se transfieren desde el disco a la memoria primaria, a medida que se necesita. El almacenamiento en disco es la forma principal de almacenamiento con acceso directo, por lo que los registros individuales se pueden acceder directamente. Aunque el almacenamiento en cinta magnética es menos costoso que el almacenamiento en disco, los registros pueden ser solamente accedidos secuencialmente (y más lentamente que en disco). Su función en el sistema de base de datos está básicamente limitada a archivar datos. La unidad física en la que está contenido el medio de grabación del disco se llama controlador de disco (disk driver). El controlador de disco contiene un paquete de disco o volumen, el cual está formado por un conjunto de superficies grabables (discos) montados sobre un eje. En operación, el eje y los discos rotan a una alta velocidad. Los datos se graban sobre las pistas, que son coronas circulares grabables encontradas sobre cada superficie, tal como lo muestra la siguiente figura. _____________________________________________________________________________ 67 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Una metáfora para tal descripción es que el paquete de disco es una pila de discos musicales sobre un eje, excepto que aquí las pistas son concéntricas y no en forma de espiral interna hacia el centro. Un conjunto de cabezas de lectura/escritura ubicadas al final de un brazo, simbólicamente como los dientes de un peine, se mueven como un grupo, de tal forma que éstos pueden ser posicionados sobre todas las pistas del mismo radio. El conjunto de dichas pistas se denomina cilindro. Es decir, conceptualmente, un conjunto de pistas del mismo diámetro rotando a alta velocidad forma un cilindro. Esta definición es muy útil, ya que cualquier posicionamiento de un conjunto de cabezas de lectura/escritura puede ser descrito por la localización del cilindro (por ejemplo, cilindro 199). Por lo que todas las pistas del cilindro especificado se pueden escribir o leer, sin un movimiento adicional de las cabezas de lectura/escritura. La dirección de un registro del disco normalmente necesita información sobre el número del cilindro, de la superficie y del bloque. Bloques de almacenamiento físico El registro físico o bloque es la unidad de dato más pequeña en un disco que es físicamente direccionable, vea la figura anterior, en donde cada pista en una superficie está compuesta de un número de bloques. Un bloque puede contener uno o más registros lógicos. Ejemplo: Supongamos que tenemos un factor de compactación de 3, esto significa que en cada bloque se almacenan tres registros lógicos. Supongamos que deseamos recuperar el registro Juan Perez almacenado en la siguiente dirección, Nº cilindro = 5, Nº superficie = 2, Nº bloque =1 . Entonces, para recuperar el registro Juan Perez, las cabezas de lectura/escritura se mueven sobre el cilindro 5 (pista 5 en todas las superficies). Entonces se activan las cabezas de lectura/escritura para la superficie número 2 y se leen los números de bloques a la vez que la pista gira sobre las cabezas. Cuando se detecta el bloque 1, el bloque entero de tres registros lógicos se lee en memoria principal, donde se selecciona el registro Juan Perez. En nuestro ejemplo suponemos la estructura más general de un disco, donde las cabezas de lectura/escritura están sujetas a un brazo movible. No todas las unidades de disco están configuradas de esta forma. En algunas, las cabezas de lectura/escritura son fijas para cada cilindro. Típicamente estas unidades son más costosas, pero más rápidas, debido a que no hay retraso en mover las cabezas de lectura/escritura sobre un nuevo cilindro. Generalmente, el tiempo necesario para ejecutar cálculos en un bloque es mucho menor que el necesario para transferir los datos entre el almacenamiento secundario y el primario. Sin embargo, una buena estrategia de diseño es identificar, donde sea posible, los registros lógicos que probablemente se usan en las mismas operaciones y agruparlos en bloques. Ejemplo: Supongamos que una firma almacena tres tipos de alambres, A, B y C, y que se entregan en el mismo cargamento. Si cada bloque contiene tres registros y los registros A, B y C se almacenan en bloques separados, se necesitarían tres operaciones de entrada/salida (E/S) para _____________________________________________________________________________ 68 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. actualizarlos. Sin embargo, si se agrupan en el mismo bloque, entonces sólo es necesaria una operación de E/S. Debido a que generalmente el acceso a disco es un cuello de botella en las operaciones de acceso a una base de datos, una asignación cuidadosa de los registros en los bloques puede mejorar significativamente el tiempo de respuesta. Factores de Rendimiento del Disco En general, hay cuatro factores que directamente afectan a la velocidad, con la que los datos se transfieren a y desde el almacenamiento en disco: tiempo de posicionamiento (access motion time), tiempo de activación de la cabeza, retraso de rotación y razón de transferencia. -Tiempo de posicionamiento (TP), se conoce como tiempo de búsqueda (seek time), es el tiempo necesario para mover las cabezas de lectura/escritura desde su posición actual a una nueva dirección de cilindro. Obviamente, un movimiento hacia una posición adyacente no toma la misma cantidad de tiempo que moverse a través de toda la superficie del disco (desde la pista más interna hasta la más externa, y viceversa). Como una aproximación en los cálculos puede usarse el tiempo medio de posicionamiento: aproximadamente el tiempo necesario para moverse a través de la mitad de los cilindros, por lo que debe usarse un método más sofisticado. Un acuerdo estándar consiste en que la probabilidad de acceso para todos los registros sea igual, brindando una distribución de probabilidad uniforme. El promedio para una distribución uniforme se encuentra en el medio entre los valores extremos. Para el tiempo de posicionamiento, el valor extremo pudiera ser (1) mantenerse posicionado sobre el cilindro actual, o (2) moverse desde el cilindro más interno hacia el más externo (o viceversa). Dado que asumimos la distribución uniforme, la media sería el tiempo para moverse a través de la mitad de los cilindros. De doce a veinte milisegundos es el tiempo medio típico de posicionamiento, de acuerdo con el modelo y la composición del controlador de disco, hoy en día es factible de encontrar tiempos medios menores. -Tiempo de activación de la cabeza es el tiempo necesario para activar electrónicamente la cabeza que se encuentra sobre la superficie cuando ocurre la transferencia de datos. En comparación con otros factores de rendimiento, generalmente este tiempo es despreciable. Por esta razón rara vez se usa en los cálculos de rendimiento. -El retraso de rotación o latencia es el tercer factor de tiempo. Representa la cantidad de tiempo que necesita el bloque seleccionado para rotar la cabeza, de forma tal que la transferencia de datos pueda comenzar. El tiempo de rotación depende de dos factores: a qué rapidez rota el disco y la localización del bloque buscado en relación con el tiempo de activación de la cabeza de lectura/escritura. Físicamente este tiempo puede variar desde cero hasta el tiempo necesario para completar una revolución del disco (R). Ejemplo: Supongamos que desea montar sobre el caballo negro en el carrusel (asumiendo que existe sólo un caballo de este tipo). Si compra un ticket y corre a montarse en el carrusel, la probabilidad de que el caballo negro puede estar justo donde se detuvo sería la misma que para cualquiera de los otros caballos. Si fuera un fanático y lo intentara varias veces, puede que alguna vez se detenga frente al caballo negro, pero también puede encontrarse con la situación en que lo pierda y tenga que esperar por una vuelta completa del carrusel. Como media, espera media vuelta para montarse en el caballo negro. La moraleja de esta historia es que los cálculos de rendimiento generalmente asumen un retraso de rotación media de R/2. Transferencia de datos -Razón de transferencia de datos (D) se refiere a la cantidad de tiempo necesario para transferir los datos desde el disco hacia (o desde) la memoria principal. Depende de la velocidad de rotación y _____________________________________________________________________________ 69 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. de la cantidad de datos almacenados. El tiempo de transferencia de datos normalmente se expresa en cientos de bytes por segundo. -Tiempo de transferencia de datos El tiempo previsto (T) para acceder a una dirección en disco y transferir un bloque de datos se estima como T = P + R/2 + L/D, donde P es el tiempo de posicionamiento, R es el retraso de rotación, L es el tamaño del bloque en bytes y D es la velocidad de transferencia de datos. Ejemplo (registro accedido aleatoriamente). Supongamos que los registros de reclamación de una compañía de seguros se almacenan en bloques de tres registros en el disco (un factor de compactación de tres) y que cada registro de reclamación ocupa 200 bytes. La velocidad de transferencia de datos es de 806.000 bytes por segundo. El tiempo de posicionamiento medio es de 30 milisegundos. La unidad de disco rota a una velocidad de 3.600 vueltas por minuto. Supongamos que un usuario llama para averiguar el estado de una reclamación. ¿Cuál es el tiempo de transferencia de datos para buscar el bloque de datos? Para responder esta pregunta se asignan valores apropiados a las variables anteriores en la forma siguiente. A=0,030 seg. Revoluciones por segundos = 7.200/60=120. R= 1/120 seg. = 0,0083 seg. R/2 = 0, 0083 * ½(la media de espera es de media revolución) = 0, 00415 L/D = 600/806000 = 0,00074, por lo que T = 0,030 + 0, 00415 + 0, 00074 = 0, 03489 seg. Ejemplo (registro accedido secuencialmente). Ahora analizaremos el cálculo del tiempo medio de acceso a un registro en un archivo accedido secuencialmente. Supongamos que en vez de responder al acceso aleatorio de un bloque de datos, como en el ejemplo anterior, estamos actualizando el archivo de un usuario de la compañía de seguros con los pagos recibidos a principios de mes. Tiene sentido que estos archivos estén organizados secuencialmente por el número de la póliza y que estén localizados en bloques secuenciales por cilindros. Esto significa que primero se rellena el cilindro N con bloques secuenciales, después el N + 1 y así sucesivamente. De esta forma se minimiza el tiempo de movimiento de la cabeza. En particular, si las cabezas de lectura/escritura se encuentran en el primer cilindro, entonces todos los registros en este cilindro se transfieren sin un tiempo de posicionamiento adicional. Por lo que, en el cálculo del tiempo de acceso medio para cada registro de un archivo procesado secuencialmente, el tiempo de posicionamiento es despreciable y se ignora. Pudiera existir un pequeño retraso cada vez que la función de lectura/escritura cambia desde una pista de un cilindro hacia otra. Esto es necesario con el objetivo de disminuir pequeñas diferencias en la alineación de las pistas sobre diferentes superficies. Para nuestros propósitos, este retraso puede ser aproximadamente el tiempo necesario para dar una media vuelta del paquete de disco. Una vez se haya encontrado el registro inicial de la nueva pista, se pueden transmitir el resto de los bloques sobre la pista. Por tanto, si el archivo del usuario ocupa ocho pistas sobre el cilindro, el número de retraso de medias vueltas sería 8. Supongamos ahora que cada pista contiene 1.000 bloques. Tenemos un total de 8.000 bloques; si los mismos tienen un factor de compactación de tres, tenemos 24.000 registros de pólizas. Asumamos, como antes, que cada registro contiene 200 bytes, entonces nuestros bloques ocupan 600 bytes. Si procesamos secuencialmente un archivo completo, el tiempo medio para acceder a un registro se calcula como sigue: Tiempo total para leer todos los bloques = 0,00415 * (8) + 0,00074 * (8000) = 0,0664 + 5,92 = 5,9232 , T representa el tiempo de transferencia medio para un registro accedido secuencialmente en el archivo de pólizas. _____________________________________________________________________________ 70 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Aplicación: Suponga que tenemos almacenados registros en un dispositivo de disco que posee las siguientes características: Tiempo de posicionamiento medio: 0,02 seg., velocidad de rotación del disco: 3.600 revoluciones por minuto, velocidad de transferencia de datos: 312.000 bytes por segundo. ¿Cuál es el tiempo de transferencia de datos esperado para un registro físico accedido aleatoriamente que ocupa 500 bytes?. Volviendo al tema de los B-trees, digamos que los B-trees son ABB balanceados diseñados para trabajar sobre discos mágneticos u otros accesos o dispositivos de almacenemaiento secundario, también llamados periféricos. Los B-tree son similar a los red-black trees, pero mejores que ellos, pues minimizan los accesos al disco en operaciones de I/O. Muchos son los sistemas de BD que usan B-trees, o variantes de ellos, para almacenar la información. Una de las grandes diferencias de los B-tree con los red-black trees es que los nodos de los B-tree pueden tener muchos hijos, también llamado "branching factor". Sin embargo, esto está usualmente determinado por las características de la unidad de disco usado. ISAM (indexed sequential access method ) tiene una variante de B-tree en el manejo y rendimiento de su información, llamado B+-tree. Un B-tree T es un árbol (cuya raíz es root[T]) que tiene las siguientes propiedades: 1. Los datos en un nodo están ordenados 2. Un nodo contiene maximal T subárboles 3. Los datos (claves) del subárbol izquierdo son más pequeños que la raíz, así como los de la derecha son mayores. Existe una cota superior e inferior sobre el nº de claves que puede contener un nodo. Esta cota puede ser expresada en términos de un entero t ≥ 2 llamado el minimum degree del B-tree: a) Todo nodo que no sea la raíz debe tener al menos t - 1 claves. Todo nodo interno que no sea la raíz debe tener al menos t hijos. Si el árbol es vacío, la raíz debe tener al menos una clave. b) Todo nodo puede contener a lo más 2t - 1 claves. Por lo tanto, un nodo interno puede tener a lo más 2t hijos. Se dice que el nodo está lleno “full” si este contiene exactamente 2t – 1 claves. El B-tree más simple ocurre para cuando t = 2. Todo nodo interno entonces tiene ya sea 2, 3, o 4 hijos, también llamado 2-3-4 tree. El nº de accesos al disco requeridos para las operaciones sobre un B-tree es proporcional a la altura del B-tree. Ejemplo: Se muestra un B-tree de altura 2 que contiene sobre un billón de claves y más de un millón de nodos. Notar que cada nodo interno y hojas que contienen 1000 claves. Existiendo 1001 nodos en la profundidad 1 y sobre un millón de nodos en la profundidad 2 Basado en un B-tree de minimum degree t, se tienen las siguientes condiciones _____________________________________________________________________________ 71 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Teorema: Si n ≥ 1, entonces para cualquier n- claves en un B-tree T de altura h y un minimum degree t ≥ 2, h ≤ logt(n+1/2). A continuación se muestra un B-tree de altura h = 3 que contiene un nº posible de claves. n[x] corresponde al interior del nodo x. En general, n = 2th – 1. De manera que a partir de esta fórmula podemos encontrar una serie de información, pues ella involucra varias variables, tales como altura, minimum degree y el nº de nodos. Un B-tree de altura H se encuentra entre N min 2 (t ) h1 1 y N max 2 (t ) h 1 claves. Digamos que los árbol B-tree son una generalización de los árbol 2-3, pues aumenta el nº de enlaces que cada nodo puede tener. Denotemos este por M y supongamos que M = 1000. Se muestra a continuación un B-tree con M = 5. _____________________________________________________________________________ 72 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Búsqueda en un B-tree Parte desde la raíz. • Encuentra el intervalo para buscar la clave y toma su camino, (derecha) si es mayor e (izquierda) si es menor. • Sigue buscando hasta llegar a los nodos externo. Si lo encuentra Eureka, sino no esta. Ejemplo: Se muestra un ejemplo en donde se busca la clave E, considerando el mismo B-tree anterior. Inserción en un B-tree Buscamos el lugar apropiado para la nueva clave. • Insertamos y listo. • escindir (M+1)-nodos dentro del camino del árbol. Ejemplo: Consideremos la distribución e ingreso de las salas de clases de una Institución Educacional, considerando que M = 5. _____________________________________________________________________________ 73 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Otros B-tree variants son B+ tree, B*tree, B# tree, …y otros. Por ejemplo los árboles R… Ejemplo: Dada la figura R2 R1 Luego, podemos representarla a través de un árbol R como : _____________________________________________________________________________ 74 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) R1 Dr. Eric Jeltsch F. R2 R3 R4 R5 Alsace Auvergne Bourgogne R6 R7 R8 Aquitaine Basse Normandie Bretagne A continuación se muestra un ejemplo de un árbol R de orden (3,4), si nos fijamos en la figura hay tres rectángulos o cajas límites mayores que indican que el árbol debe tener tres hijos, y cada uno de esos hijos a su vez encierran a cajas limites que son las más pequeñas, que serán las que contendrá el nodo de cada hijo. Además, en la figura podemos observar la principal característica de un árbol R, donde las cajas limites definidas en el mismo nivel pueden solaparse (A, B y C). Resumen. El paquete java.util contiene varias clases utilitarias para ayudar al desarrollador en el diseño de diferentes tipos de estructuras de datos, entre ellas esta la nueva clase Collection implementada usando una forma genérica, dándole así mayor robustez a las propuestas de software. Entre otras, la interface Enumeration, la que se utiliza para implementar una clase capaz de enumerar sus valores. Su implementación facilita el recorrido de estructuras de datos. Tiene dos métodos: hasMoreElements y nextElement. hasMoreElements retorna true si quedan elementos por visitar en la estructura de datos, mientras que nextElement retorna el siguiente objeto en la estructura que se está enumerando. La clase Vector proporciona una manera fácil de implementar estructuras de datos dinámicas. Es eficiente pues asigna más memoria de la necesaria cuando se agrega nuevos elementos. capacityIncrement indica el incremento en capacidad cuando se agrega un elemento. A continuación los principales métodos. _____________________________________________________________________________ 75 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. JGL (Java Generics Library) mejora las funcionalidades del JDK. Los beneficios incluyen soporte para Serialización, multi-threads seguros, rendimiento óptimo, 11 estructuras de datos, 40 algoritmos y compatible con JDK. _____________________________________________________________________________ 76 Escuela de Ingeniería en Computación, Universidad de La Serena. Diseño y Análisis de Algoritmos. (DAA-2009) Dr. Eric Jeltsch F. Bibliografía utilizada para este segmento (1) Mark Allen Weiss, "Data Structures and Algorithm Analysis (in Java)", Addison_Wesley, 1999. (2) Gregory Heileman, “Estructuras de Datos, Algoritmos y Programación Orientada a Objetos”, Mc Graw Hill, 1997. (3) Cormen, Leiserson, Rivest, "Introduction to algorithms", McGraw-Hill, 1990. (4) Roberto Tamassia, http://www.cs.brown.edu/~rt/ (5) Bob Sedgewick y Kevin Wayne. http://www.cs.princeton.edu/algs4/43balanced/ _____________________________________________________________________________ 77 Escuela de Ingeniería en Computación, Universidad de La Serena.