En informática , un árbol rojo-negro es una estructura de datos de árbol de búsqueda binaria autoequilibrada que se destaca por el rápido almacenamiento y recuperación de información ordenada. Los nodos de un árbol rojo-negro contienen un bit de "color" adicional, a menudo representado como rojo y negro, que ayuda a garantizar que el árbol esté siempre aproximadamente equilibrado. [1]
Cuando se modifica el árbol, el nuevo árbol se reorganiza y se "repinta" para restaurar las propiedades de coloración que limitan el desequilibrio que puede presentar el árbol en el peor de los casos. Las propiedades están diseñadas de modo que esta reorganización y recoloración se puedan realizar de manera eficiente.
El (re)equilibrio no es perfecto, pero garantiza la búsqueda en el tiempo, donde es el número de entradas en el árbol. Las operaciones de inserción y eliminación, junto con la reorganización y el cambio de color del árbol, también se ejecutan en el tiempo. [2] [3]
Para rastrear el color de cada nodo se necesita solo un bit de información por nodo porque solo hay dos colores (debido a la alineación de memoria presente en algunos lenguajes de programación, el consumo real de memoria puede diferir). El árbol no contiene ningún otro dato específico de que sea un árbol rojo-negro, por lo que su huella de memoria es casi idéntica a la de un árbol binario de búsqueda clásico (sin color) . En algunos casos, el bit de información adicional se puede almacenar sin costo adicional de memoria.
En 1972, Rudolf Bayer [4] inventó una estructura de datos que era un caso especial de orden 4 de un árbol B. Estos árboles mantenían todos los caminos desde la raíz hasta la hoja con el mismo número de nodos, creando árboles perfectamente equilibrados. Sin embargo, no eran árboles binarios de búsqueda. Bayer los llamó "árboles B binarios simétricos" en su artículo y más tarde se hicieron populares como árboles 2-3-4 o incluso árboles 2-3. [5]
En un artículo de 1978, "A Dichromatic Framework for Balanced Trees", [6] Leonidas J. Guibas y Robert Sedgewick derivaron el árbol rojo-negro del árbol B binario simétrico. [7] El color "rojo" fue elegido porque era el color más atractivo producido por la impresora láser a color disponible para los autores mientras trabajaban en Xerox PARC . [8] Otra respuesta de Guibas afirma que fue debido a los bolígrafos rojo y negro disponibles para dibujar los árboles. [9]
En 1993, Arne Andersson introdujo la idea de un árbol inclinado hacia la derecha para simplificar las operaciones de inserción y eliminación. [10]
En 1999, Chris Okasaki demostró cómo hacer que la operación de inserción fuera puramente funcional. Su función de equilibrio solo necesitaba ocuparse de 4 casos desequilibrados y un caso equilibrado predeterminado. [11]
El algoritmo original utilizaba 8 casos desequilibrados, pero Cormen et al. (2001) lo redujeron a 6 casos desequilibrados. [1] Sedgewick demostró que la operación de inserción se puede implementar en solo 46 líneas de código Java . [12] [13] En 2008, Sedgewick propuso el árbol rojo-negro inclinado a la izquierda , aprovechando la idea de Andersson que simplificaba las operaciones de inserción y eliminación. Sedgewick originalmente permitía nodos cuyos dos hijos fueran rojos, haciendo que sus árboles se parecieran más a árboles 2-3-4, pero más tarde se agregó esta restricción, haciendo que los árboles nuevos se parecieran más a árboles 2-3. Sedgewick implementó el algoritmo de inserción en solo 33 líneas, acortando significativamente sus 46 líneas de código originales. [14] [15]
Un árbol rojo-negro es un tipo especial de árbol binario de búsqueda , utilizado en informática para organizar fragmentos de datos comparables , como fragmentos de texto o números (como los números de las figuras 1 y 2). Los nodos que contienen claves o datos se denominan con frecuencia "nodos internos" , pero para que esto sea más específico, en este artículo también se los denomina nodos no NIL.
Los nodos de hoja de los árboles rojo-negros ( NIL en la figura 1) no contienen claves ni datos. Estas "hojas" no necesitan ser individuos explícitos en la memoria de la computadora: un puntero NULL puede, como en todas las estructuras de datos de árboles binarios, codificar el hecho de que no hay ningún nodo hijo en esta posición en el nodo (padre). Sin embargo, por su posición en el árbol, estos objetos están en relación con otros nodos que son relevantes para la estructura RB, pueden tener un nodo padre, hermano (es decir, el otro hijo del padre), tío, incluso sobrino; y pueden ser hijos, pero nunca padres, de otro nodo. En realidad, no es necesario atribuir un "color" a estos objetos de fin de ruta, porque la condición "es o " está implícita en la condición "es " (ver también esta observación). NIL
BLACK
NIL
La figura 2 muestra el mismo árbol rojo-negro conceptualmente sin estas hojas NIL. Para llegar a la misma noción de un camino , uno debe notar que, por ejemplo, 3 caminos pasan por el nodo 1 , es decir, un camino a través de 1 izquierda más 2 caminos agregados a través de 1 derecha , es decir, los caminos a través de 6 izquierda y 6 derecha . De esta manera, estos extremos de los caminos también son puntos de acoplamiento para que se inserten nuevos nodos, completamente equivalentes a las hojas NIL de la figura 1.
En lugar de ello, para ahorrar una cantidad marginal de tiempo de ejecución, estas (posiblemente muchas) hojas NIL pueden implementarse como punteros a un nodo centinela único (y negro) (en lugar de punteros de valor NULL ).
Como conclusión, el hecho de que un hijo no exista (no sea un nodo verdadero, no contenga datos) puede especificarse en todas las instancias mediante el mismo puntero NULL o como el mismo puntero a un nodo centinela. En este artículo, cualquiera de las dos opciones se denomina nodo NIL y tiene el valor constante NIL
.
La profundidad negra de un nodo se define como el número de nodos negros desde la raíz hasta ese nodo (es decir, el número de ancestros negros). La altura negra de un árbol rojo-negro es el número de nodos negros en cualquier camino desde la raíz hasta las hojas, que, por el requisito 4, es constante (alternativamente, podría definirse como la profundidad negra de cualquier nodo hoja). [16] : 154–165 La altura negra de un nodo es la altura negra del subárbol enraizado por él. En este artículo, la altura negra de un nodo NIL se establecerá en 0, porque su subárbol está vacío como lo sugiere la figura 2, y su altura de árbol también es 0.
Además de los requisitos impuestos a un árbol de búsqueda binario, un árbol rojo-negro debe satisfacer lo siguiente : [17]
Algunos autores, por ejemplo Cormen y otros [17], afirman que "la raíz es negra" es el quinto requisito, pero no Mehlhorn y Sanders [16] o Sedgewick y Wayne. [15] : 432–447 Dado que la raíz siempre se puede cambiar de roja a negra, esta regla tiene poco efecto en el análisis. Este artículo también la omite, porque altera ligeramente los algoritmos y las demostraciones recursivas.
Por ejemplo, cada árbol binario perfecto que consta únicamente de nodos negros es un árbol rojo-negro.
Las operaciones de solo lectura, como la búsqueda o el recorrido de un árbol, no afectan a ninguno de los requisitos. Por el contrario, las operaciones de modificación insert y delete mantienen fácilmente los requisitos 1 y 2, pero con respecto a los demás requisitos se debe hacer un esfuerzo adicional para evitar introducir una violación del requisito 3, llamadaviolación roja , o del requisito 4, llamadaviolación negra .
Los requisitos imponen una propiedad crítica de los árboles rojo-negros: el camino desde la raíz hasta la hoja más lejana no es más del doble de largo que el camino desde la raíz hasta la hoja más cercana . El resultado es que el árbol está equilibrado en altura . Dado que las operaciones como insertar, eliminar y encontrar valores requieren un tiempo en el peor de los casos proporcional a la altura del árbol, este límite superior en la altura permite que los árboles rojo-negros sean eficientes en el peor de los casos, es decir, logarítmicos en el número de entradas, es decir , que no es el caso de los árboles de búsqueda binarios ordinarios . Para una prueba matemática, consulte la sección Prueba de límites.
Los árboles rojo-negros, como todos los árboles binarios de búsqueda , permiten un acceso secuencial bastante eficiente (por ejemplo, un recorrido en orden , es decir: en el orden Izquierda-Raíz-Derecha) de sus elementos. Pero también admiten un acceso directo asintóticamente óptimo a través de un recorrido desde la raíz hasta la hoja, lo que resulta en tiempo de búsqueda.
Los árboles rojo-negros son similares en estructura a los árboles 2–3–4 , que son árboles B de orden 4. [18] En los árboles 2–3–4, cada nodo puede contener entre 1 y 3 valores y tener entre 2 y 4 hijos. Estos nodos 2–3–4 corresponden a grupos de nodo negro-hijos rojos en árboles rojo-negros, como se muestra en la figura 3. No es una correspondencia 1 a 1 , porque los 3 nodos tienen dos representaciones equivalentes: el hijo rojo puede estar a la izquierda o a la derecha. La variante del árbol rojo-negro con inclinación a la izquierda hace que esta relación sea exactamente 1 a 1, al permitir solo la representación del hijo izquierdo. Dado que cada nodo 2–3–4 tiene un nodo negro correspondiente, el invariante 4 de los árboles rojo-negros es equivalente a decir que las hojas de un árbol 2–3–4 se encuentran todas al mismo nivel.
A pesar de las similitudes estructurales, las operaciones en árboles rojo-negros son más económicas que en árboles B. Los árboles B requieren la gestión de vectores de longitud variable, mientras que los árboles rojo-negros son simplemente árboles binarios. [19]
Los árboles rojo-negros ofrecen garantías en el peor de los casos para el tiempo de inserción, el tiempo de eliminación y el tiempo de búsqueda. Esto no solo los hace valiosos en aplicaciones sensibles al tiempo, como las aplicaciones en tiempo real , sino que los convierte en valiosos bloques de construcción en otras estructuras de datos que brindan garantías en el peor de los casos. Por ejemplo, muchas estructuras de datos utilizadas en geometría computacional se basan en árboles rojo-negros, y el Completely Fair Scheduler y la llamada al sistema epoll del núcleo Linux utilizan árboles rojo-negros. [20] [21] El árbol AVL es otra estructura que admite la búsqueda, la inserción y la eliminación. Los árboles AVL pueden colorearse de rojo-negro y, por lo tanto, son un subconjunto de los árboles rojo-negros. La altura en el peor de los casos de AVL es 0,720 veces la altura en el peor de los casos de los árboles rojo-negros, por lo que los árboles AVL están equilibrados de manera más rígida. Las mediciones de rendimiento de Ben Pfaff con casos de prueba realistas en 79 ejecuciones encuentran proporciones AVL a RB entre 0,677 y 1,077, una mediana de 0,947 y una media geométrica de 0,910. [22] El rendimiento de los árboles WAVL se encuentra entre los árboles AVL y los árboles rojo-negros. [ cita requerida ]
Los árboles rojo-negros también son particularmente valiosos en la programación funcional , donde son una de las estructuras de datos persistentes más comunes , utilizadas para construir matrices y conjuntos asociativos que pueden conservar versiones anteriores después de mutaciones. La versión persistente de los árboles rojo-negros requiere espacio para cada inserción o eliminación, además de tiempo.
Por cada árbol 2–3–4 , existen árboles rojo–negro correspondientes con elementos de datos en el mismo orden. Las operaciones de inserción y eliminación en árboles 2–3–4 también son equivalentes a la inversión de color y las rotaciones en árboles rojo–negro. Esto hace que los árboles 2–3–4 sean una herramienta importante para comprender la lógica detrás de los árboles rojo–negro, y es por eso que muchos textos introductorios de algoritmos introducen los árboles 2–3–4 justo antes de los árboles rojo–negro, aunque los árboles 2–3–4 no se usan a menudo en la práctica.
En 2008, Sedgewick introdujo una versión más simple del árbol rojo-negro, llamada árbol rojo-negro inclinado a la izquierda [23], eliminando un grado de libertad no especificado previamente en la implementación. El LLRB mantiene una invariante adicional que establece que todos los enlaces rojos deben inclinarse a la izquierda, excepto durante las inserciones y eliminaciones. Los árboles rojo-negros se pueden hacer isométricos a árboles 2-3 [24] o árboles 2-3-4 [23] para cualquier secuencia de operaciones. La isometría del árbol 2-3-4 fue descrita en 1978 por Sedgewick [6] . Con árboles 2-3-4, la isometría se resuelve mediante un "cambio de color", correspondiente a una división, en la que el color rojo de dos nodos secundarios abandona los nodos secundarios y se mueve al nodo primario.
La descripción original del árbol de tango , un tipo de árbol optimizado para búsquedas rápidas, utiliza específicamente árboles rojo-negros como parte de su estructura de datos. [25]
A partir de Java 8, el HashMap se ha modificado de modo que, en lugar de utilizar una LinkedList para almacenar diferentes elementos con códigos hash en colisión , se utiliza un árbol rojo-negro. Esto da como resultado una mejora en la complejidad temporal de la búsqueda de dicho elemento desde hasta donde es el número de elementos con códigos hash en colisión. [26]
Las operaciones de solo lectura, como la búsqueda o el recorrido de un árbol, en un árbol rojo-negro no requieren ninguna modificación con respecto a las utilizadas para los árboles binarios de búsqueda , porque cada árbol rojo-negro es un caso especial de un árbol binario de búsqueda simple. Sin embargo, el resultado inmediato de una inserción o eliminación puede violar las propiedades de un árbol rojo-negro, cuya restauración se denomina reequilibrio, de modo que los árboles rojo-negros se vuelven autoequilibrados.Requiere en el peor de los casos un número pequeño, en notación Big O , donde es el número de objetos en el árbol, en promedio o amortizado , un número constante, [27] : 310 [16] : 158 de cambios de color (que son muy rápidos en la práctica); y no más de tres rotaciones de árbol [28] (dos para inserción).
Si la implementación de ejemplo a continuación no es adecuada, se pueden encontrar otras implementaciones con explicaciones en la biblioteca C anotada GNU libavl (v2.0.3 a junio de 2019) de Ben Pfaff [29] .
Los detalles de las operaciones de inserción y eliminación se demostrarán con un código C++ de ejemplo , que utiliza las definiciones de tipo, las macros a continuación y la función auxiliar para rotaciones:
// Definiciones de tipos básicos:enumeración color_t { NEGRO , ROJO }; struct RBnode { // nodo del árbol rojo-negro RBnode * padre ; // == NIL si es la raíz del árbol RBnode * hijo [ 2 ]; // == NIL si el hijo está vacío // El índice es: // IZQUIERDA := 0, si (clave < padre->clave) // DERECHA := 1, si (clave > padre->clave) enum color_t color ; int clave ; }; #define NIL NULL // puntero nulo o puntero a nodo centinela #define IZQUIERDA 0 #define DERECHA 1 #define hijo izquierdo[IZQUIERDA] #define hijo derecho[DERECHA]struct RBtree { // árbol rojo-negro RBnode * root ; // == NIL si el árbol está vacío }; // Obtener la dirección del hijo (∈ { IZQUIERDA, DERECHA }) // del nodo RB no raíz no NIL* N: #define childDir(N) ( N == (N->parent)->right ? RIGHT : LEFT )
RBnode * RotateDirRoot ( RBtree * T , // árbol rojo-negro RBnode * P , // raíz del subárbol ( puede ser la raíz de T ) int dir ) { // dir ∈ { IZQUIERDA, DERECHA } RBnode * G = P -> padre ; RBnode * S = P -> hijo [ 1 - dir ]; RBnode * C ; assert ( S != NIL ); // puntero al nodo verdadero requerido C = S -> hijo [ dir ]; P -> hijo [ 1 - dir ] = C ; if ( C != NIL ) C -> padre = P ; S -> hijo [ dir ] = P ; P -> padre = S ; S -> padre = G ; if ( G != NULL ) G -> hijo [ P == G -> derecha ? DERECHA : IZQUIERDA ] = S ; else T -> raíz = S ; return S ; // nueva raíz del subárbol } #define RotarDir(N,dir) RotarDirRaiz(T,N,dir) #define RotarIzquierda(N) RotarDirRaiz(T,N, IZQUIERDA) #define RotarDerecha(N) RotarDirRaiz(T,N,DERECHA)
La propuesta descompone tanto la inserción como la eliminación (sin mencionar algunos casos muy simples) en seis constelaciones de nodos, aristas y colores, que se denominan casos. La propuesta contiene, tanto para la inserción como para la eliminación, exactamente un caso que avanza un nivel negro más cerca de la raíz y hace bucles, mientras que los otros cinco casos reequilibran el árbol por sí solos. Los casos más complicados se representan en un diagrama.
U == NIL || U->color == BLACK // considered black
U != NIL && U->color == RED // not considered black
U == NIL
. Entonces, en ambos casos U->color
no se toca (véase Evaluación de cortocircuito ). considered black
está de acuerdo con el requisito 2).if
deben ocurrir con mucha menos frecuencia si la propuesta [30] se realiza.La inserción comienza colocando el nuevo nodo (no NIL), digamos N , en la posición en el árbol binario de búsqueda de un nodo NIL cuya clave de predecesor en orden se compara menos que la clave del nuevo nodo, que a su vez se compara menos que la clave de su sucesor en orden. (Con frecuencia, este posicionamiento es el resultado de una búsqueda dentro del árbol inmediatamente anterior a la operación de inserción y consta de un nodo P
junto con una dirección dir
con P->child[dir] == NIL
).
El nodo recién insertado se colorea temporalmente de rojo para que todas las rutas contengan la misma cantidad de nodos negros que antes. Pero si su padre, digamos P , también es rojo, entonces esta acción introduce una violación roja.
void RBinsert1 ( RBtree * T , // -> árbol rojo-negro struct RBnode * N , // -> nodo a insertar struct RBnode * P , // -> nodo padre de N (puede ser NULL) int dir ) // lado (IZQUIERDO o DERECHO) de P donde insertar N { struct RBnode * G ; // -> nodo padre de P struct RBnode * U ; // -> tío de N N -> color = ROJO ; N -> izquierda = NIL ; N -> derecha = NIL ; N -> padre = P ; if ( P == NULL ) { // No hay padre T -> raíz = N ; // N es la nueva raíz del árbol T. return ; // inserción completa } P -> hijo [ dir ] = N ; // inserta N como dir-hijo de P // inicio del bucle (do while): do {
El bucle de reequilibrio de la operación de inserción tiene la siguiente invariante :
dir
.El padre P del nodo actual es negro, por lo que se cumple el requisito 3. El requisito 4 también se cumple según el invariante de bucle.
if ( P -> color == BLACK ) { // Case_I1 (P black): return ; // inserción completa } // De ahora en adelante P es rojo. if (( G = P -> parent ) == NULL ) goto Case_I4 ; // P rojo y raíz // else: P rojo y G!=NULL. dir = childDir ( P ); // el lado del padre G en el que se encuentra el nodo P U = G -> child [ 1 - dir ]; // tío if ( U == NIL || U -> color == BLACK ) // considerado negro goto Case_I56 ; // P rojo && U negro
Si tanto el padre P como el tío U son rojos, entonces ambos pueden ser repintados de negro y el abuelo G se vuelve rojo para mantener el requisito 4. Dado que cualquier camino a través del padre o tío debe pasar por el abuelo, la cantidad de nodos negros en estos caminos no ha cambiado. Sin embargo, el abuelo G ahora puede violar el requisito 3, si tiene un padre rojo. Después de reetiquetar G a N, se cumple el invariante de bucle para que el reequilibrio se pueda iterar en un nivel negro (= 2 niveles de árbol) superior.
// Caso_I2 (P+U rojo): P -> color = NEGRO ; U -> color = NEGRO ; G -> color = ROJO ; N = G ; // nuevo nodo actual // iterar 1 nivel negro más arriba // (= 2 niveles de árbol) } while (( P = N -> padre ) != NULL ); // fin del bucle (do while)
El caso de inserción I2 se ha ejecutado varias veces y la altura total del árbol ha aumentado en 1, siendo ahora El nodo actual N es la raíz (roja) del árbol y se satisfacen todas las propiedades RB.
// Saliendo del bucle (do while) (después de haber pasado por Case_I2). // Case_I3: N es la raíz y rojo. return ; // inserción completa
El padre P es rojo y la raíz. Como N también es rojo, se viola el requisito 3. Pero después de cambiar el color de P , el árbol tiene forma de RB. La altura negra del árbol aumenta en 1.
Caso_I4 : // P es la raíz y rojo: P -> color = BLACK ; return ; // inserción completa
El padre P es rojo pero el tío U es negro. El objetivo final es rotar el nodo padre P a la posición de abuelo, pero esto no funcionará si N es un nieto "interno" de G (es decir, si N es el hijo izquierdo del hijo derecho de G o el hijo derecho del hijo izquierdo de G ). Una dir
rotación en P cambia los roles del nodo actual N y su padre P. La rotación agrega caminos a través de N (aquellos en el subárbol etiquetado 2 , vea el diagrama) y elimina caminos a través de P (aquellos en el subárbol etiquetado 4 ). Pero tanto P como N son rojos, por lo que se conserva el requisito 4. El requisito 3 se restaura en el caso 6.
Caso_I56 : // P rojo && U negro: if ( N == P -> child [ 1 - dir ]) { // Caso_I5 (P rojo && U negro && N nieto interno de G): RotateDir ( P , dir ); // P nunca es la raíz N = P ; // nuevo nodo actual P = G -> child [ dir ]; // nuevo padre de N // pasar a Caso_I6 }
Ahora es seguro que el nodo actual N es un nieto "externo" de G (a la izquierda del hijo izquierdo o a la derecha del hijo derecho). Ahora (1-dir)
-rote en G , colocando a P en lugar de G y haciendo que P sea el padre de N y G. G es negro y su hijo anterior P es rojo, ya que se violó el requisito 3. Después de cambiar los colores de P y G, el árbol resultante satisface el requisito 3. El requisito 4 también sigue satisfecho, ya que todos los caminos que pasaban por el G negro ahora pasan por el P negro .
// Caso_I6 (P rojo && U negro && N nieto externo de G): RotateDirRoot ( T , G , 1 - dir ); // G puede ser la raíz P -> color = NEGRO ; G -> color = ROJO ; return ; // inserción completa } // fin de RBinsert1
Debido a que el algoritmo transforma la entrada sin utilizar una estructura de datos auxiliar y utilizando solo una pequeña cantidad de espacio de almacenamiento adicional para variables auxiliares, se encuentra en el lugar .
- Cuando el nodo eliminado tiene 2 hijos (no NIL), podemos intercambiar su valor con su sucesor en orden (el hijo más a la izquierda del subárbol derecho) y luego eliminar el sucesor en su lugar. Dado que el sucesor está más a la izquierda, solo puede tener un hijo derecho (no NIL) o ningún hijo en absoluto.
- Cuando el nodo eliminado tiene solo un hijo (no NIL). En este caso, simplemente reemplace el nodo con su hijo y coloréelo de negro.
El único hijo (no NIL) debe ser rojo de acuerdo con la conclusión 5, y el nodo eliminado debe ser negro de acuerdo con el requisito 3.
- Cuando el nodo eliminado no tiene hijos (ambos NIL) y es la raíz, reemplácelo por NIL. El árbol queda vacío.
- Cuando el nodo eliminado no tiene hijos (ambos NIL) y está en rojo , simplemente elimine el nodo hoja.
- Cuando el nodo eliminado no tiene hijos (ambos NIL) y es negro , eliminarlo creará un desequilibrio y requerirá una reparación, como se explica en la siguiente sección.
El caso complejo es cuando N no es la raíz, está coloreada en negro y no tiene hijos propios (⇔ solo NIL hijos). En la primera iteración, N se reemplaza por NIL.
void RBdelete2 ( RBtree * T , // -> árbol rojo-negro struct RBnode * N ) // -> nodo que se eliminará { struct RBnode * P = N -> parent ; // -> nodo padre de N byte dir ; // lado de P en el que se encuentra N (∈ {LEFT, RIGHT}) struct RBnode * S ; // -> hermano de N struct RBnode * C ; // -> sobrino cercano struct RBnode * D ; // -> sobrino lejano // P != NULL, ya que N no es la raíz. dir = childDir ( N ); // lado del padre P en el que se encuentra el nodo N // Reemplazar N en su padre P por NIL: P -> child [ dir ] = NIL ; goto Start_D ; // saltar al bucle // inicio del bucle (do while): do { dir = childDir ( N ); // lado del padre P en el que se encuentra el nodo N Start_D : S = P -> child [ 1 - dir ]; // hermano de N (tiene altura negra >= 1) D = S -> child [ 1 - dir ]; // sobrino lejano C = S -> child [ dir ]; // sobrino cercano if ( S -> color == RED ) goto Case_D3 ; // S rojo ===> P+C+D negro // S es negro: if ( D != NIL && D -> color == RED ) // no se considera negro goto Case_D6 ; // D rojo && S negro if ( C != NIL && C -> color == RED ) // no se considera negro goto Case_D5 ; // C rojo && S+D negro // Aquí ambos sobrinos son == NIL (primera iteración) o negros (posteriormente). si ( P -> color == ROJO ) ir a Caso_D4 ; // P rojo && C+S+D negro
El bucle de reequilibrio de la operación de eliminación tiene la siguiente invariante :
dir
.Start_D
hasta "Eliminar caso D2", donde el problema de reequilibrio se escala a un nivel superior en el árbol en el que el padre P se convierte en el nuevo nodo actual N. Por lo tanto, se necesitan el máximo de iteraciones para reparar el árbol (donde es la altura del árbol). Debido a que la probabilidad de escalada disminuye exponencialmente con cada iteración, el costo total de reequilibrio es constante en promedio, de hecho, constante amortizado. (Solo como acotación al margen: Mehlhorn y Sanders señalan: "Los árboles AVL no admiten costos de actualización amortizados constantes". [16] : 165, 158 Esto es cierto para el reequilibrio después de una eliminación, pero no para la inserción AVL. [33] )El nodo actual N es la nueva raíz. Se ha eliminado un nodo negro de cada ruta, por lo que se conservan las propiedades de RB. La altura negra del árbol disminuye en 1.
// Caso_D1 (P == NULL): return ; // eliminación completa
Los hijos de P , S y S son negros. Después de pintar S de rojo, todos los caminos que pasan por S , que son precisamente aquellos caminos que no pasan por N , tienen un nodo negro menos. Ahora todos los caminos en el subárbol enraizado por P tienen el mismo número de nodos negros, pero uno menos que los caminos que no pasan por P , por lo que el requisito 4 todavía puede violarse. Después de reetiquetar P a N , se cumple el invariante de bucle de modo que el reequilibrio se puede iterar en un nivel negro (= 1 nivel de árbol) superior.
// Caso_D2 (P+C+S+D negro): S -> color = ROJO ; N = P ; // nuevo nodo actual (quizás la raíz) // iterar 1 nivel de negro // (= 1 nivel de árbol) más alto } while (( P = N -> padre ) != NULL ); // fin del bucle (do while) // (con retorno;)
El hermano S es rojo, por lo que P y los sobrinos C y D tienen que ser negros. Una dir
rotación en P convierte a S en el abuelo de N. Luego, después de invertir los colores de P y S , el camino a través de N todavía es corto por un nodo negro. Pero N ahora tiene un padre rojo P y después de la reasignación un hermano negro S , por lo que las transformaciones en los casos D4, D5 o D6 pueden restaurar la forma RB.
Case_D3 : // S rojo && P+C+D negro: RotateDirRoot ( T , P , dir ); // P puede ser la raíz P -> color = ROJO ; S -> color = NEGRO ; S = C ; // != NIL // ahora: P rojo && S negro D = S -> hijo [ 1 - dir ]; // sobrino lejano if ( D != NIL && D -> color == ROJO ) goto Case_D6 ; // D rojo && S negro C = S -> hijo [ dir ]; // sobrino cercano if ( C != NIL && C -> color == ROJO ) goto Case_D5 ; // C rojo && S+D negro // De lo contrario, C+D se considera negro. // pasa a Case_D4
Los hijos de los hermanos S y S son negros, pero P es rojo. Intercambiar los colores de S y P no afecta la cantidad de nodos negros en las rutas que pasan por S , pero sí suma uno a la cantidad de nodos negros en las rutas que pasan por N , lo que compensa el nodo negro eliminado en esas rutas.
Case_D4 : // P rojo && S+C+D negro: S -> color = ROJO ; P -> color = NEGRO ; return ; // eliminación completa
El hermano S es negro, el hijo cercano de S , C , es rojo y el hijo lejano de S , D, es negro. Después de una (1-dir)
rotación en S, el sobrino C se convierte en el padre de S y en el nuevo hermano de N. Los colores de S y C se intercambian. Todos los caminos siguen teniendo el mismo número de nodos negros, pero ahora N tiene un hermano negro cuyo hijo lejano es rojo, por lo que la constelación es apta para el caso D6. Ni N ni su padre P se ven afectados por esta transformación, y P puede ser rojo o negro (en el diagrama).
Case_D5 : // C rojo && S+D negro: RotateDir ( S , 1 - dir ); // S nunca es la raíz S -> color = ROJO ; C -> color = NEGRO ; D = S ; S = C ; // ahora: D rojo && S negro // pasan a Case_D6
El hermano S es negro, el hijo lejano de S , D, es rojo. Después de una dir
rotación en P, el hermano S se convierte en el padre de P y el hijo lejano de S , D. Los colores de P y S se intercambian y D se vuelve negro. Todo el subárbol todavía tiene el mismo color en su raíz S , es decir, rojo o negro.en el diagrama), que se refiere al mismo color tanto antes como después de la transformación. De esta manera se conserva el requisito 3. Los caminos en el subárbol que no pasan por N (es decir, que pasan por D y el nodo 3 en el diagrama) pasan por la misma cantidad de nodos negros que antes, pero N ahora tiene un ancestro negro adicional: o P se ha vuelto negro, o era negro y S se agregó como abuelo negro. Por lo tanto, los caminos que pasan por N pasan por un nodo negro adicional, de modo que se restaura el requisito 4 y el árbol total tiene forma de RB.
Case_D6 : // D rojo y S negro: RotateDirRoot ( T , P , dir ); // P puede ser la raíz S -> color = P -> color ; P -> color = BLACK ; D -> color = BLACK ; return ; // eliminación completa } // fin de RBdelete2
Debido a que el algoritmo transforma la entrada sin utilizar una estructura de datos auxiliar y utilizando solo una pequeña cantidad de espacio de almacenamiento adicional para variables auxiliares, se encuentra en el lugar .
Porque hay un árbol rojo-negro de altura con
nodos ( es la función de piso ) y no hay ningún árbol rojo-negro de esta altura de árbol con menos nodos, por lo tanto, es mínimo . Su altura negra es (con raíz negra) o para impar (entonces con una raíz roja) también
Para que un árbol rojo-negro de una cierta altura tenga un número mínimo de nodos, debe tener exactamente un camino más largo con un número máximo de nodos rojos, para lograr una altura de árbol máxima con una altura negra mínima. Además de este camino, todos los demás nodos deben ser negros. [15] : 444 Boceto de prueba Si se quita un nodo de este árbol, pierde altura o alguna propiedad RB.
El árbol RB de altura con raíz roja es mínimo. Esto concuerda con
Un árbol RB mínimo (RB h en la figura 4) de altura tiene una raíz cuyos dos subárboles hijos tienen diferentes alturas. El subárbol hijo superior también es un árbol RB mínimo, RB h –1 , que contiene también un camino más largo que define su altura ; tiene nodos y la altura negra El otro subárbol es un árbol binario perfecto de altura (negra) que tiene nodos negros y ningún nodo rojo. Entonces, el número de nodos es por inducción
La gráfica de la función es convexa y lineal por partes con puntos de corte en donde La función ha sido tabulada como A027383( h –1) para (secuencia A027383 en la OEIS ).
La desigualdad conduce a , lo que para impar conduce a
Así que en ambos casos, el par y el impar, está en el intervalo
siendo el número de nodos. [34]
Un árbol rojo-negro con nodos (claves) tiene una altura de árbol
Además de las operaciones de inserción, eliminación y búsqueda de un solo elemento, se han definido varias operaciones de conjunto en árboles rojo-negros: unión , intersección y diferencia de conjuntos . Luego, se pueden implementar operaciones rápidas en masa sobre inserciones o eliminaciones basadas en estas funciones de conjunto. Estas operaciones de conjunto se basan en dos operaciones auxiliares, Split y Join . Con las nuevas operaciones, la implementación de árboles rojo-negros puede ser más eficiente y altamente paralelizable. [35] Para lograr sus complejidades de tiempo, esta implementación requiere que se permita que la raíz sea roja o negra, y que cada nodo almacene su propia altura negra .
El algoritmo de unión es el siguiente:
función joinRightRB(T L , k, T R ): si (T L .color=negro) y (T L .blackHeight=T R .blackHeight): devuelve Nodo(T L ,⟨k,rojo⟩,T R ) T'=Nodo(T L .izquierda,⟨T L .clave,T L .color⟩,unirseDerechaRB(T L .derecha,k,T R )) si (T L .color=negro) y (T'.derecha.color=T'.derecha.derecha.color=rojo): T'.right.right.color=negro; devolver rotarIzquierda(T') devolver T' /* T ' '[T recta'] */función joinLeftRB(T L , k, T R ): /* simétrico para unirse a RightRB */función join(T L , k, T R ): si T L .blackHeight>T R .blackHeight: T'=joinRightRB(T L ,k,T R ) si (T'.color=rojo) y (T'.right.color=rojo): T'.color=negro devuelve T' si T R .blackHeight>T L .blackHeight: /* simétrico */ si (T L .color=negro) y (T R .color=negro): devuelve Nodo(T L ,⟨k,rojo⟩,T R ) devuelve Nodo(T L ,⟨k,negro⟩,T R )
El algoritmo de división es el siguiente:
función split(T, k): si (T = nulo) devuelve (nulo, falso, nulo) si (k = T.clave) devuelve (T.izquierda, verdadero, T.derecha) si (k < T.clave): (L',b,R') = dividir(T.izquierda, k) devolver (L',b,unir(R',T.tecla,T.derecha)) (L',b,R') = dividir(T.derecha, k) regresar (unir(T.izquierda,T.tecla,L'),b,T.derecha)
La unión de dos árboles rojo-negros t 1 y t 2 que representan los conjuntos A y B , es un árbol rojo-negro t que representa A ∪ B . La siguiente función recursiva calcula esta unión:
función unión(t 1 , t 2 ): si t 1 = nulo devuelve t 2 si t 2 = nulo devuelve t 1 (L 1 ,b,R 1 )=split(t 1 ,t 2 .key) proc1= inicio : T L = unión (L 1 ,t 2 .izquierda) proc2= inicio : T R = unión(R 1 ,t 2 .derecha) esperar todo proc1,proc2 devolver unión(T L , t 2 .clave, T R )
Aquí, se supone que split devuelve dos árboles: uno que contiene las claves menos su clave de entrada, y otro que contiene las claves mayores. (El algoritmo no es destructivo , pero también existe una versión destructiva en el lugar).
El algoritmo para la intersección o diferencia es similar, pero requiere la rutina auxiliar Join2 que es la misma que Join pero sin la clave intermedia. Con base en las nuevas funciones para la unión, intersección o diferencia, se puede insertar o eliminar una o varias claves en el árbol rojo-negro. Dado que Split llama a Join pero no se ocupa directamente de los criterios de equilibrio de los árboles rojo-negros, una implementación de este tipo se suele denominar implementación "basada en join" .
La complejidad de cada unión, intersección y diferencia es para dos árboles rojo-negros de tamaños y . Esta complejidad es óptima en términos de la cantidad de comparaciones. Más importante aún, dado que las llamadas recursivas a unión, intersección o diferencia son independientes entre sí, se pueden ejecutar en paralelo con una profundidad paralela . [35] Cuando , la implementación basada en unión tiene el mismo gráfico acíclico dirigido (DAG) computacional que la inserción y eliminación de un solo elemento si se usa la raíz del árbol más grande para dividir el árbol más pequeño.
Los algoritmos paralelos para construir árboles rojo-negros a partir de listas ordenadas de elementos pueden ejecutarse en tiempo constante o en tiempo, dependiendo del modelo de computadora, si la cantidad de procesadores disponibles es asintóticamente proporcional a la cantidad de elementos donde . También se conocen algoritmos paralelos de búsqueda, inserción y eliminación rápida. [36]
Los algoritmos basados en unión para árboles rojo-negros son paralelos para operaciones en masa, incluidas unión, intersección, construcción, filtro, mapa-reducción, etc.
Las operaciones básicas como inserción, eliminación o actualización se pueden paralelizar definiendo operaciones que procesen lotes de múltiples elementos. También es posible procesar lotes con varias operaciones básicas; por ejemplo, los lotes pueden contener elementos para insertar y también elementos para eliminar del árbol.
Los algoritmos para operaciones masivas no solo son aplicables al árbol rojo-negro, sino que también se pueden adaptar a otras estructuras de datos de secuencias ordenadas, como el árbol 2-3 , el árbol 2-3-4 y el árbol (a,b) . A continuación, se explicarán diferentes algoritmos para la inserción masiva, pero los mismos algoritmos también se pueden aplicar a la eliminación y actualización. La inserción masiva es una operación que inserta cada elemento de una secuencia en un árbol .
Este enfoque se puede aplicar a cualquier estructura de datos de secuencia ordenada que admita operaciones de unión y división eficientes. [37] La idea general es dividir en múltiples partes y realizar las inserciones en estas partes en paralelo.
Tenga en cuenta que en el Paso 3 las restricciones para la división aseguran que en el Paso 5 los árboles puedan unirse nuevamente y se ordene la secuencia resultante.
El pseudocódigo muestra una implementación simple de divide y vencerás del algoritmo basado en join para la inserción masiva. Ambas llamadas recursivas se pueden ejecutar en paralelo. La operación de join que se utiliza aquí difiere de la versión explicada en este artículo; en su lugar, se utiliza join2 , que omite el segundo parámetro k.
inserción masiva (T, I, k): yo.ordenar() InsertarRec en masa (T, I, k)bulkInsertRec (T, I, k): si k = 1: para todo e en I: T.insert(e) de lo contrario m := ⌊tamaño(I) / 2⌋ (T 1 , _, T 2 ) := dividir(T, I[m]) InsertarRec en masa (T 1 , I[0 .. m], ⌈k / 2⌉) || bulkInsertRec(T 2 , I[m + 1 .. tamaño(I) - 1], ⌊k / 2⌋) T ← unir2(T 1 , T 2 )
La clasificación no se considera en este análisis.
Esto se puede mejorar utilizando algoritmos paralelos para dividir y unir. En este caso, el tiempo de ejecución es . [38]
Otro método para paralelizar operaciones en bloque es utilizar un enfoque de segmentación . [39] Esto se puede hacer dividiendo la tarea de procesar una operación básica en una secuencia de subtareas. Para múltiples operaciones básicas, las subtareas se pueden procesar en paralelo asignando cada subtarea a un procesador independiente.
En este análisis no se tiene en cuenta la ordenación . Además, se supone que es menor que , de lo contrario sería más eficiente construir el árbol resultante desde cero.
Mucha gente pregunta por qué usamos el nombre rojo–negro. Bueno, inventamos esta estructura de datos, esta forma de ver los árboles balanceados, en Xerox PARC, que fue el hogar de la computadora personal y muchas otras innovaciones con las que vivimos hoy en día, introduciendo interfaces gráficas de usuario, Ethernet y programación orientada a objetos y muchas otras cosas. Pero una de las cosas que se inventó allí fue la impresión láser y estábamos muy emocionados de tener cerca una impresora láser a color que pudiera imprimir cosas en color y, de los colores, el rojo se veía mejor. Entonces, es por eso que elegimos el color rojo para distinguir los enlaces rojos, los tipos de enlaces, en tres nodos. Entonces, esa es una respuesta a la pregunta para las personas que han estado preguntando.
Nuestro algoritmo paralelo para construir un árbol rojo-negro a partir de una lista ordenada de
elementos se ejecuta en
tiempo con
procesadores en la PRAM CRCW y se ejecuta en
tiempo con
procesadores en la PRAM EREW.