Estructura de Datos Wikipedia)

January 4, 2017 | Author: Genaro Alberto Gómez Chi | Category: N/A
Share Embed Donate


Short Description

Download Estructura de Datos Wikipedia)...

Description

Estructura de datos En programación, una estructura de datos es una forma de organizar un conjunto de datos elementales con el objetivo de facilitar su manipulación. Un dato elemental es la mínima información que se tiene en un sistema. Una estructura de datos define la organización e interrelación de éstos y un conjunto de operaciones que se pueden realizar sobre ellos. Las operaciones básicas son: Alta, adicionar un nuevo valor a la estructura. Baja, borrar un valor de la estructura. Búsqueda, encontrar un determinado valor en la estructura para realizar una operación con este valor, en forma SECUENCIAL o BINARIO (siempre y cuando los datos estén ordenados)... Otras operaciones que se pueden realizar son: Ordenamiento, de los elementos pertenecientes a la estructura. Apareo, dadas dos estructuras originar una nueva ordenada y que contenga a las apareadas. Cada estructura ofrece ventajas y desventajas en relación a la simplicidad y eficiencia para la realización de cada operación. De esta forma, la elección de la estructura de datos apropiada para cada problema depende de factores como la frecuencia y el orden en que se realiza cada operación sobre los datos.

Vector (informática)

Arreglo unidimensional con 10 elementos

En programación, un array es un conjunto o agrupación de variables del mismo tipo cuyo acceso se realiza por índices. Los vectores o arreglos (array en inglés) de dos o más dimensiones se denominan con frecuencia matrices, y pueden tener tantas dimensiones como se desee; aunque para evitar confusiones con el concepto matemático de matriz numérica (que normalmente sólo tiene dos dimensiones), se suele utilizar el termino array (o arreglo) para referirse de forma genérica a matrices de cualquier número de dimensiones.

Introducción Desde el punto de vista de un programa de ordenador, un array (matriz o vector) es una zona de almacenamiento contiguo, que contiene una serie de elementos del mismo tipo, los elementos de la matriz. Desde el punto de vista lógico un array se puede ver como un conjunto de elementos ordenados en 1

fila (o filas y columnas si tuviera dos dimensiones). En principio, se puede considerar que todos los arrays son de una dimensión, la dimensión principal, pero los elementos de dicha fila pueden ser a su vez arrays (un proceso que puede ser recursivo), lo que nos permite hablar de la existencia de arrays multidimensionales, aunque los más fáciles de "mondaa" o imaginar son los de una, dos y tres dimensiones. Estas estructuras de datos son adecuadas para situaciones en las que el acceso a los datos se realice de forma aleatoria e impredecible. Por el contrario, si los elementos pueden estar ordenados y se va a utilizar acceso secuencial sería más adecuado utilizar una lista, ya que esta estructura puede cambiar de tamaño fácilmente durante la ejecución de un programa.

Índices Todo vector se compone de un determinado número de elementos. Cada elemento es referenciado por la posición que ocupa dentro del vector. Dichas posiciones son llamadas índice y siempre son correlativos. Existen tres formas de indexar los elementos de un array: Indexación base-cero (0): En este modo el primer elemento del vector será la componente cero ('0') del mismo, es decir, tendrá el indice '0'. En consecuencia, si el vector tiene 'n' componentes la última tendrá como índice el valor 'n-1'. El C es un ejemplo típico de lenguaje que utiliza este modo de indexación. Indexación base-uno (1): En esta forma de indexación, el primer elemento del array tiene el indice '1' y el último tiene el índice 'n' (para un array de 'n' componentes). Indexación base-n (n): Este es un modo versátil de indexación en la que el índice del primer elemento puede ser elegido libremente, en algunos lenguajes de programación se permite que los índices puedan ser negativos e incluso de cualquier tipo escalar (también cadenas de caracteres).

Notación La representación de un elemento en un vector se suele hacer mediante el identificador del vector seguido del índice entre corchetes, paréntesis o llaves: Notación Ejemplos vector[índice_1,índice_2...,índice_N] (Java, Léxico, etc.) vector[índice_1][índice_2]...[índice_N] (C, C++, PHP, etc.) vector(índice_1,índice_2...,índice_N) (Basic) vector{índice_1,índice_2...,índice_N} (Perl)

Aunque muchas veces en pseudocódigo y en libros de matemática se representan como letras acompañadas de un subíndice numérico que indica la posición a la que se quiere acceder. Por ejemplo, para un vector "A": A0,A1,A2,... (vector unidimensional)

2

Forma de Acceso La forma de acceder a los elementos del array es directa; esto significa que el elemento deseado es obtenido a partir de su índice y no hay que ir buscándolo elemento por elemento (en contraposición, en el caso de una lista, para llegar, por ejemplo, al tercer elemento hay que acceder a los dos anteriores o almacenar un apuntador o puntero que permita acceder de manera rápida a ese elemento. Para trabajar con vectores muchas veces es preciso recorrerlos. Esto se realiza por medio de bucles. El siguiente pseudocódigo muestra un algoritmo típico para recorrer un vector y aplicar una función 'f(...)' a cada una de las componentes del vector: i = 0 mientras (i < longitud) #Se realiza alguna operación con el vector en la i-ésima posición f(v[i]) i=i+1 fin_mientras

Vectores dinámicos Lo habitual es que un vector tenga una cantidad fija de memoria asignada, aunque dependiendo del tipo de vector y del lenguaje de programación un vector podría tener una cantidad variable de datos. En este caso, se los denomina vectores dinámicos, en oposición, a los vectores con una cantidad fija de memoria asignada se los denomina vectores estáticos. El uso de vectores dinámicos requiere realizar una apropiada gestión de memoria dinámica. Un uso incorrecto de los vectores dinámicos, o mejor dicho, una mala gestión de la memoria dinámica, puede conducir a una fuga de memoria (Error de software que ocurre cuando un bloque de memoria reservada no es liberado en un programa de computación. Comúnmente ocurre porque se pierden todas las referencias a esa área de memoria antes de haberse liberado. Dependiendo de la cantidad de memoria perdida y el tiempo que el programa siga en ejecución, este problema puede llevar al agotamiento de la memoria disponible en la computadora. Este problema se da principalmente en aquellos lenguajes de programación en los que el manejo de memoria es manual (C o C++ principalmente), y por lo tanto es el programador el que debe saber en qué momento exacto puede liberar la memoria. Otros lenguajes utilizan un recolector de basura que automáticamente efectúa esta liberación. Sin embargo todavía es posible la existencia de fugas en estos lenguajes si el programa acumula referencias a objetos, impidiendo así que el recolector llegue a considerarlos en desuso. Existen varias formas de luchar contra este problema. Una forma es el uso de un recolector de basura incluso en el caso en el que éste no sea parte estándar del lenguaje. El más conocido recolector de basura usado de esta manera es el Boehm-Demers-Weiser conservative garbage collector. Otras técnicas utilizadas son la adopción de esquemas de conteo de referencias o el uso de pools de memoria (técnica menos popular, utilizada en el servidor Apache y en el sistema de versiones Subversion). También hay herramientas para "auscultar" un programa y detectar las fugas. Una de las herramientas más conocidas es Valgrind). Al utilizar vectores dinámicos siempre habrá que liberar la memoria utilizada cuando ésta ya no se vaya a seguir utilizando.

3

Lenguajes más modernos y de más alto nivel, cuentan con un mecanismo denominado recolector de basura (como es el caso de Java) que permiten que el programa decida si debe liberar el espacio basándose en si se va a utilizar en el futuro o no un determinado objeto.

Ejemplos en C Declaración en C (o C++) de un vector estático.- La forma de crear vectores estáticos es igual que en C y C++. int v[5]; int i; for (i=0 ; inext = *p; *p = n; n->data = i; return n; } void list_remove(node **p) { /* borrar cabeza*/ if (*p != NULL) { node *n = *p; *p = (*p)->next; free(n); } } node **list_search(node **n, int i) { while (*n != NULL) { if ((*n)->data == i) { return n; } n = &(*n)->next; } return NULL; } void list_print(node *n) { if (n == NULL) { printf("lista esta vacía\n");

18

} while (n != NULL) { printf("print %p %p %d\n", n, n->next, n->data); n = n->next; } } int main(void) { node *n = NULL; list_add(&n, 0); /* lista: 0 */ list_add(&n, 1); /* lista: 1 0 */ list_add(&n, 2); /* lista: 2 1 0 */ list_add(&n, 3); /* lista: 3 2 1 0 */ list_add(&n, 4); /* lista: 4 3 2 1 0 */ list_print(n); list_remove(&n); /* borrar primero(4) */ list_remove(&n->next); /* borrar nuevo segundo (2) */ list_remove(list_search(&n, 1)); /* eliminar la celda que contiene (primera) */ list_remove(&n->next); /* eliminar segundo nodo del final(0)*/ list_remove(&n); /* eliminar ultimo nodo (3) */ list_print(n);

el

1

.

***

return 0; }

Y ahora una posible especificación de Listas Enlazadas en Maude fmod LISTA-GENERICA {X :: TRIV} is protecting NAT . *** tipos sorts ListaGenNV{X} ListaGen{X} . subsort ListaGenNV{X} < ListaGen{X} . *** generadores op crear : -> ListaGen{X} [ctor] . op cons : X$Elt ListaGen{X} -> ListaGenNV{X} [ctor] . *** constructores op _::_ : ListaGen{X} concatenacion

ListaGen{X}

->

ListaGen{X}

[assoc

id:

crear

]

op invertir : ListaGen{X} -> ListaGen{X} . op resto

: ListaGenNV{X} -> ListaGen{X} .

*** selectores

19

op primero : ListaGenNV{X} -> X$Elt . op esVacia? : ListaGen{X} -> Bool . op longitud : ListaGen{X} -> Nat . *** variables vars L L1 L2 : ListaGen{X} . vars E E1 E2 : X$Elt . *** ecuaciones eq esVacia?(crear) = true . eq esVacia?(cons(E, L)) = false . eq primero(cons(E, L)) = E . eq resto(cons(E, L)) = L . eq longitud(crear) = 0 . eq longitud(cons(E, L)) = 1 + longitud(L) . eq cons(E1, L1) :: cons(E2, L2) = cons(E1, L1 :: cons(E2, L2)) . eq invertir(crear) = crear . eq invertir(cons(E, L)) = invertir(L) :: cons(E, crear) . endfm

Almacenamiento interno y externo Cuando se construye una lista enlazada, nos enfrentamos a la elección de si almacenar los datos de la lista directamente en los nodos enlazados de la lista, llamado almacenamiento interno, o simplemente almacenar una referencia al dato, llamado almacenamiento externo. El almacenamiento interno tiene la ventaja de hacer accesos a los datos más eficientes, requiriendo menos almacenamiento global, teniendo mejor referencia de localidad, y simplifica la gestión de memoria para la lista (los datos son alojados y desalojados al mismo tiempo que los nodos de la lista). El almacenamiento externo, por otro lado, tiene la ventaja de ser más genérico, en la misma estructura de datos y código máquina puede ser usado para una lista enlazada, no importa cual sea su tamaño o los datos. Esto hace que sea más fácil colocar el mismo dato en múltiples listas enlazadas. Aunque con el almacenamiento interno los mismos datos pueden ser colocados en múltiples listas incluyendo múltiples referencias siguientes en la estructura de datos del nodo, esto podría ser entonces necesario para crear rutinas separadas para añadir o borrar celdas basadas en cada campo. Esto es posible creando listas enlazadas de elementos adicionales que usen almacenamiento interno usando almacenamiento externo, y teniendo las celdas de las listas enlazadas adicionales almacenadas las referencias a los nodos de las listas enlazadas que contienen los datos. 20

En general, si una serie de estructuras de datos necesita ser incluida en múltiples listas enlazadas, el almacenamiento externo es el mejor enfoque. Si una serie de estructuras de datos necesitan ser incluidas en una sola lista enlazada, entonces el almacenamiento interno es ligeramente mejor, a no ser que un paquete genérico de listas genéricas que use almacenamiento externo esté disponible. Asimismo, si diferentes series de datos que pueden ser almacenados en la misma estructura de datos son incluidos en una lista enlazada simple, entonces el almacenamiento interno puede ser mejor. Otro enfoque que puede ser usado con algunos lenguajes implica tener diferentes estructuras de datos, pero todas tienen los campos iniciales, incluyendo la siguiente (y anterior si es una lista doblemente enlazada) referencia en la misma localización. Después de definir estructuras distintas para cada tipo de dato, una estructura genérica puede ser definida para que contenga la mínima cantidad de datos compartidos por todas las estructuras y contenidos al principio de las estructuras. Entonces las rutinas genéricas pueden ser creadas usando las mínimas estructuras para llevar a cabo las operaciones de los tipos de las listas enlazadas, pero separando las rutinas que pueden manejar los datos específicos. Este enfoque es usado a menudo en rutinas de análisis de mensajes, donde varios tipos de mensajes son recibidos, pero todos empiezan con la misma serie de campos, generalmente incluyendo un campo para el tipo de mensaje. Las rutinas genéricas son usadas para añadir nuevos mensajes a una cola cuando son recibidos, y eliminarlos de la cola en orden para procesarlos. El campo de tipo de mensaje es usado para llamar a la rutina correcta para procesar el tipo específico de mensaje.

Ejemplos de almacenamiento interno y externo Suponiendo que queremos crear una lista enlazada de familias y sus miembros. Usando almacenamiento interno, la estructura podría ser como la siguiente: record member { // miembro de una familia member next string firstName integer age } record family { // // la propia familia family next string lastName string address member members // de la lista de miembros de la familia }

Para mostrar una lista completa de familias y sus miembros usando almacenamiento interno podríamos escribir algo como esto: aFamily := Families // comienzo de la lista de familias while aFamily ≠ null { // bucle a través de la lista de familias print information about family aMember := aFamily.members // coger cabeza de esta lista de miembros de esta familia while aMember ≠ null { //bucle para recorrer la lista de miembros print information about member aMember := aMember.next } aFamily := aFamily.next }

21

Usando almacenamiento externo, nosotros podríamos crear las siguientes estructuras: record node { // estructura genérica de enlace node next pointer data // puntero genérico del dato al nodo } record member { // estructura de una familia string firstName integer age } record family { // estructura de una familia string lastName string address node members // cabeza de la lista de miembros de esta familia }

Para mostrar una lista completa de familias y sus miembros usando almacenamiento externo, podríamos escribir: famNode := Families // comienzo de la cabeza de una lista de familias while famNode ≠ null { // bucle de lista de familias aFamily = (family) famNode.data // extraer familia del nodo print information about family memNode := aFamily.members // coger lista de miembros de familia while memNode ≠ null { bucle de lista de miembros aMember := (member) memNode.data // extraer miembro del nodo print information about member memNode := memNode.next } famNode := famNode.next }

Hay que fijarse en que cuando usamos almacenamiento externo, se necesita dar un paso extra para extraer la información del nodo y hacer un casting dentro del propio tipo del dato. Esto es porque ambas listas, de familias y miembros, son almacenadas en dos listas enlazadas usando la misma estructura de datos (nodo), y este lenguaje no tiene tipos paramétricos. Si conocemos el número de familias a las que un miembro puede pertenecer en tiempo de compilación, el almacenamiento interno trabaja mejor. Si, sin embargo, un miembro necesita ser incluido en un número arbitrario de familias, sabiendo el número específico de familias solo en tiempo de ejecución, el almacenamiento externo será necesario.

Agilización de la búsqueda Buscando un elemento específico en una lista enlazada, incluso si esta es ordenada, normalmente requieren tiempo O (n) (búsqueda lineal). Esta es una de las principales desventajas de listas enlazadas respecto a otras estructuras. Además algunas de las variantes expuestas en la sección anterior, hay numerosas vías simples para mejorar el tiempo de búsqueda. En una lista desordenada, una forma simple para decrementar el tiempo de búsqueda medio es el mover al frente de forma heurística, que simplemente mueve un elemento al principio de la lista una vez 22

que es encontrado. Esta idea, útil para crear cachés simples, asegura que el ítem usado más recientemente es también el más rápido en ser encontrado otra vez. Otro enfoque común es indizar una lista enlazada usando una estructura de datos externa más eficiente. Por ejemplo, podemos construir un árbol rojo-negro o una tabla hash cuyos elementos están referenciados por los nodos de las listas enlazadas. Pueden ser construidos múltiples índices en una lista simple. La desventaja es que estos índices puede necesitar ser actualizados cada vez que uno nodo es añadido o eliminado (o al menos, antes que el índice sea utilizado otra vez).

Estructuras de datos relacionadas Tanto las pilas como las colas son a menudo implementadas usando listas enlazadas, y simplemente restringiendo el tipo de operaciones que son soportadas. La skip list, o lista por saltos, es una lista enlazada aumentada con capas de punteros para saltos rápidos sobre grandes números de elementos, y descendiendo hacía la siguiente capa. Este proceso continúa hasta llegar a la capa inferior, la cual es la lista actual. Un árbol binario puede ser visto como un tipo de lista enlazada donde los elementos están enlazados entre ellos mismos de la misma forma. El resultado es que cada nodo puede incluir una referencia al primer nodo de una o dos listas enlazadas, cada cual con su contenido, formando así los subárboles bajo el nodo. Una lista enlazada desenrollada es una lista enlazada cuyos nodos contiene un array de datos. Esto mejora la ejecución de la caché, siempre que las listas de elementos estén contiguas en memoria, y reducen la sobrecarga de la memoria, porque necesitas menos metadatos para guardar cada elemento de la lista. Una tabla hash puede usar listas enlazadas para guardar cadenas de ítems en la misma posición de la tabla hash.

Referencias 1. ↑ Preiss, Bruno R. (1999), Data Structures and Algorithms with Object-Oriented Design Patterns in Java, Wiley, p. page 97, 165, ISBN 0471-34613-6, http://www.brpreiss.com/books/opus5/html/page97.html 2. ↑ If maintaining a link to the tail of the list, time is O(1); if the entire list must be searched to locate the tail link, O(n) National Institute of Standards and Technology (August 16, 2004). Definition of a linked list. Retrieved December 14, 2004. Antonakos, James L. and Mansfield, Kenneth C., Jr. Practical Data Structures Using C/C++ (1999). PrenticeHall. ISBN 0-13-280843-9, pp. 165–190 Collins, William J. Data Structures and the Java Collections Framework (2002,2005) New York, NY: McGraw Hill. ISBN 0-07-282379-8, pp. 239–303 Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford Introductions to Algorithms (2003). MIT Press. ISBN 0-262-03293-7, pp. 205–213, 501–505 Green, Bert F. Jr. (1961). Computer Languages for Symbol Manipulation. IRE Transactions on Human Factors in Electronics. 2 pp. 3-8. 23

McCarthy, John (1960). Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I. Communications of the ACM. [1] HTML DVI PDF PostScript Donald Knuth. Fundamental Algorithms, Third Edition. Addison-Wesley, 1997. ISBN 0-201-89683-4. Sections 2.2.3–2.2.5, pp.254–298. Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein. Introduction to Algorithms, Second Edition. MIT Press and McGraw-Hill, 2001. ISBN 0-262-03293-7. Section 10.2: Linked lists, pp.204– 209. Newell, Allen and Shaw, F. C. (1957). Programming the Logic Theory Machine. Proceedings of the Western Joint Computer Conference. pp. 230-240. Parlante, Nick (2001). Linked list basics. Stanford University. PDF Sedgewick, Robert Algorithms in C (1998). Addison Wesley. ISBN 0-201-31452-5, pp. 90–109 Shaffer, Clifford A. A Practical Introduction to Data Structures and Algorithm Analysis (1998). NJ: Prentice Hall. ISBN 0-13-660911-2, pp. 77–102 Wilkes, Maurice Vincent (1964). An Experiment with a Self-compiling Compiler for a Simple List-Processing Language. Annual Review in Automatic Programming 4, 1. Published by Pergamon Press. Wilkes, Maurice Vincent (1964). Lists and Why They are Useful. Proceeds of the ACM National Conference, Philadelphia 1964 (ACM Publication P-64 page F1-1); Also Computer Journal 7, 278 (1965). Kulesh Shanmugasundaram (April 4, 2005). Linux Kernel Linked List Explained.

Skip list Una skip list o lista por saltos es una Estructura de datos, basada en Listas enlazadas paralelas con eficiencia comparable a la de un árbol binario (tiempo en orden O(log n) para la mayoría de las operaciones). Una lista por saltos se construye por capas. La capa del fondo es una sencilla lista enlazada. Cada capa subsiguiente es como una "vía rápida" para la lista de la capa anterior. Un elemento de la capa i aparece en la capa i+1 con una probabilidad fija p. En promedio, cada elemento aparece en 1/(1-p) listas, el elemento más alto (generalmente un elemento inicial colocado al principio de la lista por saltos) aparece en O(log(1/p) n) listas.

Para buscar un elemento, se inicia con el elemento inicial de la lista de la capa más alta hasta alcanzar el máximo elemento que es menor o igual al buscado, se pasa a la capa anterior y se continua la búsqueda. Se puede verificar que el número esperado de pasos en cada lista enlazada es 1/p. De manera que el costo total de búsqueda es O(log(1/p) n / p), que es lo mismo que O(log n) cuando p es una constante. Dependiendo del valor escogido para p, se puede favorecer el costo de búsqueda contra el costo de almacenamiento.

24

Las operaciones de inserción y borrado se implantan como las de sus correspondientes listas enlazadas, salvo que los elementos de las capas superiores deben ser insertados o borrados de más de una lista enlazada. A diferencia de los árboles de búsqueda balanceados, el peor caso para las operaciones de listas por saltos no está garantizado como logarítmico, dado que es posible aunque poco probable, que se produzca una estructura no balanceada. Sin embargo, las listas por saltos trabajan bien en la práctica y el esquema de balanceo es más sencillo de implementar que el de los árboles binarios balanceados. Las listas por saltos son útiles también para cómputo paralelo, dado que se pueden realizar inserciones en paralelo sobre segmentos diferentes sin tener luego que balancear la estructura.

Origen Las listas por saltos fueron creadas por William Pugh y publicadas en su artículo Skip lists: a probabilistic alternative to balanced trees in Communications of the ACM, June 1990, 33(6) 668-676. Véase también en [1]. El creador de la estructura de datos las describe así: Las listas por saltos son una estructura probabilística que podría remplazar los árboles balanceados como método de implementación preferido en muchas aplicaciones. Las operaciones de listas por saltos tienen el mismo comportamiento asintótico esperado que las de los árboles balanceados, son más rápidas y utilizan menos espacio.

Pila (informática) Una pila (stack en inglés) es una lista ordinal o estructura de datos en la que el modo de acceso a sus elementos es de tipo LIFO (del inglés Last In First Out, último en entrar, primero en salir) que permite almacenar y recuperar datos. Se aplica en multitud de ocasiones en informática debido a su simplicidad y ordenación implícita en la propia estructura. Para el manejo de los datos se cuenta con dos operaciones básicas: apilar (push), que coloca un objeto en la pila, y su operación inversa, retirar (o desapilar, pop), que retira el último elemento apilado. En cada momento sólo se tiene acceso a la parte superior de la pila, es decir, al último objeto apilado (denominado TOS, Top of Stack en inglés). La operación retirar permite la obtención de este 25

elemento, que es retirado de la pila permitiendo el acceso al siguiente (apilado con anterioridad), que pasa a ser el nuevo TOS. Por analogía con objetos cotidianos, una operación apilar equivaldría a colocar un plato sobre una pila de platos, y una operación retirar a retirarlo. Las pilas suelen emplearse en los siguientes contextos: Evaluación de expresiones en notación postfija (notación polaca inversa). Reconocedores sintácticos de lenguajes independientes del contexto Implementación de recursividad.

Pila de llamadas La pila de llamadas es un segmento de memoria que utiliza esta estructura de datos para almacenar información sobre las llamadas a subrutinas actualmente en ejecución en un programa en proceso. Cada vez que una nueva subrutina es llamada, se apila una nueva entrada con información sobre ésta tal como sus variables locales. En especial, se almacena aquí el punto de retorno al que regresar cuando esta subrutina termine (para volver a la subrutina anterior y continuar su ejecución después de esta llamada).

Pila como tipo abstracto de datos A modo de resumen tipo de datos, la pila es un contenedor de nodos y tiene dos operaciones básicas: push (o apilar) y pop (o desapilar). 'Push' añade un nodo a la parte superior de la pila, dejando por debajo el resto de los nodos. 'Pop' elimina y devuelve el actual nodo superior de la pila. Una metáfora que se utiliza con frecuencia es la idea de una pila de platos en una cafetería con muelle de pila. En esa serie, sólo la primera placa es visible y accesible para el usuario, todas las demás placas permanecen ocultas. Como se añaden las nuevas placas, cada nueva placa se convierte en la parte superior de la pila, escondidos debajo de cada plato, empujando a la pila de placas. A medida que la placa superior se elimina de la pila, la segunda placa se convierte en la parte superior de la pila. Dos principios importantes son ilustrados por esta metáfora: En primer lugar la última salida es un principio, la segunda es que el contenido de la pila está oculto. Sólo la placa de la parte superior es visible, por lo que para ver lo que hay en la tercera placa, el primer y segundo platos tendrán que ser retirados.

Operaciones Una pila cuenta con 2 operaciones imprescindibles: apilar y desapilar, a las que en las implementaciones modernas de las pilas se suelen añadir más de uso habitual. Crear: se crea la pila vacía. Apilar: se añade un elemento a la pila.(push) Desapilar: se elimina el elemento frontal de la pila.(pop) Cima: devuelve el elemento que esta en la cima de la pila. (top o peek) Vacía: devuelve cierto si la pila está vacía o falso en caso contrario. 26

Implementación Un requisito típico de almacenamiento de una pila de n elementos es O (n). El requisito típico de tiempo de O (1) las operaciones también son fáciles de satisfacer con un array o con listas enlazadas simples. La biblioteca de plantillas de C++ estándar proporciona una "pila" clase templated que se limita a sólo apilar/desapilar operaciones. Java contiene una biblioteca de la clase Pila que es una especialización de Vector. Esto podría ser considerado como un defecto, porque el diseño heredado get () de Vector método LIFO ignora la limitación de la Pila. Estos son ejemplos sencillos de una pila con las operaciones descritas anteriormente (pero no hay comprobación de errores):

En Python class Stack(object): def __init__(self): self.stack_pointer = None def push(self, element): self.stack_pointer = Node(element, self.stack_pointer) def pop(self): e = self.stack_pointer.element self.stack_pointer = self.stack_pointer.next return e def peek(self): return self.stack_pointer.element def __len__(self): i = 0 sp = self.stack_pointer while sp: i += 1 sp = sp.next return i class Node(object): def __init__(self, element=None, next=None): self.element = element self.next = next if __name__ == '__main__': # small use example s = Stack() [s.push(i) for i in xrange(10)] print [s.pop() for i in xrange(len(s))]

27

En Maude La PilaNV es la pila no vacía, que diferenciamos de la pila normal a la hora de tomar en cuenta errores. El elemento X representa el tipo de valor que puede contener la pila: entero, carácter, registro.... fmod PILA-GENERICA {X :: TRIV} is sorts Pila{X} PilaNV{X}. subsorts PilaNV{X} < Pila{X}. ***generadores: op crear: -> Pila {X} [ctor]. op apilar : X$Elt Pila{X} -> PilaNV{X} [ctor]. ***constructores op desapilar : Pila{X} -> Pila{X}. ***selectores op cima : PilaNV{X} -> X$Elt. ***variables var P : Pila{X}. var E : X$Elt. ***ecuaciones eq desapilar (crear) = crear. eq desapilar (apilar(E, P)) = P. eq cima (apilar(E, P)) = E. endfm

En C++ #ifndef PILA #define PILA // define la pila template class Pila { private: struct Nodo { T elemento; Nodo* siguiente; // coloca el nodo en la segunda posicion }* ultimo; unsigned int elementos; public: Pila() { elementos = 0; } ~Pila() { while (elementos != 0) pop(); } void push(const T& elem) {

28

Nodo* aux = new Nodo; aux->elemento = elem; aux->siguiente = ultimo; ultimo = aux; ++elementos; } void pop() { Nodo* aux = ultimo; ultimo = ultimo->siguiente; delete aux; --elementos; } T cima() const { return ultimo->elemento; } bool vacia() const { return elementos == 0; } unsigned int altura() const { return elementos; } }; #endif

En Pascal UNIT Pila; INTERFACE Uses Elemento; Type TPila=^TNodo; TNodo=RECORD info:TElemento; ant:TPila; END; PROCEDURE CrearPilaVacia (VAR p:Tpila); PROCEDURE InsertarEnPila (e:TElemento; VAR p:TPila); PROCEDURE Cima(p:TPila; VAR c:TElemento); FUNCTION EsPilaVacia(p.Tpila):boolean; PROCEDURE Desapilar (VAR p: TPila); IMPLEMENTATION PROCEDURE CrearPilaVacia (VAR p:Tpila); BEGIN Destruir(p); p:=NIL; END

29

PROCEDURE InsertarEnPila (e:TElemento; VAR p:TPila); VAR paux:TPila; BEGIN new(paux); paux^.info:=e; paux^.ant:=p; p:=paux; END; PROCEDURE Cima(p:TPila; VAR c:TElemento); BEGIN e:=p^.info; END; FUNCTION EsPilaVacia(p: TPila): Boolean; BEGIN EsPilaVacia := (p=NIL); END; PROCEDURE Desapilar (VAR p: TPila); VAR auxPNodo: TPila; BEGIN IF NOT EsPilaVacia(p) THEN BEGIN auxPNodo:=p; p:=p^.ant; dispose(auxPNodo); END; END; PROCEDURE Destruir (VAR p:TPila); BEGIN WHILE NOT EsPilaVacia(p) DO Desapilar(pila); END; END.

Estructuras de datos relacionadas El tipo base de la estructura FIFO (el primero en entrar es el primero en salir) es la cola, y la combinación de las operaciones de la pila y la cola es proporcionado por el deque. Por ejemplo, el cambio de una pila en una cola en un algoritmo de búsqueda puede cambiar el algoritmo de búsqueda en primera profundidad (en inglés, DFS) por una búsqueda en amplitud (en inglés, BFS). Una pila acotada es una pila limitada a un tamaño máximo impuesto en su especificación.

Pilas Hardware Un uso muy común de las pilas a nivel de arquitectura hardware es la asignación de memoria.

30

Arquitectura básica de una pila Una pila típica es un área de la memoria de los computadores con un origen fijo y un tamaño variable. Al principio, el tamaño de la pila es cero. Un puntero de pila, por lo general en forma de un registro de hardware, apunta a la más reciente localización en la pila; cuando la pila tiene un tamaño de cero, el puntero de pila de puntos en el origen de la pila. Las dos operaciones aplicables a todas las pilas son: Una operación apilar, en el que un elemento de datos se coloca en el lugar apuntado por el puntero de pila, y la dirección en el puntero de pila se ajusta por el tamaño de los datos de partida. Una operación desapilar: un elemento de datos en la ubicación actual apuntado por el puntero de pila es eliminado, y el puntero de pila se ajusta por el tamaño de los datos de partida.

Hay muchas variaciones en el principio básico de las operaciones de pila. Cada pila tiene un lugar fijo en la memoria en la que comienza. Como los datos se añadirán a la pila, el puntero de pila es desplazado para indicar el estado actual de la pila, que se expande lejos del origen (ya sea hacia arriba o hacia abajo, dependiendo de la aplicación concreta). Por ejemplo, una pila puede comenzar en una posición de la memoria de mil, y ampliar por debajo de las direcciones, en cuyo caso, los nuevos datos se almacenan en lugares que van por debajo de 1000, y el puntero de pila se decrementa cada vez que un nuevo elemento se agrega. Cuando un tema es eliminado de la pila, el puntero de pila se incrementa. Los punteros de pila pueden apuntar al origen de una pila o de un número limitado de direcciones, ya sea por encima o por debajo del origen (dependiendo de la dirección en que crece la pila), sin embargo el puntero de pila no puede cruzar el origen de la pila. En otras palabras, si el origen de la pila está en la dirección 1000 y la pila crece hacia abajo (hacia las direcciones 999, 998, y así sucesivamente), el puntero de pila nunca debe ser incrementado más allá de 1000 (para 1001, 1002, etc.) Si un desapilar operación en la pila hace que el puntero de pila se deje atrás el origen de la pila, una pila se produce desbordamiento. Si una operación de apilar hace que el puntero de pila incremente o decremente más allá del máximo de la pila, en una pila se produce desbordamiento. La pila es visualizada ya sea creciente de abajo hacia arriba (como pilas del mundo real), o, con el máximo elemento de la pila en una posición fija, o creciente, de izquierda a derecha, por lo que el máximo elemento se convierte en el máximo a "la derecha". Esta visualización puede ser independiente de la estructura real de la pila en la memoria. Esto significa que rotar a la derecha es mover el primer elemento a la tercera posición, la segunda a la primera y la tercera a la segunda. Aquí hay dos equivalentes visualizaciones de este proceso: Manzana Plátano Fresa Fresa

Plátano ==rotar a la derecha==>

Fresa Manzana Manzana

31

Plátano

==rotar a la izquierda==>

Manzana

Fresa Plátano

Una pila es normalmente representada en los ordenadores por un bloque de celdas de memoria, con los "de abajo" en una ubicación fija, y el puntero de pila de la dirección actual de la "cima" de células de la pila. En la parte superior e inferior se utiliza la terminología con independencia de que la pila crece realmente a la baja de direcciones de memoria o direcciones de memoria hacia mayores. Apilando un elemento en la pila, se ajusta el puntero de pila por el tamaño de elementos (ya sea decrementar o incrementar, en función de la dirección en que crece la pila en la memoria), que apunta a la próxima celda, y copia el nuevo elemento de la cima en área de la pila. Dependiendo de nuevo sobre la aplicación exacta, al final de una operación de apilar, el puntero de pila puede señalar a la siguiente ubicación no utilizado en la pila, o tal vez apunte al máximo elemento de la pila. Si la pila apunta al máximo elemento de la pila, el puntero de pila se actualizará antes de que un nuevo elemento se apile, si el puntero que apunta a la próxima ubicación disponible en la pila, que se actualizará después de que el máximo elemento se apile en la pila. Desapilando es simplemente la inversa de apilar. El primer elemento de la pila es eliminado y el puntero de pila se actualiza, en el orden opuesto de la utilizada en la operación de apilar.

Soporte de Hardware Muchas CPUs tienen registros que se pueden utilizar como punteros de pila. Algunos, como el Intel x86, tienen instrucciones especiales que implícitamente el uso de un registro dedicado a la tarea de ser un puntero de pila. Otros, como el DEC PDP-11 y de la familia 68000 de Motorola tienen que hacer frente a los modos de hacer posible la utilización de toda una serie de registros como un puntero de pila. La serie Intel 80x87 numérico de coprocessors tiene un conjunto de registros que se puede acceder ya sea como una pila o como una serie de registros numerados. Algunos microcontroladores, por ejemplo algunos PICs, tienen un fondo fijo de pila que no es directamente accesible. También hay una serie de microprocesadores que aplicar una pila directamente en el hardware: Computer vaqueros MuP21 Harris RTX línea Novix NC4016

Muchas pilas basadas en los microprocesadores se utilizan para aplicar el lenguaje de programación Forth en el nivel de microcódigo. Pila también se utilizaron como base de una serie de mainframes y miniordenadores. Esas máquinas fueron llamados pila de máquinas, el más famoso es el Burroughs B5000

Soporte de Software En programas de aplicación escrito en un lenguaje de alto nivel, una pila puede ser implementada de manera eficiente, ya sea usando vectores o listas enlazadas. En LISP no hay necesidad de aplicar la pila, ya que las funciones apilar y desapilar están disponibles para cualquier lista. Adobe PostScript también está diseñada en torno a una pila que se encuentra directamente visible y manipuladas por el 32

programador. El uso de las pilas está muy presente en el desarrollo de software por ello la importancia de las pilas como tipo abstracto de datos.

Expresión de evaluación y análisis sintáctico sintaxis Se calcula empleando la notación polaca inversa utilizando una estructura de pila para los posibles valores. Las expresiones pueden ser representadas en prefijo, infijo, postfijo. La conversión de una forma de la expresión a otra forma necesita de una pila. Muchos compiladores utilizan una pila para analizar la sintaxis de las expresiones, bloques de programa, etc. Antes de traducir el código de bajo nivel. La mayoría de los lenguajes de programación son de contexto libre de los idiomas que les permite ser analizados con máquinas basadas en la pila. Por ejemplo, el cálculo: ((1 + 2) * 4) + 3, puede ser anotado como en notación postfija con la ventaja de no prevalecer las normas y los paréntesis necesarios: 12+4*3+ La expresión es evaluada de izquierda a derecha utilizando una pila: Apilar cuando se enfrentan a un operando y Desafilar dos operandos y evaluar el valor cuando se enfrentan a una operación. Apilar el resultado.

De la siguiente manera (la Pila se muestra después de que la operación se haya llevado a cabo): ENTRADA 1 2 + 4 * 3 +

OPERACION Apilar operando Apilar operando Añadir Apilar operando Multiplicar Apilar operando Añadir

PILA 1 1, 2 3 3, 4 12 12, 3 15

El resultado final, 15, se encuentra en la parte superior de la pila al final del cálculo.

Tiempo de ejecución de la gestión de memoria Pila basada en la asignación de memoria y Pila máquina. Una serie de lenguajes de programación están orientadas a la pila, lo que significa que la mayoría definen operaciones básicas (añadir dos números, la impresión de un carácter) cogiendo sus argumentos de la pila, y realizando de nuevo los valores de retorno en la pila. Por ejemplo, PostScript tiene una pila de retorno y un operando de pila, y también tiene un montón de gráficos estado y un diccionario de pila. Forth utiliza dos pilas, una para pasar argumentos y una subrutina de direcciones de retorno. El uso de una pila de retorno es muy común, pero el uso poco habitual de un argumento para una pila legible para humanos es el lenguaje de programación Forth razón que se denomina una pila basada en el idioma. 33

Muchas máquinas virtuales también están orientadas hacia la pila, incluida la p-código máquina y la máquina virtual Java. Casi todos los entornos de computación de tiempo de ejecución de memoria utilizan una pila especial PILA para tener información sobre la llamada de un procedimiento o función y de la anidación con el fin de cambiar al contexto de la llamada a restaurar cuando la llamada termina. Ellos siguen un protocolo de tiempo de ejecución entre el que llama y el llamado para guardar los argumentos y el valor de retorno en la pila. Pila es una forma importante de apoyar llamadas anidadas o a funciones recursivas. Este tipo de pila se utiliza implícitamente por el compilador para apoyar CALL y RETURN estados (o sus equivalentes), y no es manipulada directamente por el programador. Algunos lenguajes de programación utilizar la pila para almacenar datos que son locales a un procedimiento. El espacio para los datos locales se asigna a los temas de la pila cuando el procedimiento se introduce, y son borradas cuando el procedimiento termina. El lenguaje de programación C es generalmente aplicado de esta manera. Utilizando la misma pila de los datos y llamadas de procedimiento tiene importantes consecuencias para la seguridad (ver más abajo), de los que un programador debe ser consciente, a fin de evitar la introducción de graves errores de seguridad en un programa.

Solucionar problemas de búsqueda La búsqueda de la solución de un problema, es independientemente de si el enfoque es exhaustivo u óptimo, necesita espacio en la pila. Ejemplos de búsqueda exhaustiva métodos son fuerza bruta y backtraking. Ejemplos de búsqueda óptima a explorar métodos, son branch and bound y soluciones heurísticas. Todos estos algoritmos utilizan pilas para recordar la búsqueda de nodos que se han observado, pero no explorados aún. La única alternativa al uso de una pila es utilizar la recursividad y dejar que el compilador sea recursivo (pero en este caso el compilador todavía está utilizando una pila interna). El uso de pilas es frecuente en muchos problemas, que van desde almacenar la profundidad de los árboles hasta resolver crucigramas o jugar al ajedrez por ordenador. Algunos de estos problemas pueden ser resueltos por otras estructuras de datos como una cola.

Seguridad La seguridad a la hora de desarrollar software usando estructuras de datos de tipo pila es un factor a tener en cuenta debido a cierta vulnerabilidad que un uso incorrecto de éstas puede originar en la seguridad de nuestro software o en la seguridad del propio sistema que lo ejecuta. Por ejemplo, algunos lenguajes de programación usan una misma pila para almacenar los datos para un procedimiento y el link que permite retornar a su invocador. Esto significa que el programa introduce y extrae los datos de la misma pila en la que se encuentra información crítica con las direcciones de retorno de las llamadas a procedimiento, supongamos que al introducir datos en la pila lo hacemos en una posición errónea de manera que introducimos una datos de mayor tamaño al soportado por la pila corrompiendo así las llamadas a procedimientos provocaríamos un fallo en nuestro programa. Ésta técnica usada de forma maliciosa (es similar pero en otro ámbito al buffer overflow) permitiría a un atacante modificar el funcionamiento normal de nuestro programa y nuestro sistema, y es al menos una técnica útil si no lo evitamos en lenguajes muy populares como el ejemplo C.

34

Cola (informática) Una cola es una estructura de datos, caracterizada por ser una secuencia de elementos en la que la operación de inserción push se realiza por un extremo y la operación de extracción pop por el otro. También se le llama estructura FIFO (del inglés First In First Out), debido a que el primer elemento en entrar será también el primero en salir. Las colas se utilizan en sistemas informáticos, transportes y operaciones de investigación (entre otros), dónde los objetos, personas o eventos son tomados como datos que se almacenan y se guardan mediante colas para su posterior procesamiento. Este tipo de estructura de datos abstracta se implementa en lenguajes orientados a objetos mediante clases, en forma de listas enlazadas.

Usos concretos de la cola [editar] La particularidad de una estructura de datos de cola es el hecho de que sólo podemos acceder al primer y al último elemento de la estructura. Así mismo, los elementos sólo se pueden eliminar por el principio y sólo se pueden añadir por el final de la cola.

Ejemplos de colas en la vida real serían: personas comprando en un supermercado, esperando para entrar a ver un partido de béisbol, esperando en el cine para ver una película, una pequeña peluquería, etc. La idea esencial es que son todas líneas de espera. En estos casos, el primer elemento de la lista realiza su función (pagar comida, pagar entrada para el partido o para el cine) y deja la cola. Este movimiento está representado en la cola por la función pop o desencolar. Cada vez que otro elemento se añade a la lista de espera se añaden al final de la cola representando la función push o encolar. Hay otras funciones auxiliares para ver el tamaño de la cola (size), para ver si está vacía en el caso de que no haya nadie esperando (empty) o para ver el primer elemento de la cola (front).

35

Información adicional Teóricamente, la característica de las colas es que tienen una capacidad específica. Por muchos elementos que contengan siempre se puede añadir un elemento más y en caso de estar vacía borrar un elemento sería imposible hasta que no se añade un nuevo elemento. A la hora de añadir un elemento podríamos darle una mayor importancia a unos elementos que a otros (un cargo VIP) y para ello se crea un tipo de cola especial que es la cola de prioridad. (Ver cola de prioridad).

Operaciones Básicas Crear: se crea la cola vacía. Encolar (añadir, entrar, push): se añade un elemento a la cola. Se añade al final de esta. Desencolar (sacar, salir, pop): se elimina el elemento frontal de la cola, es decir, el primer elemento que entró. Frente (consultar, front): se devuelve el elemento frontal de la cola, es decir, el primero elemento que entró.

Implementaciones Colas en Maude La ColaNV es la cola no vacía, que diferenciamos de la cola normal a la hora de tomar en cuenta errores. A su vez, el elemento X representa el tipo de valor que puede contener la cola: entero, carácter, registro.... fmod COLA {X :: TRIV} is sorts ColaNV{X} Cola{X} . subsort ColaNV{X} < Cola{X} . *** generadores op crear : -> Cola{X} [ctor] . op encolar : X$Elt Cola{X} -> ColaNV {X} [ctor] . *** constructores op desencolar : Cola{X} -> Cola{X} . *** selectores op frente : ColaNV{X} -> X$Elt . *** variables var C : ColaNV{X} . vars E E2 : X$Elt . *** ecuaciones eq desencolar(crear) = crear . eq desencolar(encolar(E, crear)) = crear . eq desencolar(encolar(E, C)) = encolar(E, desencolar(C)) . eq frente(encolar(E, crear)) = E . eq frente(encolar(E, C)) = frente(C) . endfm

36

Especificación de una cola de colas de enteros en Maude: view VInt from TRIV to INT is sort Elt to Int . endv view VColaInt from TRIV to COLA{VInt} is sort Elt to Cola{VInt} . endv fmod COLA-COLAS-INT is protecting INT . protecting COLA{VColaInt} . *** operaciones propias de la cola de colas de enteros op encolarInt : Int ColaNV{VColaInt} -> ColaNV{VColaInt} . op desencolarInt : Cola{VColaInt} -> Cola{VColaInt} . op frenteInt : ColaNV{VColaInt} -> [Int] . *** var var var var

variables CCNV : ColaNV{VColaInt} . CC : Cola{VColaInt} . CE : Cola{VInt} . E : Int .

*** ecuaciones eq encolarInt(E, encolar(CE, CC)) = encolar(encolar(E, CE), CC) . eq desencolarInt (encolar(CE, crear)) = encolar(desencolar(CE), crear) . eq desencolarInt (encolar(CE, CCNV)) = encolar(CE, desencolarInt(CCNV)) . eq frenteInt(CCNV) = frente(frente(CCNV)) . endfm

Colas en C++ #ifndef COLA #define COLA // define la cola template class Cola { private: struct Nodo { T elemento; Nodo* siguiente; // coloca el nodo en la segunda posicion }* primero; Nodo* ultimo; unsigned int elementos; public: Cola() { elementos = 0; }

37

~Cola() { while (elementos != 0) pop(); } void push(const T& elem) { Nodo* aux = new Nodo; aux->elemento = elem; if (elementos == 0) primero = aux; else ultimo->siguiente = aux; ultimo = aux; ++elementos; } void pop() { Nodo* aux = primero; primero = primero->siguiente; delete aux; --elementos; } T consultar() const { return primero->elemento; } bool vacia() const { return elementos == 0; } unsigned int size() const { return elementos; } }; #endif

Colas en JAVA public void inserta(Elemento x) { Nodo Nuevo; Nuevo=new Nodo(x, null); if (NodoCabeza==null) NodoCabeza=Nuevo; else NodoFinal.Siguiente=Nuevo; NodoFinal=Nuevo; } public Elemento cabeza()throws IllegalArgumentException { if (NodoCabeza == null) throw new IllegalArgumentException(); else return NodoCabeza.Info; } public Cola(){ // Devuelve una Cola vacía NodoCabeza=null;

38

NodoFinal=null; }

Colas en C# public partial class frmPrincipal { // Variables globales public static string[] Cola; public static int Frente; public static int Final; public static int N; [STAThread] public static void Main(string[] args) { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new frmPrincipal()); } public frmPrincipal() {

// Constructor

InitializeComponent(); Cola = new string[5]; N = 4; Frente = -1; Final = -1;

// Arreglo lineal de 5

} void CmdInsercionClick(object sender, System.EventArgs e) { frmInsercion Insercion = new frmInsercion(); Insercion.Show(); } void CmdRecorridoClick(object sender, System.EventArgs e) { frmRecorrido Recorrido = new frmRecorrido(); Recorrido.Show(); } void CmdBusquedaClick(object sender, EventArgs e) { frmBusqueda Busqueda = new frmBusqueda(); Busqueda.Show(); } void CmdEliminacionClick(object sender, EventArgs e) { frmEliminacion Eliminar = new frmEliminacion(); Eliminar.Show(); } }

39

Algoritmo Insertar(Cola, N, Frente, Final, Elemento) void CmdInsertarClick(object sender, System.EventArgs e) { elemento = txtInsercion.Text; // Se verifica que haya espacio en la Cola if (frmPrincipal.Frente == 0 && frmPrincipal.Final == frmPrincipal.N) { MessageBox.Show("La Cola esta llena"); return; } if (frmPrincipal.Frente == frmPrincipal.Final + 1) { MessageBox.Show("La Cola esta llena"); return; } // Si la cola esta vacia se inicializan punteros if (frmPrincipal.Frente == -1) { frmPrincipal.Frente = 0; frmPrincipal.Final = 0; } else if (frmPrincipal.Final == frmPrincipal.N) { frmPrincipal.Final = 0; } else { frmPrincipal.Final = frmPrincipal.Final + 1; } // Se agrega elemento a la Cola frmPrincipal.Cola[frmPrincipal.Final] = elemento; txtInsercion.Text = ""; }

Algoritmo Eliminación (Cola, Frente, Final, N) void CmdEliminarClick(object sender, EventArgs e) { if (frmPrincipal.Frente == -1) { MessageBox.Show("Cola Vacia"); return; } string elemento = frmPrincipal.Cola[frmPrincipal.Frente]; // si la cola tiene un solo elemento if (frmPrincipal.Frente == frmPrincipal.Final) { frmPrincipal.Frente = -1; frmPrincipal.Final = -1; } else if (frmPrincipal.Frente == frmPrincipal.N)

40

{ frmPrincipal.Frente = 0; } else { frmPrincipal.Frente = frmPrincipal.Frente + 1; } lsEliminado.Items.Add(elemento); }

Tipos de colas Colas circulares (anillos): en las que el último elemento y el primero están unidos. Colas de prioridad: En ellas, los elementos se atienden en el orden indicado por una prioridad asociada a cada uno. Si varios elementos tienen la misma prioridad, se atenderán de modo convencional según la posición que ocupen. Hay 2 formas de implementación: 1. Añadir un campo a cada nodo con su prioridad. Resulta conveniente mantener la cola ordenada por orden de prioridad. 2. Crear tantas colas como prioridades haya, y almacenar cada elemento en su cola. Bicolas: son colas en donde los nodos se pueden añadir y quitar por ambos extremos; se les llama DEQUE (Double Ended QUEue). Para representar las bicolas lo podemos hacer con un array circular con Inicio y Fin que apunten a cada uno de los extremos. Hay variantes: Bicolas de entrada restringida: Son aquellas donde la inserción sólo se hace por el final, aunque podemos eliminar al inicio ó al final. Bicolas de salida restringida: Son aquellas donde sólo se elimina por el final, aunque se puede insertar al inicio y al final.

Cola de prioridades (estructura de datos) Una cola de prioridades es una estructura de datos en la que los elementos se atienden en el orden indicado por una prioridad asociada a cada uno. Si varios elementos tienen la misma prioridad, se atenderán de modo convencional según la posición que ocupen.

Características generales Este tipo especial de colas tienen las mismas operaciones que las colas FIFO, pero con la condición de que los elementos se atienden en orden de prioridad. Ejemplos de la vida diaria serían la sala de urgencias de un hospital, ya que los enfermos se van atendiendo en función de la gravedad de su enfermedad. Entendiendo la prioridad como un valor numérico y asignando a altas prioridades valores pequeños, las colas de prioridad nos permiten añadir elementos en cualquier orden y recuperarlos de menor a mayor.

41

Implementación Hay 2 formas de implementación: 1. Añadir un campo a cada nodo con su prioridad. Resulta conveniente mantener la cola ordenada por orden de prioridad. 2. Crear tantas colas como prioridades haya, y almacenar cada elemento en su cola.

Tipos Colas de prioridades con ordenamiento ascendente: en ellas los elementos se insertan de forma arbitraria, pero a la hora de extraerlos, se extrae el elemento de menor prioridad. Colas de prioridades con ordenamiento descendente: son iguales que la colas de prioridad con ordenamiento ascendente, pero al extraer el elemento se extrae el de mayor prioridad.

Operaciones Las operaciones de las colas de prioridad son las mismas que las de las colas genéricas: Crear: se crea la cola vacía. Encolar: se añade un elemento a la cola, con su correspondiente prioridad. Desencolar: se elimina el elemento frontal de la cola. Frente: se devuelve el elemento frontal de la cola.

Implementación en Maude Para la implementación de las colas de prioridad el elemento a insertar tiene que ser de un tipo que soporte un orden total y eso lo conseguimos creando una teoría, que será la siguiente: ***( Vamos a manejar explicitamente las prioridades dentro de la cola, por lo que precisamos que el tipo base proporcione operaciones para acceder a la prioridad, y para compararlas. Se asume que p1 > p2, donde p1 y p2 son prioridades, significa que p1 es preferente frente a p2, esto es, un elemento con prioridad p1 es más prioritario que otro con prioeidad p2. ) fth ELEMENTO-PRIORIDAD is protecting BOOL . sorts Elt Prioridad . *** operaciones op prioridad : Elt -> Prioridad . op _>_ : Prioridad Prioridad -> Bool. endfth

42

Una vez que tenemos la teoría procedemos a la implementación de la cola de prioridad: fmod COLA-PRIORIDAD {X :: ELEMENTO-PRIORIDAD} is sorts Cola PrioNV{X} ColaPrio{X} . subsort Cola PrioNV{X} < ColaPrio{X} . *** operaciones op crear : -> Cola PrioNV{X} . op encolar : X$Elt Cola Prio{X} -> Cola PrioNV{X} [ctor] . *** constructores op desencolar : Cola Prio{X} -> Cola {X} . *** selectores op frente : Cola PrioNV{X} -> X$Elt . *** variables var C : Cola PrioNV{X} . var E : X$Elt . *** ecuaciones eq desencolar(crear) = crear . eq desencolar(encolar(E,crear)) = crear . eq desencolar(encolar(E,C)) = if prioridad(E) > prioridad(frente(C)) then C else encolar(E,desencolar(C)) fi . eq frente(encolar(E,crear)) = E . eq frente(encolar(E,C)) = if prioridad(E) > prioridad(frente(C)) then E else frente(C) fi . endfm

Posible instanciación ***( Usamos pares de naturales, en la que el primer valor es un dato, y el segundo su prioridad. Suponemos que un valor natural más pequeño indica mayor prioridad. ) fmod PAR-NAT is protecting NAT . sort ParNat . op : Nat Nat -> ParNat . op info : ParNat -> Nat . op clave : ParNat -> Nat . vars E C : Nat . vars P1 P2 : ParNat . eq info(< E : C >) = E . eq clave(< E : C >) = C . endfm

43

*** Realizamos la vista correspondiente view VParNat from ELEMENTO-PRIORIDAD to PAR-NAT is sort Elt to ParNat . sort Prioridad to Nat . op prioridad to clave . op _>_ to _ Bool . endfth fmod COLA-MEDIEVAL {x::MEDIEVAL} is protecting NAT, BOOL . sort colaM{x} . subsort colaMNV{x} < colaM{x} . op crear: --> colaM{x} [ctor] . op insertar: x$Elt colaM{x} --> colaMNV{x} [ctor] . op op op op

extraer: colaM{x} --> colaM{x} . frente: colaMNV{x} --> x$Elt . NNobles: colaM{x} --> Nat . NPlebleyos: colaM{x} --> Nat .

var C: colaMNV{x} . var E: x$Elt . eq extraer(crear) = crear . eq extraer(insertar(E,crear)) = crear . eq extraer(insertar(E,C)) = if NOT(esNoble?(frente(c))) AND esNoble?(E) then c else insertar(E,extraer(c)) fi . eq frente(insertar(E,crear)) = E . eq frente(insertar(E,C)) = if (esNoble?(E)) AND (esNoble?(frente(C))) then E else frente(C) fi .

44

eq NNobles(crear) = 0 . eq NNobles(insertar(E,C)) = if esNobles?(E) then 1 + NNobles(C) else NNobles(C) fi . eq NPlebleyos(crear) = 0 . eq NPlebleyos(insertar(E,C)) = if NOT(esNobles?(E)) then 1 + NPlebeyos(C) else NPlebeyos(C) fi . endfm

Implementación en JAVA Partimos a partir de la implementación en JAVA utilizando clases. package colaPrioridadSimpleEnlazada; import colaException.*; public class ColaPrioridad implements colaPrioridadInterface.ColaPrioridad { class Celda { Object elemento; int prioridad; Celda sig; } private Celda cola; public ColaPrioridad() { cola = new Celda(); cola.sig = null; } public boolean vacia() { return (cola.sig==null); } public Object primero() throws ColaVaciaException { if (vacia()) throw new ColaVaciaException(); return cola.sig.elemento; } public int primero_prioridad() throws ColaVaciaException { if (vacia()) throw new ColaVaciaException(); return cola.sig.prioridad; } public int primero_prioridad() throws ColaVaciaException { if (vacia()) throw new ColaVaciaException(); return cola.sig.prioridad; } public void inserta(Object elemento, int prioridad) { Celda p,q; boolean encontrado = false; p = cola; while((p.sig!=null)&&(!encontrado)) { if (p.sig.prioridad ***constructores ops hijoIzq hijoDer:ArbolBNV{X}->ArbolB{X} ***selectores op raiz:ArbolBNV{X}->X$Elt. ***variables var R:X$Elt. vars I D:ArbolB{X}. ***ecuaciones eq raiz(arbolBinario(R,I,D))=R. eq hijoIzq(arbolBinario(R,I,D))=I. eq hijoDer(arbolBinario(R,I,D))=D. endfm

Aquí definiremos un nuevo módulo para incorporar operaciones útiles y básicas en un Árbol Binario: fmod ÁRBOL-BIN-OPS-1{X::TRIV}is protecting ÁRBOL-BINARIO{X}. protecting NAT. ***selectores ops numElemsaltura:ArbolB{X}->Nat. op igualForma:ArbolB{X}ArbolB{X}->Bool[comm]. ***variables vars N M:Nat. vars R R2 R3:X$Elt. vars I I2 D D2:ArbolB{X}. var A:ArbolBNV{X}. ***ecuaciones eq numElems(crear)=0. eq numElems(arbolBinario(R,I,D))=1+numElems(I)+numElems(D). eq altura(crear)=0. eq altura(arbolBinario(R,I,D))=1+max(altura(I),altura(D)). eq igualForma(crear,crear)=true. eq igualForma(crear,A)=false. eq igualForma(arbolBinario(R,I,D),arbolBinario(R2,I2,D2))= igualForma(I,I2)andigualForma(D,D2). endfm

Y aquí encontramos operaciones más avanzadas para comprobar ciertos estados del Árbol Binario: fmod ÁRBOL-BIN-OPS-3{X::TRIV} is protecting ÁRBOL-BINARIO{X}. protecting ÁRBOL-BIN-OPS-1{X}.

50

protecting INT. ***selectores ops esLleno? esCompleto?:ArbolB{X}->Bool. ops esEquilibrado? esTotEqui?:ArbolB{X}->Bool. ***variables vars R:X$Elt. vars ID:ArbolB{X}. ***ecuaciones eq esLleno?(crear)=true. eq esLleno?(arbolBinario(R,I,D))=altura(I)==altura(D)and esLleno?(I) and esLleno?(D). eq esCompleto?(crear)=true. eq esCompleto?(arbolBinario(R,I,D))=(altura(I)==altura(D) and esLleno?(I) and esCompleto?(D)) or (altura(I)==(altura(D)+1) and esCompleto?(I) and esLleno?(D)). eq esEquilibrado?(crear)=true. eq esEquilibrado?(arbolBinario(R,I,D))=sd(altura(I),altura(D))=iz)&&(getHijoDerecho()!=null)){ return (1+getHijoDerecho().altura()); }else{ return 0; } } } } public int cantNodos(){ int aux1=0,aux2=0; if (esHoja()){ return 1; }else{ if(getHijoIzquierdo()!=null) aux1=getHijoIzquierdo().cantNodos(); if(getHijoDerecho()!=null) aux2=getHijoDerecho().cantNodos(); return(1+aux1+aux2); } } public void dispose(){ getHijoDerecho().dispose(); getHijoIzquierdo().dispose(); dispose(); } public ArbolBinarioNumerico getPadre(){ return padre; } public void setPadre(ArbolBinarioNumerico p){ padre=p; } public int profundidad(){ if (getPadre()!=null){ return 1+getPadre().profundidad(); }else{ return 0; } } public int aridad(){ int iz=0; int de=0; if (!this.esHoja()) { if(this.getHijoIzquierdo()!=null){ iz=getHijoIzquierdo().aridad(); } //System.out.println(dato); if(this.getHijoDerecho()!=null){ de=this.getHijoDerecho().aridad();

53

} }else{ return 1; } return iz+de; } public void caminos(ListaInt aux){ if(esHoja()){ aux.insert(getDato()); System.out.println("Camino"); aux.imprimirLista(); aux.delete(getDato()); }else{ aux.insert(getDato()); if(getHijoIzquierdo()!=null){ getHijoIzquierdo().caminos(aux); } if(getHijoDerecho()!=null){ getHijoDerecho().caminos(aux); } aux.delete(getDato()); } } public boolean camListaIgual(ListaInt lista){ if((lista==null)||(lista.esVacia())){ return false; }else{ if(getDato()==lista.first()){ if (esHoja()){ if(lista.size()==1){ return true; }else{ return false; } }else{ lista.delete(lista.first()); if ((getHijoIzquierdo()!=null)&&(getHijoDerecho()!=null)){ return getHijoIzquierdo().camListaIgual(lista)||getHijoDerecho().camListaIgual(lista); }else{ if (getHijoIzquierdo()==null){ return getHijoDerecho().camListaIgual(lista); }else{ return getHijoIzquierdo().camListaIgual(lista); } } } }else{ return false; } }

} public boolean esta(int valor){ if(getDato()==valor){ return true; }else{ if(esHoja()){

54

return false; }else{ if((getHijoIzquierdo()!=null)&&(getHijoDerecho()!=null)){ return getHijoIzquierdo().esta(valor)||getHijoDerecho().esta(valor); }else{ if(getHijoIzquierdo()!=null){ return getHijoIzquierdo().esta(valor); }else{ return getHijoDerecho().esta(valor); } } } }

}

Implementación en C Un árbol binario puede declararse de varias maneras. Algunas de ellas son: Estructura con manejo de memoria dinámica: typedef struct tArbol { int clave; struct tArbol *hIzquierdo, *hDerecho; } tArbol;

Estructura con arreglo indexado: typedef struct tArbol { int clave; int hIzquierdo, hDerecho; }; tArbol árbol[NUMERO_DE_NODOS];

En el caso de un árbol binario casi-completo (o un árbol completo), puede utilizarse un sencillo arreglo de enteros con tantas posiciones como nodos deba tener el árbol. La información de la ubicación del nodo en el árbol es implícita a cada posición del arreglo. Así, si un nodo está en la posición i, sus hijos se encuentran en las posiciones 2i+1 y 2i+2, mientras que su padre (si tiene), se encuentra en la posición truncamiento((i-1)/2) (suponiendo que la raíz está en la posición cero). Este método se beneficia de un almacenamiento más compacto y una mejor localidad de referencia, particularmente durante un recorrido en preorden. La estructura para este caso sería por tanto: int árbol[NUMERO_DE_NODOS];

Recorridos sobre árboles binarios

Recorridos en profundidad El método de este recorrido es tratar de encontrar de la cabecera a la raíz en nodo de unidad binaria

55

Especificación en Maude de los recorridos preorden, inorden, postorden Especificaremos antes en Maude las operaciones de recorrido en preorden, inorden y postorden: fmod ÁRBOL-BIN-REC-PROF{X::TRIV} is protecting ÁRBOL-BINARIO{X}. protecting LISTA-GENERICA{X}. protecting INT. ***selectores ops preOrden inOrden posOrden:ArbolB{X}->ListaGen{X}. ***variables var R:X$Elt. vars ID:ArbolB{X}. ***ecuaciones eq preOrden(crear)=crear. eq preOrden(arbolBinario(R,I,D))=cons(R,preOrden(I))::preOrden(D). eq inOrden(crear)=crear. eq inOrden(arbolBinario(R,I,D))=inOrden(I)::cons(R,inOrden(D)). eq posOrden(crear)=crear. eq posOrden(arbolBinario(R,I,D))=posOrden(I)::posOrden(D)::cons(R,crear). endfm

Ahora pasamos a ver la implementación de los distintos recorridos:

Recorrido en preorden En este tipo de recorrido se realiza cierta acción (quizás simplemente imprimir por pantalla el valor de la clave de ese nodo) sobre el nodo actual y posteriormente se trata el subárbol izquierdo y cuando se haya concluido, el subárbol derecho. En el árbol de la figura el recorrido en preorden sería: 2, 7, 2, 6, 5, 11, 5, 9 y 4. void preorden(tArbol *a) { if (a != NULL) { tratar(a); preorden(a->hIzquierdo); preorden(a->hDerecho); } }

//Realiza una operación en nodo

Implementación en pseudocódigo de forma iterativa: push(s,NULL); //insertamos en una pila (stack) el valor NULL, para asegurarnos de que esté vacía push(s,raíz); //insertamos el nodo raíz MIENTRAS (s NULL) HACER p = pop(s); //sacamos un elemento de la pila tratar(p); //realizamos operaciones sobre el nodo p

56

SI (I(p) NULL) ENTONCES push(s,D(p)); FIN-SI SI (D(p) NULL) ENTONCES push(s,I(p)); FIN-SI FIN-MIENTRAS

//preguntamos si p tiene árbol derecho //preguntamos si p tiene árbol izquierdo

En Java: public void preOrden(){ if (!esHoja()){ System.out.println(dato); if(getHijoIzquierdo()!=null){ getHijoIzquierdo().preOrden(); } if (getHijoDerecho()!=null){ getHijoDerecho().postOrden(); } }else{ System.out.println(dato); } }

Recorrido en postorden En este caso se trata primero el subárbol izquierdo, después el derecho y por último el nodo actual. En el árbol de la figura el recorrido en postorden sería: 2, 5, 11, 6, 7, 4, 9, 5 y 2. void postorden(tArbol *a) { if (a != NULL) { postorden(a->hIzquiedo); postorden(a->hDerecho); tratar(a); } }

//Realiza una operación en nodo

En Java: public void postOrden(){ if(!esHoja()){ if (getHijoIzquierdo()!=null){ getHijoIzquierdo().postOrden(); } if (getHijoDerecho()!=null){ getHijoDerecho().postOrden(); } System.out.println(dato); }else{ System.out.println(dato); } }

57

Recorrido en inorden En este caso se trata primero el subárbol izquierdo, después el nodo actual y por último el subárbol derecho. En un ABB este recorrido daría los valores de clave ordenados de menor a mayor. En el árbol de la figura el recorrido en inorden sería: 2, 7, 5, 6, 11, 2, 5, 4 y 9. Pseudocódigo: funcion inorden(nodo) inicio si(existe(nodo)) inicio inorden(hijo_izquierdo(nodo)); tratar(nodo); inorden(hijo_derecho(nodo)); fin; fin;

//Realiza una operación en nodo

Implementación en C: void inorden(tArbol *a) { if (a != NULL) { inorden(a->hIzquierdo); tratar(a); inorden(a->hDerecho); } }

//Realiza una operación en nodo

En Java: public void inorden(){ if (!this.esHoja()) { if(this.getHijoIzquierdo()!=null){ this.getHijoIzquierdo().inorden(); } System.out.print(dato); if(this.getHijoDerecho()!=null){ this.getHijoDerecho().inorden(); } }else{ System.out.print(dato); } }

Recorridos en amplitud (o por niveles) En este caso el recorrido se realiza en orden por los distintos niveles del árbol. Así, se comenzaría tratando el nivel 1, que sólo contiene el nodo raíz, seguidamente el nivel 2, el 3 y así sucesivamente. En el árbol de la figura el recorrido en amplitud sería: 2, 7, 5, 2, 6, 9, 5, 11 y 4.

58

Al contrario que en los métodos de recorrido en profundidad, el recorrido por niveles no es de naturaleza recursiva. Por ello, se debe utilizar una cola para recordar los subárboles izquierdos y derecho de cada nodo. Pseudocódigo: encolar(raiz); mientras(cola_no_vacia()) inicio nodo=desencolar(); visitar(nodo); encolar_nodos_hijos(nodo); fin;

//Saca un nodo de la cola //Realiza una operación en nodo //Mete en la cola los hijos del nodo actual

Implementación en C: void amplitud(tArbol *a) { tCola cola; tArbol *aux; if (a != NULL) { crearCola(cola); encolar(cola, a); while (!colavacia(cola)) { desencolar(cola, aux); visitar(aux); operación en nodo if (aux->hIzquierdo != NULL) encolar(cola, aux->hIzquierdo ); if (aux->hDerecho!= NULL) encolar(cola, aux->hDerecho); } } }

//Realiza una

Implementación en Java: public void amplitud(NodoArbol a) { Cola cola, colaAux; NodoArbol aux;

//SE RECIBE LA RAÍZ DEL ÁRBOL

if (a != null) { cola=new Cola(); colaAux=new Cola(); cola.push(a); NODO EN LA COLA while (cola.colavacia()!=1) { colaAux.push(aux=cola.pop()); ASIGNADO

//SI EL ÁRBOL CONTIENE NODOS...

//DEFINICIÓN DE 2 VARIABLES DE TIPO COLA //DEFINICIÓN AUX DE TIPO NODOARBOL

//SE INSTANCIA EL OBJETO COLA //SE INSTANCIA EL OBJETO COLAAUX //SE INSERTA EL NODOARBOL "A" (RAÍZ) COMO PRIMER //MIENTRAS HAYAN ELEMENTOS EN LA COLA... /*EL ELEMENTO EXTRAÍDO DE LA COLA PRINCIPAL ES A

AUXILIAR*/ if (aux.izq != null) {

AUX

Y

A

SU

VEZ

INSERTADO

EN

LA

COLA

//SI EL HIJO IZQUIERDO DEL NODO ACTUAL EXISTE

59

cola.push(aux.izq); LA COLA } if (aux.der!= null) { cola.push(aux.der); LA COLA } } colaAux.print(); } }

//SE INSERTA ESE HIJO COMO ELEMENTO SIGUIENTE EN //SI EL HIJO DERECHO DEL NODO ACTUAL EXISTE //SE INSERTA ESE HIJO COMO ELEMENTO SIGUIENTE EN

//POR ÚLTIMO SE IMPRIME LA COLA AUXILIAR

NOTA: Para hacer un recorrido en anchura, la idea es ir guardando en una cola los hijos del nodo que se están visitando y el siguiente a visitar es el próximo nodo de la cola.

Métodos para almacenar árboles binarios Los árboles binarios pueden ser construidos a partir de lenguajes de programación de varias formas. En un lenguaje con registros y referencias, los árboles binarios son construidos típicamente con una estructura de nodos y punteros en la cual se almacenan datos, cada uno de estos nodos tiene una referencia o puntero a un nodo izquierdo y a un nodo derecho denominados hijos. En ocasiones, también contiene un puntero a un único nodo. Si un nodo tiene menos de dos hijos, algunos de los punteros de los hijos pueden ser definidos como nulos para indicar que no dispone de dicho nodo. En la figura adjunta se puede observar la estructura de dicha implementación.

Los árboles binarios también pueden ser almacenados como una estructura de datos implícita en arreglos, y si el árbol es un árbol binario completo, este método no desaprovecha el espacio en memoria. Tomaremos como notación la siguiente: si un nodo tiene un índice i, sus hijos se encuentran en índices 2i + 1 y 2i + 2, mientras que sus padres (si los tiene) se encuentra en el índice (partiendo de que la raíz tenga índice cero). Este método tiene como ventajas el tener almacenados los datos de forma más

60

compacta y por tener una forma más rápida y eficiente de localizar los datos en particular durante un preoden transversal. Sin embargo, desperdicia mucho espacio en memoria.

Codificación de árboles n-arios como árboles binarios Hay un mapeo uno a uno entre los árboles generales y árboles binarios, el cual en particular es usado en Lisp para representar árboles generales como árboles binarios. Cada nodo N ordenado en el árbol corresponde a un nodo N 'en el árbol binario; el hijo de la izquierda de N‘ es el nodo correspondiente al primer hijo de N, y el hijo derecho de N' es el nodo correspondiente al siguiente hermano de N, es decir, el próximo nodo en orden entre los hijos de los padres de N. Esta representación como árbol binario de un árbol general, se conoce a veces como un árbol binario primer hijo/siguiente hermano, o un árbol doblemente encadenado. Una manera de pensar acerca de esto es que los hijos de cada nodo estén en una lista enlazada, encadenados junto con el campo derecho, y el nodo sólo tiene un puntero al comienzo o la cabeza de esta lista, a través de su campo izquierdo. Por ejemplo, en el árbol de la izquierda, la A tiene 6 hijos (B, C, D, E, F, G). Puede ser convertido en el árbol binario de la derecha. Un ejemplo de transformar el árbol n-ario a un árbol binario Cómo pasar de árboles n-arios a árboles FLOFO.

El árbol binario puede ser pensado como el árbol original inclinado hacia los lados, con los bordes negros izquierdos representando el primer hijo y los azules representado los siguientes hermanos. Las hojas del árbol de la izquierda serían escritas en Lisp como: (((M N) H I) C D ((O) (P)) F (L))

61

Que se ejecutará en la memoria como el árbol binario de la derecha, sin ningún tipo de letras en aquellos nodos que tienen un hijo izquierdo.

Árbol binario de búsqueda Un árbol binario de búsqueda es un tipo particular de árbol binario que presenta una estructura de datos en forma de árbol usada en informática.

Descripción Un árbol binario de búsqueda (ABB) es un árbol binario definido de la siguiente forma: Todo árbol vacío es un árbol binario de búsqueda. Un árbol binario no vacío, de raíz R, es un árbol binario de búsqueda si: • En caso de tener subárbol izquierdo, la raíz R debe ser mayor que el valor máximo almacenado en el subárbol izquierdo, y que el subárbol izquierdo sea un árbol binario de búsqueda. • En caso de tener subárbol derecho, la raíz R debe ser menor que el valor mínimo almacenado en el subárbol derecho, y que el subárbol derecho sea un árbol binario de búsqueda.

Para una fácil comprensión queda resumido en que es un árbol binario que cumple que el subárbol izquierdo de cualquier nodo (si no está vacío) contiene valores menores que el que contiene dicho nodo, y el subárbol derecho (si no está vacío) contiene valores mayores. Para estas definiciones se considera que hay una relación de orden establecida entre los elementos de los nodos. Que cierta relación este definida, o no, depende de cada lenguaje de programación. De aquí se deduce que puede haber distintos árboles binarios de búsqueda para un mismo conjunto de elementos. La altura h en el peor de los casos siempre el mismo tamaño que el número de elementos disponibles. Y en el mejor de los casos viene dada por la expresión h = ceil(log2(c + 1)), donde ceil indica redondeo por exceso.

Ejemplo de Árbol Binario de Búsqueda

62

El interés de los árboles binarios de búsqueda radica en que su recorrido en inorden proporciona los elementos ordenados de forma ascendente y en que la búsqueda de algún elemento suele ser muy eficiente. Dependiendo de las necesidades del usuario que trate con una estructura de este tipo se podrá permitir la igualdad estricta en alguno, en ninguno o en ambos de los subárboles que penden de la raíz. Permitir el uso de la igualdad provoca la aparición de valores dobles y hace la búsqueda más compleja. Un árbol binario de búsqueda no deja de ser un caso particular de árbol binario, así usando la siguiente especificación de árbol binario en maude: fmod ARBOL-BINARIO {X :: TRIV}is sorts ArbolBinNV{X} ArbolBin{X} . subsort ArbolBinNV{X} < ArbolBin{X} . *** generadores op crear : -> ArbolBin{X} [ctor] . op arbolBin : X$Elt ArbolBin{X} ArbolBin{X} -> ArbolBinNV{X} [ctor] . endfm

Podemos hacer la siguiente definicion para un árbol binario de búsqueda (también en maude): fmod ARBOL-BINARIO-BUSQUEDA {X :: ORDEN} is protecting ARBOL-BINARIO{VOrden}{X} . sorts ABB{X} ABBNV{X} . subsort ABBNV{X} < ABB{X} . subsort ABB{X} < ArbolBin{VOrden}{X} . subsort ABBNV{X} < ArbolBinNV{VOrden}{X} . *** generadores op crear : -> ArbolBin{X} [ctor] . op arbolBin : X$Elt ArbolBin{X} ArbolBin{X} -> ArbolBinNV{X} [ctor] . endfm

Con la siguiente teoría de orden: fth ORDEN is protecting BOOL . sort Elt . *** operaciones op _ Bool . endfth

Para que un árbol binario pertenezca al tipo árbol binario de búsqueda debe cumplir la condición de ordenación siguiente que iría junto al módulo ARBOL-BINARIO-BUSQUEDA: var R : X$Elt . vars INV DNV : ABBNV{X} . vars I D : ABB{X} . mb crear : ABB{X} . mb arbolBin(R, crear, crear) : ABBNV{X} . cmb arbolBin(R, INV, crear) : ABBNV{X} if R > max(INV) . cmb arbolBin(R, crear, DNV) : ABBNV{X} if R < min(DNV) . cmb arbolBin(R, INV, DNV) : ABBNV{X} if (R > max(INV)) and (R < min(DNV)) . ops min max : ABBNV{X} -> X$Elt .

63

eq eq eq eq

min(arbolBin(R, min(arbolBin(R, max(arbolBin(R, max(arbolBin(R,

crear, D)) INV, D)) = I, crear)) I, DNV)) =

= R . min(INV) . = R . max(DNV) .

Operaciones Todas las operaciones realizadas sobre árboles binarios de búsqueda están basadas en la comparación de los elementos o clave de los mismos, por lo que es necesaria una subrutina, que puede estar predefinida en el lenguaje de programacion, que los compare y pueda establecer una relación de orden entre ellos, es decir, que dados dos elementos sea capaz de reconocer cual es mayor y cual menor. Se habla de clave de un elemento porque en la mayoría de los casos el contenido de los nodos será otro tipo de estructura y es necesario que la comparación se haga sobre algún campo al que se denomina clave.

Búsqueda La búsqueda consiste acceder a la raíz del árbol, si el elemento a localizar coincide con éste la búsqueda ha concluido con éxito, si el elemento es menor se busca en el subárbol izquierdo y si es mayor en el derecho. Si se alcanza un nodo hoja y el elemento no ha sido encontrado se supone que no existe en el árbol. Cabe destacar que la búsqueda en este tipo de árboles es muy eficiente, representa una función logarítmica. El número de comparaciones que necesitaríamos para saber si un elemento se encuentra en un árbol binario de búsqueda estaría entre [log2(N+1)] y N, siendo N el numero de nodos. La búsqueda de un elemento en un ABB (Árbol Binario de Búsqueda) se puede realizar de dos formas, iterativa o recursiva. Ejemplo de versión iterativa en el lenguaje de programación C, suponiendo que estamos buscando una clave alojada en un nodo donde está el correspondiente "dato" que precisamos encontrar: data Buscar_ABB(abb t,clave k) { abb p; dato e; e=NULL; p=t; if (!estaVacio(p)) { while (!estaVacio(p) && (p->k!=k) ) { if (k < p->k) { p=p->l; } if (p->k < k) { p=p->r; } } if (!estaVacio(p) &&(p->d!=NULL) ) { e=copiaDato(p->d); }

64

} return e; }

Véase ahora la versión recursiva en ese mismo lenguaje: int buscar(tArbol *a, int elem) { if (a == NULL) return 0; else if (a->clave < elem) return buscar(a->hDerecho, elem); else if (a->clave > elem) return buscar(a->hIzquierdo, elem); else return 1; }

Otro ejemplo en Python: def search_binary_tree(node, key): if node is None: return None # not found if key < node.key: return search_binary_tree(node.left, key) else if key > node.key: return search_binary_tree(node.right, key) else: return node.value

En Pascal: Function busqueda(T:ABR, y: integer):ABR begin if (T=nil) or (^T.raiz=y) then busqueda:=T; else if (^T.raiz Bool . var R R1 R2 : X$Elt . vars I D : ABB{X} . eq esta?(R, crear) = false . eq esta?(R1, arbolBin(R2, I, D)) = if R1 == R2 then true else if R1 < R2 then esta?(R1, I) else

65

esta?(R1, D) fi fi .

Inserción La inserción es similar a la búsqueda y se puede dar una solución tanto iterativa como recursiva. Si tenemos inicialmente como parámetro un árbol vacío se crea un nuevo nodo como único contenido el elemento a insertar. Si no lo está, se comprueba si el elemento dado es menor que la raíz del árbol inicial con lo que se inserta en el subárbol izquierdo y si es mayor se inserta en el subárbol derecho. De esta forma las inserciones se hacen en las hojas.

Evolución de la inserción del elemento "5" en un ABB

Como en el caso de la búsqueda puede haber varias variantes a la hora de implementar la inserción en el TAD (Tipo Abstracto de Datos), y es la decisión a tomar cuando el elemento (o clave del elemento) a insertar ya se encuentra en el árbol, puede que éste sea modificado o que sea ignorada la inserción. Es obvio que esta operación modifica el ABB perdiendo la versión anterior del mismo. A continuación se muestran las dos versiones del algoritmo en pseudolenguaje, iterativo y recursivo, respectivamente. PROC InsertarABB(árbol:TABB; dato:TElemento) VARIABLES nuevonodo,pav,pret:TABB clavenueva:Tclave ele:TElemento INICIO nuevonodo hIzquierdo, &aux); free(aux); } } void reemplazar(tArbol **a, tArbol **aux) { if ((*a)->hDerecho == NULL) { (*aux)->clave = (*a)->clave; *aux = *a; *a = (*a)->hIzquierdo; } else reemplazar(&(*a)->hDerecho, aux); }

Otro ejemplo en Pascal. Procedure Borrar(var T:ABR, x:ABR) var aBorrar:ABR; anterior:ABR; actual:ABR; hijo:ABR; begin if (^x.izq=nil) or (^x.dch=nil) then aBorrar:=x; else aBorrar:=sucesor(T,x); actual:=T; anterior:=nil; while (actualaBorrar) do begin anterior:=actual; if (^actual.raiz X$Elt . eq min(arbolBin(R, crear, D)) = R . eq max(arbolBin(R, I, crear)) = R . eq min(arbolBin(R, INV, D)) = min(INV) . eq max(arbolBin(R, I, DNV )) = max(DNV) . eq eliminar(M, crear) = crear . ceq eliminar(M, arbolBin(R, crear, D)) = D if M == clave(R) . ceq eliminar(M, arbolBin(R, I, crear)) = I if M == clave(R) . ceq eliminar(M, arbolBin(R, INV, DNV)) = arbolBin(max(INV), eliminar(clave(max(INV)), INV), DNV) if M == clave(R) . ceq eliminar(M, arbolBin(R, I, D)) = arbolBin(R, eliminar(M, I), D) if M < clave(R) . ceq eliminar(M, arbolBin(R, I, D)) = arbolBin(R, I, eliminar(M, D)) if clave(R) < M .

Otras Operaciones Otra opereción sería por ejemplo comprobar que un árbol binario es un árbol binario de búsqueda. Su implementación en maude es la siguiente: op esABB? : ABB{X} -> Bool . var R : X$Elt . vars I D : ABB{X} . eq esABB?(crear) = true . eq esABB?(arbolbBin(R, I, D)) = (Max(I) < R) and (Min(D) > R) and (esABB?(I)) and (esABB?(D)) .

71

Recorridos Se puede hacer un recorrido de un árbol en profundidad o en anchura. Los recorridos en anchura son por niveles, se realiza horizontalmente desde la raíz a todos los hijos antes de pasar a la descendencia de alguno de los hijos. El recorrido en profundidad lleva al camino desde la raíz hacia el descendiente más lejano del primer hijo y luego continúa con el siguiente hijo. Como recorridos en profundidad tenemos inorden, preorden y postorden. Una propiedad de los ABB es que al hacer un recorrido en profundidad inorden obtenemos los elementos ordenados de forma ascendente.

Ejemplo árbol binario de búsqueda

Resultado de hacer el recorrido en: Inorden = [6, 9, 13, 14, 15, 17, 20, 26, 64, 72]. Preorden = [15, 9, 6, 14, 13, 20, 17, 64, 26, 72]. Postorden =[6, 13, 14, 9, 17, 26, 72, 64, 20, 15].

Tipos de árboles binarios de búsqueda Hay varios tipos de árboles binarios de búsqueda. Los árboles AVL, árbol rojo-negro, son árboles autobalanceables. Los árboles biselados son árboles también autobalanceables con la propiedad de que los elementos accedidos recientemente se accederán más rápido en posteriores accesos. En el montículo como en todos los árboles binarios de búsqueda cada nodo padre tiene un valor mayor q sus hijos y además es completo, esto es cuando todos los niveles están llenos con excepción del último que puede no estarlo. Hay muchos tipos de árboles binarios de búsqueda. Los árboles AVL y los árboles rojo y negro son ambos formas de árboles binarios de búsqueda autobalanceables. Un árbol biselado es un árbol binario de búsqueda que automáticamente mueve los elementos a los que se accede frecuentemente cerca de la raíz. En los monticulos, cada nodo también mantiene una prioridad y un nodo padre tiene mayor prioridad que su hijo. Otras dos maneras de configurar un árbol binario de búsqueda podría ser como un árbol completo o degenerado. Un árbol completo es un árbol con "n" niveles, donde cada nivel d dcho; aux-> dcho = p->izdo; p->izdo = aux; }

77

void rotar_dcha(struct node *p) { struct node *aux; aux = p; p = p->izdo; aux->izdo = p->dcho; p->dcho = aux, }

Búsqueda La búsqueda consiste acceder a la raíz del árbol, si el elemento a localizar coincide con éste la búsqueda ha concluido con éxito, si el elemento es menor se busca en el subárbol izquierdo y si es mayor en el derecho. Si se alcanza un nodo hoja y el elemento no ha sido encontrado se supone que no existe en el árbol. Cabe destacar que la búsqueda en este tipo de árboles es muy eficiente, representa una función logarítmica. La búsqueda de un elemento en un ABB (Árbol Binario de Búsqueda) en general, y en un árbol rojo negro en particular, se puede realizar de dos formas, iterativa o recursiva. Ejemplo de versión iterativa en el lenguaje de programación C, suponiendo que estamos buscando una clave alojada en un nodo donde está el correspondiente "dato" que precisamos encontrar: data Buscar_ABB(abb t,clave k) { abb p; dato e; e=NULL; p=t; if (!estaVacio(p)) { while (!estaVacio(p) && (p->k!=k) ) { if (k < p->k) { p=p->l; }

78

if (p->k < k) { p=p->r; } } if (!estaVacio(p) &&(p->d!=NULL) ) { e=copiaDato(p->d); } } return e; }

Véase ahora la versión recursiva en ese mismo lenguaje: int buscar(tArbol *a, int elem) { if (a == NULL) return 0; else if (a->clave < elem) return buscar(a->hDerecho, elem); else if (a->clave > elem) return buscar(a->hIzquierdo, elem); else return 1; }

Inserción La inserción comienza añadiendo el nodo como lo haríamos en un árbol binario de búsqueda convencional y pintándolo de rojo. Lo que sucede después depende del color de otros nodos cercanos. El término tío nodo será usado para referenciar al hermano del padre de un nodo, como en los árboles familiares humanos. Conviene notar que: La propiedad 3 (Todas las hojas, incluyendo las nulas, son negras) siempre se cumple. La propiedad 4 (Ambos hijos de cada nodo rojo son negros) está amenazada solo por añadir un nodo rojo, por repintar un nodo negro de color rojo o por una rotación. La propiedad 5 (Todos los caminos desde un nodo dado hasta sus nodos hojas contiene el mismo número de nodos negros) está amenazada solo por añadir un nodo rojo, por repintar un nodo negro de color rojo o por una rotación.

Al contrario de lo que sucede en otros árboles como puede ser el Árbol AVL, en cada inserción se realiza un máximo de una rotación, ya sea simple o doble. Por otra parte, se asegura un tiempo de recoloración máximo de O(log2n) por cada inserción. Nota: En los esquemas que acompañan a los algoritmos, la etiqueta N será utilizada por el nodo que está siendo insertado, P para los padres del nodo N, G para los abuelos del nodo N, y U para los tíos del nodo N. Notamos que los roles y etiquetas de los nodos están intercambiados entre algunos casos, pero en cada caso, toda etiqueta continúa representando el mismo nodo que representaba al comienzo del caso. Cualquier color mostrado en el diagrama está o bien supuesto en el caso o implicado por dichas suposiciones. 79

Los nodos tío y abuelo pueden ser encontrados por las siguientes funciones: struct node * abuelo(struct node *n) { if ((n != NULL) && (n->padre != NULL)) return n->padre->padre; else return NULL; } struct node * tio(struct node *n) { struct node *a = abuelo(n); if (n->padre == a->izdo) return a->dcho; else return a->izdo; }

Estudiemos ahora cada caso de entre los posibles que nos podemos encontrar al insertar un nuevo nodo. Caso 1: El nuevo nodo N es la raíz de del árbol. En este caso, es repintado a color negro para satisfacer la propiedad 2 (la raíz es negra). Como esto añade un nodo negro a cada camino, la propiedad 5 (todos los caminos desde un nodo dado a sus hojas contiene el mismo número de nodos negros) se mantiene. En C quedaría así: void insercion_caso1(struct node *n) { if (n->padre == NULL) n->color = NEGRO; else insercion_caso2(n); }

Caso 2: El padre del nuevo nodo (esto es, el nodo P) es negro, así que la propiedad 4 (ambos hijos de cada nodo rojo son negros) se mantiene. En este caso, el árbol es aun válido. La propiedad 5 (todos los caminos desde cualquier nodo dado a sus hojas contiene igual número de nodos negros) se mantiene, porque el nuevo nodo N tiene dos hojas negras como hijos, pero como N es rojo, los caminos a través de cada uno de sus hijos tienen el mismo número de nodos negros que el camino hasta la hoja que reemplazó, que era negra, y así esta propiedad se mantiene satisfecha. Su implementación: void insercion_caso2(struct node *n) { if (n->padre->color == NEGRO) return; /* Árbol válido. */ else insercion_caso3(n); }

80

Nota: En los siguientes casos se puede asumir que N tiene un abuelo, el nodo G, porque su padre P es rojo, y si fuese la raíz, sería negro. Consecuentemente, N tiene también un nodo tío U a pesar de que podría ser una hoja en los casos 4 y 5.

Caso 3: Si el padre P y el tío U son rojos, entonces ambos nodos pueden ser repintados de negro y el abuelo G se convierte en rojo para mantener la propiedad 5 (todos los caminos desde cualquier nodo dado hasta sus hojas contiene el mismo número de nodos negros). Ahora, el nuevo nodo rojo N tiene un padre negro. Como cualquier camino a través del padre o el tío debe pasar a través del abuelo, el número de nodos negros en esos caminos no ha cambiado. Sin embargo, el abuelo G podría ahora violar la propiedad 2 (la raíz es negra) o la 4 (ambos hijos de cada nodo rojo son negros), en el caso de la 4 porque G podría tener un padre rojo. Para solucionar este problema, el procedimiento completo se realizará de forma recursiva hacia arriba hasta alcanzar el caso 1. El código en C quedaría de la siguiente forma: void insercion_caso3(struct node *n) { struct node *t = tio(n), *a; if ((t != NULL) && (t->color == ROJO)) { n->padre->color = NEGRO; t->color = NEGRO; a = abuelo(n); a->color = ROJO; insercion_caso1(a); } else { insercion_caso4(n); } }

Nota: En los casos restantes, se asume que el nodo padre P es el hijo izquierdo de su padre. Si es el hijo derecho, izquierda y derecha deberían ser invertidas a partir de los casos 4 y 5. El código del ejemplo toma esto en consideración.

81

Caso 4: El nodo padre P es rojo pero el tío U es negro; también, el nuevo nodo N es el hijo derecho de P, y P es el hijo izquierdo de su padre G. En este caso, una rotación a la izquierda que cambia los roles del nuevo nodo N y su padre P puede ser realizada; entonces, el primer nodo padre P se ve implicado al usar el caso 5 de inserción (reetiquetando N y P ) debido a que la propiedad 4 (ambos hijos de cada nodo rojo son negros) se mantiene aún incumplida. La rotación causa que algunos caminos (en el sub-árbol etiquetado como ―1‖) pasen a través del nuevo nodo donde no lo hacían antes, pero ambos nodos son rojos, así que la propiedad 5 (todos los caminos desde cualquier nodo dado a sus hojas contiene el mismo número de nodos negros) no es violada por la rotación. Aquí tenemos una posible implementación: void insercion_caso4(struct node *n) { struct node *a = abuelo(n); if ((n == n->padre->dcho) && (n->padre == a->izdo)) { rotar_izda(n->padre); n = n->izdo; } else if ((n == n->padre->izdo) && (n->padre == a->dcho)) { rotar_dcha(n->padre); n = n->dcho; } insercion_caso5(n); }

Caso 5: El padre P es rojo pero el tío U es negro, el nuevo nodo N es el hijo izquierdo de P, y P es el hijo izquierdo de su padre G. En este caso, se realiza una rotación a la derecha sobre el padre P; el resultado es un árbol donde el padre P es ahora el padre del nuevo nodo N y del inicial abuelo G. Este nodo G ha de ser negro, así como su hijo P rojo. Se intercambian los colores de ambos y el resultado satisface la propiedad 4 (ambos hijos de un nodo rojo son negros). La propiedad 5 (todos los caminos desde un nodo dado hasta sus hojas contienen el mismo número de nodos negros) también se mantiene satisfecha, ya que todos los caminos que iban a través de esos tres nodos entraban por G antes, y ahora entran por P. En cada caso, este es el único nodo negro de los tres. Una posible implementación en C es la siguiente: void insercion_caso5(struct node *n) { struct node *a = abuelo(n); n->padre->color = NEGRO; a->color = ROJO; if ((n == n->padre->izdo) && (n->padre == a->izdo)) {

82

rotar_dcha(a); } else {

/* * En este caso, (n == n->padre->dcho) && (n->padre == a->dcho). */ rotar_izda(a); } }

Nótese que la inserción se realiza sobre el propio árbol y que los códigos del ejemplo utilizan recursión de cola.

Eliminación En un árbol binario de búsqueda normal, cuando se borra un nodo con dos nodos internos como hijos, tomamos el máximo elemento del subárbol izquierdo o el mínimo del subárbol derecho, y movemos su valor al nodo que es borrado (como se muestra aquí). Borramos entonces el nodo del que copiábamos el valor que debe tener menos de dos nodos no hojas por hijos. Copiar un valor no viola ninguna de las propiedades rojo-negro y reduce el problema de borrar en general al de borrar un nodo con como mucho un hijo no hoja. No importa si este nodo es el nodo que queríamos originalmente borrar o el nodo del que copiamos el valor. Resumiendo, podemos asumir que borramos un nodo con como mucho un hijo no hoja (si solo tiene nodos hojas por hijos, tomaremos uno de ellos como su hijo). Si borramos un nodo rojo, podemos simplemente reemplazarlo con su hijo, que debe ser negro. Todos los caminos hasta el nodo borrado simplemente pasarán a través de un nodo rojo menos, y ambos nodos, el padre del borrado y el hijo, han de ser negros, así que las propiedades 3 (todas las hojas, incluyendo las nulas, son negras) y 4 (los dos hijos de cada nodo rojo son negros) se mantienen. Otro caso simple es cuando el nodo borrado es negro y su hijo es rojo. Simplemente eliminar un nodo negro podría romper las propiedades 4 (los dos hijos de cada nodo rojo son negros) y 5 (todos los caminos desde un nodo dado hasta sus hojas contienen el mismo número de nodos negros), pero si repintamos su hijo de negro, ambas propiedades quedan preservadas. El caso complejo es cuando el nodo que va a ser borrado y su hijo son negros. Empezamos por reemplazar el nodo que va a ser borrado con su hijo. Llamaremos a este hijo (en su nueva posición) N, y su hermano (el nuevo hijo de su padre) S. En los diagramas de debajo, usaremos P para el nuevo padre de N, SL para el hijo izquierdo de S, y SR para el nuevo hijo derecho de S (se puede mostrar que S no puede ser una hoja). Nota: Entre algunos casos cambiamos roles y etiquetas de los nodos, pero en cada caso, toda etiqueta sigue representando al mismo nodo que representaba al comienzo del caso. Cualquier color mostrado en el diagrama es o bien supuesto en su caso o bien implicado por dichas suposiciones. El blanco representa un color desconocido (o bien rojo o bien negro).

El cumplimiento de estas reglas en un árbol con n nodos, asegura un máximo de tres rotaciones y hasta O(log2n) recoloraciones. Encontraremos el hermano usando esta función: struct node *

83

hermano(struct node *n) { if (n == n->padre->izdo) return n->padre->dcho; else return n->padre->izdo; }

Nota: Con el fin de preservar la buena definición del árbol, necesitamos que toda hoja nula siga siendo una hoja nula tras todas las transformaciones (que toda hoja nula no tendrá ningún hijo). Si el nodo que estamos borrando tiene un hijo no hoja N, es fácil ver que la propiedad se satisface. Si, por otra parte N fuese una hoja nula, se verifica por los diagramas o el código que para todos los casos la propiedad se satisface también.

Podemos realizar los pasos resaltados arriba con el siguiente código, donde la función reemplazar_nodo sustituye hijo en el lugar de n en el árbol. Por facilitar la comprensión del ejemplo, en el código de esta sección supondremos que las hojas nulas están representadas por nodos reales en lugar de NULL (el código de la sección inserción trabaja con ambas representaciones). void elimina_un_hijo(struct node *n) {

/* * Precondición: n tiene al menos un hijo no nulo. */ struct node *hijo = es_hoja(n->dcho) ? n->izdo : n->dcho; reemplazar_nodo(n, hijo); if (n->color == NEGRO) { if (hijo->color == ROJO) hijo->color = NEGRO; else eliminar_caso1(hijo); } free(n); }

Nota: Si N es una hoja nula y no queremos representar hojas nulas como nodos reales, podemos modificar el algoritmo llamando primero a eliminar_caso1() en su padre (el nodo que borramos, n en el código anterior) y borrándolo después. Podemos hacer esto porque el padre es negro, así que se comporta de la misma forma que una hoja nula (y a veces es llamada hoja “fantasma”). Y podemos borrarla con seguridad, de tal forma que n seguirá siendo una hoja tras todas las operaciones, como se muestra arriba.

Si N y su padre original son negros, entonces borrar este padre original causa caminos que pasan por N y tienen un nodo negro menos que los caminos que no. Como esto viola la propiedad 5 (todos los caminos desde un nodo dado hasta su nodos hojas deben contener el mismo número de nodos negros), el árbol debe ser reequilibrado. Hay varios casos a considerar. Caso 1: N es la nueva raíz. En este caso, hemos acabado. Borramos un nodo negro de cada camino y la nueva raíz es negra, así las propiedades se cumplen. Una posible implementación en el lenguaje de programación C sería la siguiente: 84

void eliminar_caso1(struct node *n) { if (n->padre!= NULL) eliminar_caso2(n); }

Nota: En los casos 2, 5 y 6, asumimos que N es el hijo izquierdo de su padre P. Si éste fuese el hijo derecho, la izquierda y la derecha deberían ser invertidas en todos estos casos. De nuevo, el código del ejemplo toma ambos casos en cuenta.

Caso 2: S es rojo. En este caso invertimos los colores de P y S, por lo que rotamos a la izquierda P, pasando S a ser el abuelo de N. Nótese que P tiene que ser negro al tener un hijo rojo. Aunque todos los caminos tienen todavía el mismo número de nodos negros, ahora N tiene un hermano negro y un padre rojo, así que podemos proceder a al paso 4, 5 o 6 (este nuevo hermano es negro porque éste era uno de los hijos de S, que es rojo). En casos posteriores, reetiquetaremos el nuevo hermano de N como S. Aquí podemos ver una implementación: void eliminar_caso2(struct node *n) { struct node *hm = hermano_menor(n); if (hm->color == ROJO) { n->padre->color = ROJO; hm->color = NEGRO; if (n == n->padre->izdo) rotar_izda(n->padre); else rotar_dcha(n->padre); } eliminar_caso3(n); }

85

Caso 3: P, S y los hijos de S son negros. En este caso, simplemente cambiamos S a rojo. El resultado es que todos los caminos a través de S, precisamente aquellos que no pasan por N, tienen un nodo negro menos. El hecho de borrar el padre original de N haciendo que todos los caminos que pasan por N tengan un nodo negro menos nivela el árbol. Sin embargo, todos los caminos a través de P tienen ahora un nodo negro menos que los caminos que no pasan por P, así que la propiedad 5 aún no se cumple (todos los caminos desde cualquier nodo a su nodo hijo contienen el mismo número de nodos negros). Para corregir esto, hacemos el proceso de reequilibrio en P, empezando en el caso 1. Su implementación en C: void eliminar_caso3(struct node *n) { struct node *hm = hermano_menor(n); if ((n->padre->color == NEGRO) && (hm->color == NEGRO) && (hm->izdo->color == NEGRO) && (hm->dcho->color == NEGRO)) { hm->color = ROJO; eliminar_caso1(n->padre); } else eliminar_caso4(n); }

Caso 4: S y los hijos de éste son negros, pero P es rojo. En este caso, simplemente intercambiamos los colores de S y P. Esto no afecta al número de nodos negros en los caminos que no van a través de S, pero añade uno al número de nodos negros a los caminos que van a través de N, compensando así el borrado del nodo negro en dichos caminos. Si lo implementamos en C, quedaría: void eliminar_caso4(struct node *n) { struct node *hm = hermano_menor(n); if ((n->padre->color == ROJO) && (hm->color == NEGRO) && (hm->izdo->color == NEGRO) && (hm->dcho->color == NEGRO)) { hm->color = ROJO; n->padre->color = NEGRO; } else eliminar_caso5(n); }

86

Caso 5: S es negro, su hijo izquierdo es rojo, el derecho es negro, y N es el hijo izquierdo de su padre. En este caso rotamos a la derecha S, así su hijo izquierdo se convierte en su padre y en el hermano de N. Entonces intercambiamos los colores de S y su nuevo padre. Todos los caminos tienen aún el mismo número de nodos negros, pero ahora N tiene un hermano negro cuyo hijo derecho es rojo, así que caemos en el caso 6. Ni N ni su padre son afectados por esta transformación (de nuevo, por el caso 6, reetiquetamos el nuevo hermano de N como S). He aquí la implementación en C: void eliminar_caso5(struct node *n) { struct node *hm = hermano_menor(n); if ((n == n->padre->izdo) && (hm->color == NEGRO) && (hm->izdo->color == ROJO) && (hm->dcho->color == NEGRO)) { hm->color = ROJO; hm->izdo->color = NEGRO; rotar_dcha(hm); } else if ((n == n->padre->dcho) && (hm->color == NEGRO) && (hm->dcho->color == ROJO) && (hm->izdo->color == NEGRO)) { hm->color = ROJO; hm->dcho->color = NEGRO eliminar_caso6(n);

}

}

Caso 6: S es negro, su hijo derecho es rojo, y N es el hijo izquierdo de P, su padre. En este caso rotamos a la izquierda P, así que S se convierte en el padre de P y éste en el hijo derecho de S. Entonces intercambiamos los colores de P y S, y ponemos el hijo derecho de S en negro. El subárbol aún tiene el 87

mismo color que su raíz, así que las propiedades 4 (los hijos de todo nodo rojo son negros) y 5 (todos los caminos desde cualquier nodo a sus nodos hoja contienen el mismo número de nodos negros) se verifican. Sin embargo, N tiene ahora un antecesor negro mas: o bien P se ha convertido en negro, o bien era negro y S se ha añadido como un abuelo negro. De este modo, los caminos que pasan por N pasan por un nodo negro más. Mientras tanto, si un camino no pasa por N, entonces hay dos posibilidades: Éste pasa a través del nuevo hermano de N. Entonces, éste debe pasar por S y P, al igual que antes, y tienen sólo que intercambiar los colores. Así los caminos contienen el mismo número de nodos negros. Éste pasa por el nuevo tío de N, el hijo derecho de S. Éste anteriormente pasaba por S, su padre y su hijo derecho, pero ahora sólo pasa por S, el cual ha tomado el color de su anterior padre, y por su hijo derecho, el cual ha cambiado de rojo a negro. El efecto final es que este camino va por el mismo número de nodos negros.

De cualquier forma, el número de nodos negros en dichos caminos no cambia. De este modo, hemos restablecido las propiedades 4 (los hijos de todo nodo rojo son negros) y 5 (todos los caminos desde cualquier nodo a sus nodos hoja contienen el mismo número de nodos negros). El nodo blanco en diagrama puede ser rojo o negro, pero debe tener el mismo color tanto antes como después de la transformación. Adjuntamos el último algoritmo: void eliminar_caso6(struct node *n) { struct node *hm = hermano_menor(n); hm->color = n->padre->color; n->padre->color = NEGRO; if (n == n->padre->izdo) {

/* * Aquí, hm->dcho->color == ROJO. */ hm->dcho->color = NEGRO; rotar_izda(n->padre); } else {

/* * Aquí, hm->izdo->color == ROJO. */ hm->izdo->color = NEGRO; rotar_dcha(n->padre); } }

De nuevo, todas las llamadas de la función usan recursión de cola así que el algoritmo realiza sus operaciones sobre el propio árbol. Además, las llamadas no recursivas se harán después de una rotación, luego se harán un número de rotaciones (más de 3) que será constante.

Demostración de cotas Un árbol rojo-negro que contiene n nodos internos tiene una altura de O(log(n)). Hagamos los siguientes apuntes sobre notación: 88

H(v) = altura del árbol cuya raíz es el nodo v. bh(v) = número de nodos negros (sin contar v si es negro) desde v hasta cualquier hoja del subárbol (llamado altura-negra).

Lema: Un subárbol enraizado al nodo v tiene al menos 2bh(v) − 1 nodos internos. Demostración del lema (por inducción sobre la altura): Caso base: h(v)=0 Si v tiene altura cero entonces debe ser árbol vacío, por tanto bh(v)=0. Luego: 2bh(''v'') − 1 = 20 − 1 = 1 − 1 = 0

Hipótesis de Inducción: si v es tal que h(v) = k y contiene 2bh(v) − 1 nodos internos, veamos que esto implica que v' tal que h(v') = k+1 contiene 2bh(v') − 1 nodos internos. Si v' tiene h(v') > 0 entonces es un nodo interno. Como éste tiene dos hijos que tienen altura-negra, o bh(v') o bh(v')-1 (dependiendo si es rojo o negro). Por la hipótesis de inducción cada hijo tiene al menos 2bh(v') − 1 − 1 nodos internos, así que v' tiene :2bh(v') − 1 − 1 + 2bh(v') − 1 − 1 + 1 = 2bh(v') − 1 nodos internos. Usando este lema podemos mostrar que la altura del árbol es algorítmica. Puesto que al menos la mitad de los nodos en cualquier camino desde la raíz hasta una hoja negra (propiedad 4 de un árbol rojonegro), la altura-negra de la raíz es al menos h(raíz)/2. Por el lema tenemos que:

Por tanto, la altura de la raíz es O(log(n)).

Complejidad En el código del árbol hay un bucle donde la raíz de la propiedad rojo-negro que hemos querido devolver a su lugar, x, puede ascender por el árbol un nivel en cada iteración Como la altura original del árbol es O(log n), hay O(log n) iteraciones. Así que en general la inserción tiene una complejidad de O(log n).

Referencias Hernández, Zenón; Rodríguez, J.Carlos; González, J.Daniel; Díaz, Margarita; Pérez, José; Rodríguez, Gustavo. (2005). Fundamentos de Estructuras de Datos. Soluciones en Ada, Java y C++.. Madrid: Thomson Editores Spain. 84-9732-358-0. Mathworld: Red-Black Tree. San Diego State University: CS 660: Red-Black tree notes, by Roger Whitney. Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein. Introduction to Algorithms, Second Edition. MIT Press and McGraw-Hill, 2001. ISBN 0-262-03293-7 . Chapter 13: Red-Black Trees, pp.273–301. Pfaff, Ben (June de 2004). «Performance Analysis of BSTs in System Software» (PDF). Stanford_university.. Okasaki, Chris. «Red-Black Trees in a Functional Setting» (PS).. 89

Árbol AVL Árbol AVL es un tipo especial de árbol binario ideado por los matemáticos rusos Adelson-Velskii y Landis. Fue el primer árbol de búsqueda binario auto-balanceable que se ideó.

Descripción

Un ejemplo de árbol binario no equilibrado (no es AVL)

Un ejemplo de árbol binario equilibrado (sí es AVL)

El árbol AVL toma su nombre de las iniciales de los apellidos de sus inventores, Adelson-Velskii y Landis. Lo dieron a conocer en la publicación de un artículo en 1962: "An algorithm for the organization of information" ("Un algoritmo para la organización de la información"). Los árboles AVL están siempre equilibrados de tal modo que para todos los nodos, la altura de la rama izquierda no difiere en más de una unidad de la altura de la rama derecha. Gracias a esta forma de equilibrio (o balanceo), la complejidad de una búsqueda en uno de estos árboles se mantiene siempre en orden de complejidad O(log n). El factor de equilibrio puede ser almacenado directamente en cada nodo o ser computado a partir de las alturas de los subárboles. Para conseguir esta propiedad de equilibrio, la inserción y el borrado de los nodos se ha de realizar de una forma especial. Si al realizar una operación de inserción o borrado se rompe la condición de equilibrio, hay que realizar una serie de rotaciones de los nodos. Los árboles AVL más profundos son los árboles de Fibonacci. 90

Definición formal Definición de la altura de un árbol Sea T un árbol binario de búsqueda y sean Ti y Td sus subárboles, su altura H(T), es: 0 si el árbol T contiene solo la raíz 1 + max(H(Ti),H(Td)) si contiene más nodos

Definición de árbol AVL Arboles balanceados o equilibrados: · Un árbol binario de búsqueda es k-equilibrado si cada nodo lo es. · Un nodo es k-equilibrado si las alturas de sus subárboles izquierdo y derecho difieren en no más de k. Arboles AVL (Adel‘son, Vel‘skii, Landis) · Un árbol binario de búsqueda 1-equilibrado se llama árbol AVL. Cabe destacar que un árbol AVL no es un Tipo de dato abstracto (TDA) sino una estructura de datos.

Factor de equilibrio Cada nodo, además de la información que se pretende almacenar, debe tener los dos punteros a los árboles derecho e izquierdo, igual que los árboles binarios de búsqueda (ABB), y además el dato que controla el factor de equilibrio. El factor de equilibrio es la diferencia entre las alturas del árbol derecho y el izquierdo: FE = altura subárbol derecho - altura subárbol izquierdo; Por definición, para un árbol AVL, este valor debe ser -1, 0 ó 1.

Si el factor de equilibrio de un nodo es: 0 -> el nodo está equilibrado y sus subárboles tienen exactamente la misma altura. 1 -> el nodo está equilibrado y su subárbol derecho es un nivel más alto. -1 -> el nodo está equilibrado y su subárbol izquierdo es un nivel más alto.

Si el factor de equilibrio Fe≥2 o Fe≤-2 es necesario reequilibrar.

Operaciones Las operaciones básicas de un árbol AVL implican generalmente el realizar los mismos algoritmos que serían realizados en un árbol binario de búsqueda desequilibrado, pero precedido o seguido por una o más de las llamadas "rotaciones AVL". Todo tipo de operaciones del árbol es una sinergidad de la solumniscensia del pivote.

91

Rotaciones El reequilibrado se produce de abajo hacia arriba sobre los nodos en los que se produce el desequilibrio. Pueden darse dos casos: rotación simple o rotación doble; a su vez ambos casos pueden ser hacia la derecha o hacia la izquierda. ROTACIÓN SIMPLE A LA DERECHA. De un árbol de raíz (r) y de hijos izquierdo (i) y derecho (d), lo que haremos será formar un nuevo árbol cuya raíz sea la raíz del hijo izquierdo, como hijo izquierdo colocamos el hijo izquierdo de i (nuestro i‘) y como hijo derecho construimos un nuevo árbol que tendrá como raíz, la raíz del árbol (r), el hijo derecho de i (d‘) será el hijo izquierdo y el hijo derecho será el hijo derecho del árbol (d).

op rotDer: AVL{X} -> [AVL{X}] . eq rotDer(arbolBin(R1, arbolBin(R2, I2, D2), D1)) == arbolBin(R2, I2, arbolBin(R1, D2, D)) .

ROTACIÓN SIMPLE A LA IZQUIERDA. De un árbol de raíz (r) y de hijos izquierdo (i) y derecho (d), consiste en formar un nuevo árbol cuya raíz sea la raíz del hijo derecho, como hijo derecho colocamos el hijo derecho de d (nuestro d‘) y como hijo izquierdo construimos un nuevo árbol que tendrá como raíz la raíz del árbol (r), el hijo izquierdo de d será el hijo derecho (i‘) y el hijo izquierdo será el hijo izquierdo del árbol (i). Precondición: Tiene que tener hijo derecho no vacío.

92

op rotIzq: AVL{X} -> [AVL{X}] . eq rotIzq(arbolBin(R1, I, arbolBin(R2, I2, D2))) == arbolBin(R2, arbolBin(R1, I, I2), D2) .

Si la inserción se produce en el hijo derecho del hijo izquierdo del nodo desequilibrado (o viceversa) hay que realizar una doble rotación. ROTACIÓN DOBLE A LA DERECHA.

93

ROTACIÓN DOBLE A LA IZQUIERDA.

94

Inserción [editar] La inserción en un árbol de AVL puede ser realizada insertando el valor dado en el árbol como si fuera un árbol de búsqueda binario desequilibrado y después retrocediendo hacia la raíz, rotando sobre cualquier nodo que pueda haberse desequilibrado durante la inserción. Proceso de inserción: 1.buscar hasta encontrar la posición de inserción o modificación (proceso idéntico a inserción en árbol binario de búsqueda) 2.insertar el nuevo nodo con factor de equilibrio “equilibrado” 3.desandar el camino de búsqueda, verificando el equilibrio de los nodos, y re-equilibrando si es necesario

95

96

Debido a que las rotaciones son una operación que tienen complejidad constante y a que la altura esta limitada a O (log(n)), el tiempo de ejecución para la inserción es del orden O (log(n)). op insertar: X$Elt AVL{X} -> AVLNV{X} . eq insertar(R, crear) == arbolBin(R, crear, crear) . ceq insertar(R1, arbolBin(R2, I, D)) == if (R1==R2) then

97

arbolBin(R2, I, D) elseif (R1 raiz(D)) then rotarIzq(arbolBin(R, I, insertar(R1, D))) else rotatIzq(arbolBin(R, I, rotarDer(insertar(R1, D)))) fi . fi . fi .

Extracción El procedimiento de borrado es el mismo que en el caso de árbol binario de búsqueda.La diferencia se encuentra en el proceso de reequilibrado posterior. El problema de la extracción puede resolverse en O (log n) pasos. Una extracción trae consigo una disminución de la altura de la rama donde se extrajo y tendrá como efecto un cambio en el factor de equilibrio del nodo padre de la rama en cuestión, pudiendo necesitarse una rotación. Esta disminución de la altura y la corrección de los factores de equilibrio con sus posibles rotaciones asociadas pueden propagarse hasta la raíz.

Borrar A, y la nueva raíz será M. 98

Borrado A, la nueva raíz es M. Aplicamos la rotación a la derecha.

El árbol resultante ha perdido altura. En borrado pueden ser necesarias varias operaciones de restauración del equilibrio, y hay que seguir comprobando hasta llegar a la raíz. op eliminar: X$Elt AVL{X} -> AVL{X} . eq eliminar(R, crear) == crear . ceq eliminar(R1, arbolBin(R2, I, D)) == if (R1 == R2) then if esVacio(I) then D elseif esVacio(D) then

99

I else if (altura(I) - altura(eliminar(min(D),D)) < 2) then arbolBin(min(D), I, eliminar(min(D), D)) ***tenemos que reequilibrar elseif (altura(hijoIzq(I) >= altura(hijoDer(I)))) then rotDer(arbolBin(min(D), I, eliminar(min(D),D))) else rotDer(arbolBin(min(D), rotIzq(I), eliminar(min(D),D)))

Búsqueda Las búsquedas se realizan de la misma manera que en los ABB, pero al estar el árbol equilibrado la complejidad de la búsqueda nunca excederá de O (log n). En un AVL se consigue que las búsquedas sean siempre de complejidad logarítmica, oscilando entre log N en el mejor caso y 1,44 log N en el peor caso.

Árbol biselado Un Árbol biselado o Árbol Splay es un Árbol binario de búsqueda auto-balanceable, con la propiedad adicional de que a los elementos accedidos recientemente se accederá más rápidamente en accesos posteriores. Realiza operaciones básicas como pueden ser la inserción, la búsqueda y el borrado en un tiempo del orden de O(log n). Para muchas secuencias no uniformes de operaciones, el árbol biselado se comporta mejor que otros árboles de búsqueda, incluso cuando el patrón específico de la secuencia es desconocido. Esta estructura de datos fue inventada por Robert Tarjan y Daniel Sleator. Todas las operaciones normales de un árbol binario de búsqueda son combinadas con una operación básica, llamada biselación. Esta operación consiste en reorganizar el árbol para un cierto elemento, colocando éste en la raíz. Una manera de hacerlo es realizando primero una búsqueda binaria en el árbol para encontrar el elemento en cuestión y, a continuación, usar rotaciones de árboles de una manera específica para traer el elemento a la cima. Alternativamente, un algoritmo "de arriba a abajo" puede combinar la búsqueda y la reorganización del árbol en una sola fase.

Ventajas e inconvenientes El buen rendimiento de un árbol biselado depende del hecho de que es auto-balanceado, y además se optimiza automáticamente. Los nodos accedidos con más frecuencia se moverán cerca de la raíz donde podrán ser accedidos más rápidamente. Esto es una ventaja para casi todas las aplicaciones, y es particularmente útil para implementar cachés y algoritmos de recolección de basura; sin embargo, es importante apuntar que para un acceso uniforme, el rendimiento de un árbol biselado será considerablemente peor que un árbol de búsqueda binaria balanceado simple. Los árboles biselados también tienen la ventaja de ser consideradamente más simples de implementar que otros árboles binarios de búsqueda auto-balanceados, como pueden ser los árboles RojoNegro o los árboles AVL, mientras que su rendimiento en el caso promedio es igual de eficiente. Además, los árboles biselados no necesitan almacenar ninguna otra información adicional a parte de los propios datos, minimizando de este modo los requerimientos de memoria. Sin embargo, estas estructuras de datos adicionales proporcionan garantías para el peor caso, y pueden ser más eficientes en la práctica para el acceso uniforme. 100

Uno de los peores casos para el algoritmo básico del árbol biselado es el acceso secuencial a todos los elementos del árbol de forma ordenada. Esto deja el árbol completamente des balanceado (son necesarios n accesos, cada uno de los cuales del orden de O(log n) operaciones). Volviendo a acceder al primer elemento se dispara una operación que toma del orden de O(n) operaciones para volver a balancear el árbol antes de devolver este primer elemento. Esto es un retraso significativo para esa operación final, aunque el rendimiento se amortiza si tenemos en cuenta la secuencia completa, que es del orden de O(log n). Sin embargo, investigaciones recientes muestran que si aleatoriamente volvemos a balancear el árbol podemos evitar este efecto de desbalance y dar un rendimiento similar a otros algoritmos de autobalanceo. Al contrario que otros tipos de árboles auto balanceados, los árboles biselados trabajan bien con nodos que contienen claves idénticas. Incluso con claves idénticas, el rendimiento permanece amortizado del orden de O(log n). Todas las operaciones del árbol preservan el orden de los nodos idénticos dentro del árbol, lo cual es una propiedad similar a la estabilidad de los algoritmos de ordenación. Un operación de búsqueda cuidadosamente diseñada puede devolver el nodo más a la izquierda o más a la derecha de una clave dada.

Operaciones Búsqueda La búsqueda de un valor de clave en un árbol biselado tiene la característica particular de que modifica la estructura del árbol. El descenso se efectúa de la misma manera que un árbol binario de búsqueda, pero si se encuentra un nodo cuyo valor de clave coincide con el buscado, se realiza una biselación de ese nodo. Si no se encuentra, el nodo biselado será aquel que visitamos por último antes de descartar la búsqueda. Así, la raíz contendrá un sucesor o predecesor del nodo buscado.

Inserción Es igual que en el árbol binario de búsqueda con la salvedad de que se realiza una biselación sobre el nodo insertado. Además, si el valor de clave a insertar ya existe en el árbol, se bisela el nodo que lo contiene.

Extracción Esta operación requiere dos biselaciones. Primero se busca el nodo que contiene el valor de clave que se debe extraer. Si no se encuentra, el árbol es biselado en el último nodo examinado y no se realiza ninguna acción adicional. Si se encuentra, el nodo se bisela y se elimina. Con esto el árbol se queda separado en dos mitades, por lo que hay que seleccionar un nodo que haga las veces de raíz. Al ser un árbol binario de búsqueda y estar todos los valores de clave ordenados, podemos elegir como raíz el mayor valor del subárbol izquierdo o el menor valor de clave del derecho.

Operación de Biselación Esta operación traslada un nodo x, que es el nodo al que se accede, a la raíz . Para realizar esta operación debemos rotar el árbol de forma que en cada rotación el nodo x está más cerca de la raíz. Cada biselación realizada sobre el nodo de interés mantiene el árbol parcialmente equilibrado y además los 101

nodos recientemente accedidos se encuentran en las inmediaciones de la raíz. De esta forma amortizamos el tiempo empleado para realizar la biselación. Podríamos distinguir 3 casos generales: Caso 1: x es hijo izquierdo o derecho de la raíz, p. Caso 2: x es hijo izquierdo de p y este a su vez hijo izquierdo de q o bien ambos son hijos derechos. Caso 3: x es hijo izquierdo de p y este a su vez hijo derecho de q o viceversa. CASO 1:

Si x es hijo izquierdo de p entonces realizaremos una rotación simple derecha. En caso de que x sea el derecho la rotación que deberemos realizar es simple izquierda.

CASO 2:

Si x es hijo y nieto izquierdo de p y q, respectivamente. Entonces debemos realizar rotación doble a la derecha, en caso de que x sea hijo y nieto derecho de p y q la rotación será doble izquierda.

102

CASO 3:

En caso de que x sea hijo izquierdo de p y nieto derecho de q realizaremos una rotación simple derecha en el borde entre x y p y otra simple izquierda entre x y q. En caso contrario, x sea hijo derecho y nieto izquierdo de q, la rotaciones simples será izquierda y después derecha.

103

Teoremas de rendimiento Hay muchos teoremas y conjeturas con respecto al peor caso en tiempo de ejecución para realizar una secuencia S de m accesos en un árbol biselado con n elementos.

Teorema del balance El coste de realizar la secuencia de accesos S es del orden de O(m(logn + 1) + nlogn). En otras palabras, los árboles biselados se comportan tan bien como los árboles de búsqueda binaria con balanceo estático en secuencias de al menos n accesos.

Teorema de optimalidad estática Sea qi el número de veces que se accede al elemento i en S. El coste de realizar la secuencia de accesos S es del orden de . En otras palabras, los árboles biselados se comportan tan bien como los árboles binarios de búsqueda estáticos óptimos en las secuencias de al menos n accesos.

Teorema "Static Finger" Sea ij el elemento visitado en el j-ésimo acceso de S, y sea f un elemento fijo ("finger"). El coste de

realizar la secuencia de accesos S es del orden de

104

Teorema "Working Set" Sea t(j) el numero de elementos distintos accedidos desde la última vez que se accedió a j antes del instante i. El coste de realizar la secuencia de accesos S es del orden de

.

Teorema "Dynamic Finger" El

coste

de

realizar

la

secuencia

de

accesos

S

es

del

orden

de

.

Teorema "Scanning" También conocido como Teorema de Acceso Secuencial. El acceso a los n elementos de un árbol biselado en orden simétrico es de orden exácto Θ(n), independientemente de la estructura inicial del árbol. El límite superior más ajustado demostrado hasta ahora es 4,5n

Conjetura de optimalidad dinámica Además del las garantías probadas del rendimiento de los árboles biselados, en el documento original de Sleator y Tarjan hay una conjetura no probada de gran interés. Esta conjetura se conoce como la conjetura de optimalidad dinámica, y básicamente sostiene que los árboles biselados se comportan tan bien como cualquier otro algoritmo de búsqueda en árboles binarios hasta un factor constante. Conjetura de optimalidad dinámica: Sea A cualquier algoritmo de búsqueda binaria en árboles que accede a un elemento x atravesando el camino desde la raíz hasta x, a un coste de d(x) + 1, y que entre los accesos puede hacer cualquier rotación en el árbol a un coste de 1 por rotación. Sea A(S) el coste para que A realice la secuencia S de accesos. Entonces el coste de realizar los mismos accesos para un árbol biselado es del orden O(n + A (S)).

Existen varios corolarios de la conjetura de optimalidad dinámica que permanecen sin probar: Conjetura Transversal: Sean T1 y T2 dos árboles biselados que contienen los mismos elementos. Sea S la secuencia obtenida tras visitar los elementos de T2 en preorden. El coste total para realizar la secuencia S de accesos en T1 es del orden de O(n). Conjetura Deque: Sea S una secuencia de m operaciones de cola doblemente terminada (push, pop, inject, eject). Entonces el coste para la realización de esta secuencia de operaciones S en un árbol biselado es del orden de O(m + n). Conjetura Split: Sea S cualquier permutación de los elementos del árbol biselado. Entonces el coste de la eliminación de los elementos en el orden S es del orden de O(n).

105

Código de ejemplo /* * SplayTreeApplet: * * The applet demonstrates the Splay Tree. It takes textual commands in a TextArea * and when the user clicks on the Execute button, it processes the commands, updating * the display as it goes. * * @author Hyung-Joon Kim. CSE373, University of Washington. * * Copyrights Note: * This applet is extended from FullHuffmanApplet created by Prof. Steve Tanimoto, * Department of Computer Science and Engineering, University of Washington. * The setup of applet panels and the tree display methods are apprecicatively reused. * */ import import import import

javax.swing.*; java.awt.*; java.awt.event.*; java.util.*;

public class SplayTreeApplet extends JApplet implements ActionListener, Runnable { ScrolledPanel visPanel; //Donde se pinta el árbol MyScrollPane msp; Button executeButton; Button historyButton; TextArea userInputText; TextArea history; JFrame historyFrame; JTextField statusLine; // La Estructura de datos (árboles biselados) de la clase SplayTree. SplayTree theSplayTree; Font headingFont, treeFont; int topMargin = 40; // Space above top of the tree. int leftMargin = 20; // x value for left side of array. int rightMargin = leftMargin; int yTreeTops = topMargin + 10; // y coord of top of trees. int bottomMargin = 10; // Minimum space betw. bot. of visPanel and bot. of lowest cell. int leftOffset = 5; // space between left side of cell and contents string. int delay = 1500; // default is to wait 1500 ms between updates. Thread displayThread = null; // For SplayTree display: int nodeHeight = 25; // Para dibujar los nodos int nodeWidth = 25; // How wide to plot pink rectangles int nodeVGap = 10; // vertical space between successive nodes int nodeHGap = 10; // horizontal space between successive nodes

106

int nodeHorizSpacing = nodeWidth + nodeHGap; int nodeVertSpacing = nodeHeight + nodeVGap; int interTreeGap = 15; // horizontal space between trees. int ycentering = 3; Color treeColor = new Color(255,255,215); static int m; // variable used when computing columns for tree layouts. public void init() { setSize(700,500); // default size of applet. visPanel = new ScrolledPanel(); visPanel.setPreferredSize(new Dimension(400,400)); msp = new MyScrollPane(visPanel); msp.setPreferredSize(new Dimension(400,200)); Container c = getContentPane(); c.setLayout(new BorderLayout()); c.add(msp, BorderLayout.CENTER); JPanel buttons = new JPanel(); buttons.setLayout(new FlowLayout()); JPanel controls = new JPanel(); controls.setLayout(new BorderLayout()); executeButton = new Button("Execute"); executeButton.addActionListener(this); buttons.add(executeButton); historyButton = new Button("History"); historyButton.addActionListener(this); buttons.add(historyButton); userInputText = new TextArea("\n"); statusLine = new JTextField(); statusLine.setBackground(Color.lightGray); controls.add(buttons, BorderLayout.WEST); controls.add(userInputText, BorderLayout.CENTER); controls.add(statusLine, BorderLayout.SOUTH); controls.setPreferredSize(new Dimension(400,100)); c.add(controls, BorderLayout.SOUTH); c.validate(); theSplayTree = new SplayTree(); // Se crea una instancia de SplayTree headingFont = new Font("Helvetica", Font.PLAIN, 20); treeFont = new Font("Arial", Font.BOLD, 13); history = new TextArea("SplayTreeApplet history:\n", 20, 40); } class ScrolledPanel extends JPanel { public void paintComponent(Graphics g) { super.paintComponent(g); paintTrees(g); } } class MyScrollPane extends JScrollPane { MyScrollPane(JPanel p) { super(p, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); }

107

} public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals("Execute")) { displayThread = new Thread(this); displayThread.start(); return; } if (e.getActionCommand().equals("History")) { if (historyFrame == null) { historyFrame = new JFrame("History of the SplayTreeApplet"); history.setFont(new Font("Courier", Font.PLAIN, 12)); historyFrame.getContentPane().add(history); historyFrame.setSize(new Dimension(500,500)); } historyFrame.show(); } } // The following is executed by a separate thread for the display. public void run() { String commands = userInputText.getText(); String line = ""; StringTokenizer lines; for (lines = new StringTokenizer(commands, "\n\r\f"); lines.hasMoreTokens();) { line = lines.nextToken(); process(line, lines); } userInputText.setText(""); // Erase all the processed input. } // Helper function called by the run method above: void process(String command, StringTokenizer lines) { String arg1 = ""; String arg2 = ""; StringTokenizer st = new StringTokenizer(command); if (! st.hasMoreTokens()) { return; } String firstToken = st.nextToken(); if (firstToken.startsWith(";")) { return; } history.appendText(command + "\n"); statusLine.setText(command); if (firstToken.equals("RESET")) { theSplayTree = new SplayTree(); updateDisplay(); return; } if (firstToken.equals("DELAY")) { if (st.hasMoreTokens()) { arg1 = st.nextToken(); try { delay =(new Integer(arg1)).intValue(); } catch(NumberFormatException e) { delay = 0; } statusLine.setText("delay = " + delay); } history.appendText("; delay is now " + delay + "\n");

108

return; } if (firstToken.equals("INSERT")) { arg1 = "UNDEFINED ELEMENT"; if (st.hasMoreTokens()) { arg1 = st.nextToken(); } int data = (new Integer(arg1)).intValue(); if (data < 1 || data > 99) { String msg = "Input NOT valid. Should be between 1-99 integer number."; report(msg); return; } theSplayTree.insert(data); // insert an element checkScrolledPanelSize(); updateDisplay(); if (theSplayTree.statMode) { OldFamilySplayTree fam = theSplayTree.getRoot().oldFam; if (fam != null) { double currentDepth = fam.getFamilyDepth() / (double)fam.numFam; String msg2 = "Current(improved) average depth of old family: " + currentDepth; report(msg2); } } theSplayTree.getRoot().oldFam = null; // reset the old family of the root return; } if (firstToken.equals("FIND")) { arg1 = "UNDEFINED ELEMENT"; if (st.hasMoreTokens()) { arg1 = st.nextToken(); } int data = (new Integer(arg1)).intValue(); String msg = ""; // Check if data is already in the SplayTree and display the result. SplayTree node = theSplayTree.find(data); if ( node == null) { msg = "NOT FOUND: " + data + " is NOT in the Splay Tree"; } else { msg = "FOUND: " + node.element + " is in the Splay Tree."; } report(msg); checkScrolledPanelSize(); updateDisplay(); if (theSplayTree.statMode) { OldFamilySplayTree theSplayTree.getRoot().oldFam; if (fam != null) { double currentDepth (double)fam.numFam;

fam =

fam.getFamilyDepth()

= /

109

String msg2 = "Current(improved) average depth of old family: " + currentDepth; report(msg2); } } theSplayTree.getRoot().oldFam = null; // reset the old family of the root return; } if (firstToken.equals("DELETE")) { arg1 = "UNDEFINED ELEMENT"; if (st.hasMoreTokens()) { arg1 = st.nextToken(); } int data = (new Integer(arg1)).intValue(); theSplayTree.delete(data); // delete an element checkScrolledPanelSize(); updateDisplay(); if (theSplayTree.statMode) { OldFamilySplayTree fam = theSplayTree.getRoot().oldFam; if (fam != null) { double currentDepth = fam.getFamilyDepth() / (double)fam.numFam; String msg2 = "Current(improved) average depth of old family: " + currentDepth; report(msg2); } } theSplayTree.getRoot().oldFam = null; // reset the old family of the root return; } if (firstToken.equals("FIND-MIN")) { SplayTree node = theSplayTree.findMin(theSplayTree.getRoot()); theSplayTree.splay(node); // after find min, splay at the node checkScrolledPanelSize(); updateDisplay(); if (theSplayTree.statMode) { OldFamilySplayTree fam = theSplayTree.getRoot().oldFam; if (fam != null) { double currentDepth = fam.getFamilyDepth() / (double)fam.numFam; String msg2 = "Current(improved) average depth of old family: " + currentDepth; report(msg2); } } theSplayTree.getRoot().oldFam = null; // reset the old family of the root return; } if (firstToken.equals("FIND-MAX")) { SplayTree node = theSplayTree.findMax(theSplayTree.getRoot()); theSplayTree.splay(node); // after find max, splay at the node

110

checkScrolledPanelSize(); updateDisplay(); if (theSplayTree.statMode) { OldFamilySplayTree fam = theSplayTree.getRoot().oldFam; if (fam != null) { double currentDepth = fam.getFamilyDepth() / (double)fam.numFam; String msg2 = "Current(improved) average depth of old family: " + currentDepth; report(msg2); } } theSplayTree.getRoot().oldFam = null; // reset the old family of the root return; } if (firstToken.equals("STAT-MODE")) { if(delay < 2000) { delay = 2000; } theSplayTree.statMode = true; updateDisplay(); return; } history.appendText("[Unknown Splay Tree command]\n"); statusLine.setText("Unknown Spaly Tree command: " + command); } // Here is a "middleman" method that updates the display waiting with // the current time delay after each repaint request. void updateDisplay() { visPanel.repaint(); if (delay > 0) { try { Thread.sleep(delay); } catch(InterruptedException e) {} } } int getInt(String s) { int n = 1; try { Integer I = new Integer(s); n = I.intValue(); } catch(Exception e) { n = 1; } return n; }

/* The following computes the height of the display area needed by the current * heap, and if it won't fit in the scrolled panel, it enlarges the scrolled panel. */ void checkScrolledPanelSize() { // Compute width needed for trees: int treesWidthNeeded = leftMargin + rightMargin;

111

int treesHeightNeeded = 0; Dimension d = visPanel.getPreferredSize(); int currentHeight = (int) d.getHeight(); int currentWidth = (int) d.getWidth(); treesWidthNeeded += theSplayTree.getRoot().getDisplayWidth(); treesHeightNeeded += Math.max(treesHeightNeeded, theSplayTree.getDisplayHeight() + 2*topMargin); // Enlarge scrolled panel if necessary: int widthNeeded = treesWidthNeeded; int heightNeeded = treesHeightNeeded; if ((heightNeeded > currentHeight) || (widthNeeded > currentWidth)) { visPanel.setPreferredSize(new Dimension( Math.max(currentWidth,widthNeeded), Math.max(currentHeight,heightNeeded))); visPanel.revalidate(); // Adjust the vertical scroll bar. } }

/** * Splay Tree Class: * * This inner class is a data structure to be demostrated. Integer numbers are assigned * to the data field of Splay Tree, and each node of Splay Tree has left child, right child, * and parent. When a data is accessed by any operations such as FIND, INSERT, etc, Splay Tree * performs a sequence of self-reconstructing processes, so called 'splay', in order to bring * the most-recently-accessed node to the root of the tree. As a result, the nodes near the most* recently-accessed node become available for a fast accesss in futre. * * @author Hyung-Joon Kim, CSE373, University of Washington. * */ public class SplayTree { // data members for Splay trees : SplayTree root, leftSubtree, rightSubtree, parentTree; int element = 0; // comparable data member (valid between 1-99); OldFamilySplayTree oldFam; String splayStat = ""; // for stat report when splaying is performed int rotateCount = 0; boolean statMode = false; // The following are used in display layout: int depth; // relative y position of this node in the display int column; // relative x position in display, in units of nodes int maxdepth; // height of tree whose root is this node. int xcenter; // position of center of root relative to left side of tree. int ycenter;

/** 112

* Constructor por Defecto: */ SplayTree() { } // no hace nada

/** * Constructor: Crea un único nodo con los datos asignados. * @param x comparable data(integer number) */ SplayTree(int x) { element = x; }

/** * Comprueba si un nodo del árbol es externo - es decir, el nodo no tiene subarboles, es decir que sea una hoja. * @return true si el nodo es externo, sino false. */ boolean isExternal() { return ((leftSubtree == null) && (rightSubtree == null)); }

/** * Comprueba si un nodo es la raíz del árbol Splay * @return true si el nodo es la raíz, sino falso. */ boolean isRoot() { return (parentTree == null); }

/** * Asigna un nodo nuevo a la raíz del árbol Splay. * @param root toma el valor de node, que es la nueva raiz. */ void setRoot(SplayTree node) { root = node; }

/** * Accede a la raíz del árbol Splay. * @return la raíz del árbol Splay. */ SplayTree getRoot() { return root; }

/** * Comprueba si un nodo es el hijo izquierdo de su padre. * @return true si el nodo es el hijo izquierdo de su padre, sino false. */ boolean isLeftSubtree() { return ((parentTree != null) && (parentTree.leftSubtree == this)); }

/** * Comprueba si un nodo es el hijo derecho de su padre. * @return true si el nodo es el hijo derechoo de su padre, sino false. 113

*/ boolean isRightSubtree() { return ((parentTree != null) && (parentTree.rightSubtree == this)); }

/** * Encuentra el valor mínimo de los datos guardados en el Árbol Splay * @param T el nodo que funciona como raíz del árboles (o cualquier subarbol del Árbol Splay). * El valor se busca en el subárbol izquierdo debido a la filosofía de un AB.B * @return un nodo cuyo dato es el valor mínimo en el Árbol Splay. */ SplayTree findMin(SplayTree T) { if (T == null) { return null; } else if (T.leftSubtree == null) { return T; //T es el nodo cuyo elemento es el valor mínimo return findMin(T.leftSubtree); // Encuentra recursivamente en subárbol izquierdo el valor mínimo. }

el

/** *Buscar el máximo valor de los datos Árbol Splay. *@param T el nodo que funciona como raíz del árboles (o cualquier subarbol del Árbol Splay). * El valor se busca en el subárbol derecho debido a la filosofía de un AB.B * @return un nodo cuyo dato es el valor máximo en el Árbol Splay. */ SplayTree findMax(SplayTree T) { if (T == null) { return null; } else if (T.rightSubtree == null) { return T; // T es el nodo cuyo elemento es el valor máximo } return findMax(T.rightSubtree); // Encuentra recursivamente en el subárbol derecho el valor mínimo. }

/** * Encuentra un valor buscado en el árbol secuencialmente. * Si lo encuentra llama al método splay() para ajustar el árbol de tal manera que el nodo visitado quede más cerca de la raíz. * @param element valor a buscar en el Árbol Splay. * @return el nodo, si fue encontrado, en la que se realiza splaying. nulo si no se encuentra. */ SplayTree find(int element) { SplayTree T = root; //Encuentra el valor buscado hasta que el árbol no tenga más subárboles, utilizando las propiedas de un AB.B while (T != null) { if (T.element == element) { // Encontrado

114

break; } else if (T.element > element) { T = T.leftSubtree; } else { T = T.rightSubtree; } } if (T == null) { // No se encontró return null; } else { splay(T); // Se bisela el nodo encontrado return T; } }

/** * Inserta un nodo en el árbol Splay. Después de la inserción, el Árbol Splay realiza secuencial * Insert a node into the Splay Tree. After insertion, the Splay Tree performs sequential * self-reconstructing processes, so called splay, in order to bring the inserted node * up to the root of the tree. * @param element comparable data which will be inserted into the Splay Tree. */ void insert(int element) { // Crea un nuevo nodo con el elemento a insertar. SplayTree node = new SplayTree(element); //Si el árbol Splay está vacío, es decir, no tiene raíz, // entonces asigna el nuevo nodo a la raíz del árbol. if (root == null) { root = node; return; // En este caso no se necesita biselar el nodo. } SplayTree parent = null; SplayTree T = root; // Busca la ubicación adecuada para insertar el nodo, utilizando las propiedades de un AB,B. while (T != null) { parent = T; if (T.element == node.element) { // El elemento ya estaba en el Árbol Splay break; } else if (T.element > node.element) { T = T.leftSubtree; } else { T = T.rightSubtree; } }

115

if (node.element == parent.element) { String msg = parent.element + " is already in the Splay Tree."; report(msg); splay(parent); // Se bisela el nodo padre en caso de que sea el nodo que se iba a insertar. return; } //Inserta el nodo en el árbol Splay el la posición que se obtuvo en el while anterior. if (parent.element > node.element) { parent.leftSubtree = node; if (node != null) { node.parentTree = parent; } } else { parent.rightSubtree = node; if (node != null) { node.parentTree = parent; } } splay(node); // Despues de la inserción, se biselado el nodo. }

/** * This is a helper method for Delete method. It replaces a node with a new node so that * the new node is connected to the parent of the previous node. Note that it should take * care of the pointers of both direction (parent child). * @param T a node to be replaced. * @param newT a node to replace T. */ void replace(SplayTree T, SplayTree newT) { if (T.isRoot()) { // Update the root of the Splay Tree root = newT; if (newT != null) { newT.parentTree = null; } } else { if (T.isLeftSubtree()) { // Make newT be a new left child of the parent of the previous node, T T.parentTree.leftSubtree = newT; if (newT != null) { // Make newT have the parent of the previous node as a new parent newT.parentTree = T.parentTree; } } else { T.parentTree.rightSubtree = newT; if (newT != null) {

116

newT.parentTree = T.parentTree; } } } }

/** * Delete a node from the Splay Tree. When a node is deleted, its subtrees should * be reconnected to the Splay Tree somehow without violating the properties of BST. * If a node with two children is deleted, a node with the minimum-valued element * in the right subtrees replaces the deleted node. It does NOT guarantee the balance * of the Splay Tree. * @param x an element to be deleted from the Splay Tree. */ void delete(int x) { boolean wasRoot = false; SplayTree node = root; // Find the element to be deleted while (node != null) { if (node.element == x) { // Found break; } else if (node.element > x) { node = node.leftSubtree; } else { node = node.rightSubtree; } } if (node == null) { String msg = x + " is NOT in the Splay Tree."; report(msg); } else { wasRoot = node.isRoot(); // Remember whether the node is the root or not // The node has no subtrees, so just replace with null if (node.isExternal()) { replace(node, null); } // The node has at least one child, also the node might be the root else if (node.leftSubtree == null) { // the node has only right child replace(node, node.rightSubtree); } else if (node.rightSubtree == null) { // the node has only left child replace(node, node.leftSubtree); } else { // The node has two children // Get a successive node to replace the node that will be deleted SplayTree newNode = findMin(node.rightSubtree);

117

// Special case: the successive node is actually right child of the node to be deleted // The successive node will carry its own right child when it replace the node. if (newNode == node.rightSubtree) { replace(node, newNode); newNode.leftSubtree = node.leftSubtree; } else { // Now the succesive node should be replaced before it is used // Ensured that it has no left child since it's the minimum of subtrees if (newNode.rightSubtree == null) { replace(newNode, null); } else { // The succesive node has right child to take care of replace(newNode, newNode.rightSubtree); } // Replace the node with the succesive node, updating subtrees as well replace(node, newNode); newNode.leftSubtree = node.leftSubtree; newNode.rightSubtree = node.rightSubtree; } } String msg = x + " is succesively deleted from the Splay Tree."; report(msg); // Finally, splaying

at

the

parent

of

the

deleted

node. if (!wasRoot) { splay(node.parentTree); } else { splay(root); } // Delete the node completely node.leftSubtree = null; node.rightSubtree = null; node.parentTree = null; } }

/** * Splay a node until it reaches up to the root of the Splay Tree. Depending on the location * of a target node, parent, and grandparent, splaying applies one of Zig, Zig-Zig, or Zig-Zag * rotation at each stage. This method is called when a data is accessed by any operations. 118

* @param T a target node at which splaying is performed. */ void splay(SplayTree T) { // Remember total depth of T's family before splaying // Family consists of parent, sibling, children, but not including T itself // This is to see how the data near the splayed data are improved for faster // accesss in future, after spalying T.oldFam = new OldFamilySplayTree(T); double oldFamDepth = T.oldFam.getFamilyDepth() /(double)T.oldFam.numFam; // Keep splaying until T reaches the root of the Splay Tree while (!T.isRoot()) { SplayTree p = T.parentTree; SplayTree gp = p.parentTree; // T has a parent, but no grandparent if (gp == null) { splayStat = splayStat + "Zig "; if (T.isLeftSubtree()) { splayStat = splayStat + "from Left. "; } else { splayStat = splayStat + "from Right. "; } rotation(T); // Zig rotation rotateCount++; } else { // T has both parent and grandparent if (T.isLeftSubtree() == p.isLeftSubtree()) { // T and its parent are in the same direction: Zig-Zig rotation splayStat = splayStat + "Zig-Zig " ; if (T.isLeftSubtree()) { splayStat

=

splayStat + "from Left, "; } else { splayStat = splayStat + "from Right, "; } rotation(p); rotation(T); rotateCount++; } else { // T and its parent are NOT in the same direction: Zig-Zag rotation splayStat = splayStat + "Zig-Zag "; if (T.isRightSubtree()) { splayStat = splayStat + "from Left, "; } else { splayStat = splayStat + "from Right, "; } rotation(T); rotation(T); rotateCount++; } } }

119

// Report additional statistics of rotations if (statMode) { String stat = "Sequence of rotations: " + splayStat + "\n" + "; Total number of rotations: " + rotateCount + "\n" + "; Average depth of old family: " + oldFamDepth; report(stat); } splayStat reporting), reset the variables }

=

"";

rotateCount

=

0;

//

after

splaying(and

/** * Rotate subtrees of the Splay Tree. It updates subtrees of a grandparent, if exists, for * doulbe rotations, and performs single rotation depending on whether a node is left * child or right child. * @param T a node at which single rotation should be performed. */ void rotation(SplayTree T) { SplayTree p = T.parentTree; SplayTree gp = null; if (p != null) { gp = p.parentTree; } if (!T.isRoot()) { // Remember whether T is originally

left

child

or

right child final boolean wasLeft = T.isLeftSubtree(); // T has grandparent if (gp != null) { // Replace subtree of grandparent with T for Double rotations if (gp.leftSubtree == p) { gp.leftSubtree = T; T.parentTree = gp; } else { gp.rightSubtree = T; T.parentTree = gp; } } else { // T has no grandparent, set T to the new root. root = T; T.parentTree = null; } // Rotate from left if (wasLeft) { // Attach T's right child to its parent's left child p.leftSubtree = T.rightSubtree; if (T.rightSubtree != null) { T.rightSubtree.parentTree

=

p;

//

update the parent of T's subtree } // Now rotate T, so T's parent becomes T's right child T.rightSubtree = p;

120

if (p != null) { p.parentTree = T; } } else { // Rotate from right // Attach T's left child to its parent's right child p.rightSubtree = T.leftSubtree; if (T.leftSubtree != null) { T.leftSubtree.parentTree = p; // update the parent of T's subtree } // Now rotate T, so T's parent becomes T's left child T.leftSubtree = p; if (p != null) { p.parentTree = T; } } } }

/** * Self painting method. * @param g graphic object * @param xpos x-cordinate of a node in cartesian plane * @param ypos y-cordinate of a node in cartesian plane */ void paint(Graphics g, int xpos, int ypos) { treeColumns(); paintHelper(g, xpos, ypos); }

/** * Actually paint the tree by drawing line, node(circle), and data(integer) * @param g graphic object * @param xpos x-cordinate of a node in cartesian plane * @param ypos y-cordinate of a node in cartesian plane */ void paintHelper(Graphics g, int xpos, int ypos) { String space = ""; if (element < 10) { space = " "; } if (! isExternal()) { g.setColor(Color.blue); if (leftSubtree != null) { g.drawLine(xcenter + xpos - 10, ycenter + ypos, leftSubtree.xcenter + xpos, leftSubtree.ycenter

+

ypos - 10); } if (rightSubtree != null) { g.drawLine(xcenter + xpos + 10, ycenter + ypos, rightSubtree.xcenter + xpos,

121

rightSubtree.ycenter

+

ypos - 10); } g.setColor(new Color(102,0,204)); g.fillOval(xpos + column*nodeHorizSpacing -1, ypos + depth*nodeVertSpacing -1, nodeWidth+2, nodeHeight+2); g.setColor(treeColor); g.fillOval(xpos + column*nodeHorizSpacing,

ypos

+

depth*nodeVertSpacing, nodeWidth, nodeHeight); g.setColor(Color.black); } if (isExternal()) { g.setColor(new Color(102,0,204)); g.fillOval(xpos + column*nodeHorizSpacing -1, ypos + depth*nodeVertSpacing -1, nodeWidth+2, nodeHeight+2); g.setColor(treeColor); g.fillOval(xpos + column*nodeHorizSpacing, ypos + depth*nodeVertSpacing, nodeWidth, nodeHeight); g.setColor(Color.black); g.drawString( space + element, xpos

+

column*nodeHorizSpacing + leftOffset, ypos + depth*nodeVertSpacing + nodeHeight - 8 ); } else { g.drawString(space + element, xpos + column*nodeHorizSpacing + leftOffset, ypos + depth*nodeVertSpacing + nodeHeight - 8); // recursive call to paint subtrees if (leftSubtree != null) { leftSubtree.paintHelper(g, xpos, ypos); } if (rightSubtree != null) { rightSubtree.paintHelper(g, xpos, ypos); } } }

/** * Inorder traversal, filling in the depth and the column index of each node * for display purposes. It should also deal with the case where a node has * only one child. * @param currentDepth the current depth of a node * @return the column index of the rightmost node. */ int traverse(int currentDepth) { depth = currentDepth; if (isExternal()) { column = m; m++;

122

maxdepth = depth; } if (leftSubtree == null && rightSubtree != null) { column = m; m++; } if (leftSubtree != null){ leftSubtree.traverse(depth + 1); column = m; m++; } xcenter = column*nodeHorizSpacing + (nodeWidth / 2); ycenter = depth*nodeVertSpacing + nodeHeight - ycentering; if (rightSubtree != null) { int rm = rightSubtree.traverse(depth + 1); if (leftSubtree == null) { maxdepth = rightSubtree.maxdepth; } else { maxdepth = Math.max(leftSubtree.maxdepth, rightSubtree.maxdepth); } return rm; } else { return column; } }

/** * Determine total column index of each node, filling a column index of * each node for display purpose. * @return total column index of the Splay Tree */ int treeColumns() { m = 0; return traverse(0) + 1; }

/** * Determine the height of the Splay Tree * @param T a node that roots the Splay Tree * @return the height of the Splay Tree at the node */ int treeHeight(SplayTree T) { if (T == null) { return -1; } return (1 + Math.max( treeHeight(T.rightSubtree))); }

treeHeight(T.leftSubtree),

/** * Get the width needed to display the tree * @return the width of the entire Splay Tree */ int getDisplayWidth() {

123

int hGap = nodeHorizSpacing - nodeWidth; int val = treeColumns() * (nodeHorizSpacing) - hGap; return val; }

/** * Get the height needed to display the tree * @return the height of the entire Splay Tree */ int getDisplayHeight() { int maxHeight = treeHeight(root); int val = (maxHeight+1)* nodeVertSpacing - nodeVGap; return val; } } //////////////////// End of SplayTree class /////////////////////

/** * OldFamilySplayTree Class: * * This inner class is to store old family members of a splayed node. * Therefore, after splaying, we can track the old family of the new root * and calculate the relative improvement in terms of depth. After splaying, * the most-recently-accessed data becomes available for O(1) access in the * next time. In addition, all family members also improve the access time * in future. This is a simple way to compare the cost of access to those * family memembers before splaying and after splaying. * * @author Hyung-Joon Kim * */ public class OldFamilySplayTree { // Family members of a node which will be splayed SplayTree oldParent, oldSibling, oldLeftChild, oldRightChild; int numFam; // Numbers of family members

/** * Default Constructor: * @param T a node which will be splayed and needs to create old family of it */ OldFamilySplayTree(SplayTree T) { if (T.parentTree != null) { oldParent = T.parentTree; numFam++; } if (T.leftSubtree != null) { oldLeftChild = T.leftSubtree; numFam++; } if (T.rightSubtree != null) { oldRightChild = T.rightSubtree; numFam++; } if (T.isLeftSubtree()) {

124

if (T.parentTree != null && T.parentTree.rightSubtree != null) { oldSibling = T.parentTree.rightSubtree; numFam++; } } else { if (T.parentTree != null && T.parentTree.leftSubtree != null) { oldSibling = T.parentTree.leftSubtree; numFam++; } } }

/** * Calculate the average depth of all family member nodes. * This method can calculate the depth before splaying by being called * in the SPLAY method, and the depth after splaying by being called * after repaiting the tree since the depth will is updated in the * PAINT method. * @return the average depth of all family member nodes. */ double getFamilyDepth() { double famDepth = 0.0; if (oldParent != null) { famDepth = famDepth + (double)oldParent.depth; } if (oldSibling != null) { famDepth = famDepth + (double)oldSibling.depth; } if (oldLeftChild != null) { famDepth = famDepth + (double)oldLeftChild.depth; } if (oldRightChild != null) { famDepth = famDepth + (double)oldRightChild.depth; } return famDepth; } } //////////////////// End of OldFamilySplayTree class ///////////////////// // Paint the Splay tree in a left-to-right sequence of trees. void paintTrees(Graphics g) { g.setFont(treeFont); int ystart = yTreeTops; int ypos = ystart; int xpos = leftMargin; g.setFont(headingFont); g.drawString("Splay Tree Demonstration :", xpos, yTreeTops - 20); if (theSplayTree.getRoot() != null) { g.setFont(treeFont); theSplayTree.getRoot().paint(g,xpos,ypos);

125

xpos += interTreeGap; xpos += theSplayTree.getRoot().getDisplayWidth(); } }

/* A handy function that reports a message to both the * status line of the applet and the history window. * Multiline messages are not fully visible in the status line. */ void report(String message) { statusLine.setText(message); history.appendText("; " + message + "\n"); } } // Recuerden que es un applet

Árbol multicamino Los árboles multicamino o árboles multirrama son estructuras de datos de tipo árbol usadas en computación.

Definición Un árbol multicamino posee un grado g mayor a dos, donde cada nodo de información del árbol tiene un máximo de g hijos. Sea un árbol de m-caminos A, es un árbol m-caminos si y solo si: A está vacío Cada nodo

de

A

muestra

la

siguiente

estructura:

[nClaves,Enlace0,Clave1,...,ClavenClaves,EnlacenClaves]

nClaves es el número de valores de clave de un nodo, pudiendo ser: 0
View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF