He estado implementando un paquete LLRB que debería poder funcionar en cualquiera de los dos modos, Bottom-Up 2-3 o Top-Down 2-3-4 described by Sedgewick (code - código mejorado, aunque solo se trata de 2- 3 árboles here, gracias a RS para el puntero).¿Qué rotación adicional se requiere para la eliminación de un árbol rojo negro Top-Down 2-3-4 de inclinación a la izquierda?
Sedgewick proporciona una descripción muy clara de las operaciones de árbol para el modo 2-3, aunque pasa mucho tiempo hablando del modo 2-3-4. También muestra cómo una simple alteración del orden de cambio de color durante la inserción puede alterar el comportamiento del árbol (dividir en el camino hacia abajo para 2-3-4 o dividir en el camino hacia arriba para 2-3):
private Node insert(Node h, Key key, Value value)
{
if (h == null)
return new Node(key, value);
// Include this for 2-3-4 trees
if (isRed(h.left) && isRed(h.right)) colorFlip(h);
int cmp = key.compareTo(h.key);
if (cmp == 0) h.val = value;
else if (cmp < 0) h.left = insert(h.left, key, value);
else h.right = insert(h.right, key, value);
if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);
if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
// Include this for 2-3 trees
if (isRed(h.left) && isRed(h.right)) colorFlip(h);
return h;
}
Sin embargo, se pasa por alto en la eliminación 2-3-4 LLRBs con lo siguiente:
el código de la página siguiente es una implementación completa de delete() para LLRB 2-3 árboles. Se basa en el reverso del enfoque utilizado para insertar en árboles de arriba a abajo 2-3-4: realizamos rotaciones y volteos de color en el camino de búsqueda para garantizar que la búsqueda no finalice en un nodo 2, para que podamos simplemente eliminar el nodo en la parte inferior. Utilizamos el método fixUp() para compartir el código para el cambio de color y las rotaciones después de las llamadas recursivas en el código insert(). Con fixUp(), podemos dejar enlaces rojos de inclinación correcta y nodos desequilibrados a lo largo de la ruta de búsqueda, asegurándonos de que estas condiciones se solucionarán en el camino hacia arriba del árbol. (El enfoque también es eficaz 2-3-4 árboles, sino que requiere una rotación adicional cuando el nodo de la derecha de la ruta de búsqueda es un 4-nodo.)
Su función de borrado():
private Node delete(Node h, Key key)
{
if (key.compareTo(h.key) < 0)
{
if (!isRed(h.left) && !isRed(h.left.left))
h = moveRedLeft(h);
h.left = delete(h.left, key);
}
else
{
if (isRed(h.left))
h = rotateRight(h);
if (key.compareTo(h.key) == 0 && (h.right == null))
return null;
if (!isRed(h.right) && !isRed(h.right.left))
h = moveRedRight(h);
if (key.compareTo(h.key) == 0)
{
h.val = get(h.right, min(h.right).key);
h.key = min(h.right).key;
h.right = deleteMin(h.right);
}
else h.right = delete(h.right, key);
}
return fixUp(h);
}
Mi implementación mantiene correctamente invariantes LLRB 2-3 para todas las operaciones de árbol en 2-3 árboles, pero falla para una subclase de eliminaciones del lado derecho en 2-3-4 árboles (estas eliminaciones fallidas dan como resultado nodos rojos con inclinación hacia la derecha , pero desequilibrio bola de nieve a árbol y finalmente eliminación de referencias de puntero nulo). A partir de una encuesta del código de ejemplo que analiza árboles LLRB e incluye opciones para la construcción de árboles en cualquier modo, parece que ninguno implementa correctamente la eliminación de un LLRB 2-3-4 (es decir, ninguno tiene la rotación adicional aludida, por ejemplo, Sedgewick's java arriba y here).
Tengo problemas para entender exactamente lo que quiere decir con "rotación extra cuando el nodo derecho fuera de la ruta de búsqueda es un nodo 4"; presumiblemente este es un giro a la izquierda, pero ¿dónde y cuándo?
Si giro hacia la izquierda pasando más allá de un nodo de 4 nodos (es decir, nodo RR) o un nodo de 3 nodos con inclinación hacia la derecha (nodo BR) antes de llamar a fixUp() o al final de la función fixUp todavía me aparece la misma contradicción invariante.
Aquí están los estados del árbol de los ejemplos de fallas más pequeños que he encontrado (generados por la inserción secuencial de elementos desde 0 hasta el valor máximo respectivo).
El primer par de árboles muestra la transición del estado de conformidad invariante antes de la eliminación del elemento 15 al estado obviamente roto después.
El segundo es esencialmente el mismo que el anterior, pero con una deleción de 16 de 0 ..16 (eliminación de 15 resultados en la misma topología). Tenga en cuenta que la contradicción invariante se las arregla para cruzar el nodo raíz.
La clave va a ser la comprensión de cómo revertir las violaciónes generados durante el paseo por el árbol hasta el nodo de destino. Los dos árboles siguientes muestran cómo se ve el primer árbol de arriba después de una caminata hacia la izquierda y hacia la derecha respectivamente (sin eliminación y antes de volver a caminar con fixUp()).
Después intento de eliminar '-1' sin corrección:
Después intento de eliminar '16' sin corrección:
Tratando Girar a la izquierda en el camino de vuelta cuando el nodo tiene solamente un niño rojo derecho parece ser parte de la solución, pero no trata correctamente con dos niños rojos derechos consecutivos, precediendo esto con un flipColor cuando ambos niños son rojos parece mejorar la situación aún más, pero aún deja algunas invariantes violadas .
Si además verifico si el hijo correcto de un hijo derecho es rojo cuando su hermano es negro y gire a la izquierda si esto es cierto, solo fallaré una vez, pero en este momento siento que necesito una nueva teoría que un nuevo epiciclo
¿Alguna idea?
Como referencia, mi implementación está disponible here (No, no es Java).
Seguimiento:
Parte de la razón por la que estaba interesado en esta era confirmar la reclamación por muchos que LLRB 2-3 árboles son más eficientes que los árboles 2-3-4 LLRB. Mi evaluación comparativa ha confirmado esto para la inserción y la eliminación (2-3 son aproximadamente un 9% más rápido), pero creo que la recuperación es muy ligeramente más rápida para 2-3-4 árboles.
Los siguientes tiempos son representativos y consistentes a través de carreras:
BU23:
BenchmarkInsert 1000000 1546 ns/op
BenchmarkDelete 1000000 1974 ns/op
BenchmarkGet 5000000 770 ns/op
TD234:
BenchmarkInsert 1000000 1689 ns/op
BenchmarkDelete 1000000 2133 ns/op
BenchmarkGet 5000000 753 ns/op
La primera columna es el nombre de banco, segundo es el número de las operaciones, tercero es resultado. Benchmark en i5M 2.27.
He echado un vistazo a las longitudes de rama para 2-3 árboles y 2-3-4 árboles y hay poco que explicar la diferencia de recuperación (distancia media de raíz a nodo y SD de 1000 árboles cada uno con 10000 inserciones aleatorias):
Means:
TD234 leafs BU23 leafs
12.88940 12.84681
TD234 all BU23 all
11.79274 11.79163
StdDev:
TD234 leafs BU23 leafs
1.222458 1.257344
TD234 all BU23 all
1.874335 1.885204
"Esta pregunta muestra el esfuerzo de investigación". - hmm ... verifique ... – Mysticial
He encontrado varios errores en varias implementaciones de RBT que se pueden encontrar en línea (por ejemplo, en "Ordenar y buscar algoritmos" de Thomas Niemann, que es un texto introductorio + código C). Tristemente, no recuerdo todos los detalles, excepto el pseudocódigo de mala referencia ofrecido en el famoso libro "Introducción a los algoritmos" de Cormen/Leiserson/Rivest/Stein (también utilizado en el código de Niemann). Ver [esta respuesta] (http://stackoverflow.com/a/11328289/968261) para más detalles. –
Estoy de acuerdo, hay un montón de código malo/descuidado que describe esto. – kortschak