Información de la Tarea
Estudiante: Andrés Cruz Chipol
Curso: Análisis y Diseño de Algoritmos
Fecha de entrega: Martes 25 de noviembre, 2025
Descripción de la Tarea
Hacer el pseudocódigo y el código en Python para crear un árbol de búsqueda binario rojo y negro. Con tres entradas aleatorias crear el árbol rojo y negro insertando nodos y visualizar los tres árboles resultantes. Borrar nodos de forma aleatoria y visualizar el resultado para mostrar que se sigue manteniendo el árbol rojo y negro.
Árbol Rojo-Negro
Introducción
Un árbol rojo-negro es un árbol de búsqueda binario auto-balanceado donde cada nodo tiene un bit extra de almacenamiento: su color, que puede ser ROJO o NEGRO. Al restringir la forma en que los nodos se colorean en cualquier camino desde la raíz hasta una hoja, los árboles rojo-negro garantizan que ningún camino sea más del doble de largo que cualquier otro, de modo que el árbol está aproximadamente balanceado.
Propiedades del Árbol Rojo-Negro
Un árbol de búsqueda binario es un árbol rojo-negro si satisface las siguientes propiedades rojo-negro:
- Cada nodo es rojo o negro.
- La raíz es negra.
- Cada hoja (NIL) es negra.
- Si un nodo es rojo, entonces ambos hijos son negros. (No puede haber dos nodos rojos consecutivos)
- Para cada nodo, todos los caminos simples desde el nodo hasta las hojas descendientes contienen el mismo número de nodos negros. (Altura negra)
Estas propiedades garantizan que la altura del árbol sea O(log n), donde n es el número de nodos.
Operaciones del Árbol Rojo-Negro
Las operaciones básicas como buscar, mínimo, máximo, sucesor y predecesor son las mismas que en un Árbol de Búsqueda Binaria (BST).
Las operaciones adicionales necesarias para crear y mantener un árbol rojo-negro son:
- rotar_izquierda: Rotación hacia la izquierda para rebalancear
- rotar_derecha: Rotación hacia la derecha para rebalancear
- insertar: Inserta un nuevo nodo y rebalancea el árbol
- insertar_fixup: Corrige las violaciones de propiedades después de insertar
- borrar: Elimina un nodo y rebalancea el árbol
- borrar_fixup: Corrige las violaciones de propiedades después de borrar
Todas las operaciones se realizan en un tiempo O(log n).
Pseudocódigo
NOTA: Las operaciones básicas como BUSCAR, MÍNIMO, MÁXIMO, SUCESOR y PREDECESOR son las mismas que en un Árbol de Búsqueda Binaria (BST). Las siguientes son las operaciones adicionales necesarias para crear y mantener un Árbol Rojo-Negro.
Rotación Izquierda
LEFT-ROTATE(T, x)1 y = x.right // y es el hijo derecho de x2 x.right = y.left // el subárbol izq de y pasa a ser subárbol der de x3 if y.left ≠ T.nil4 y.left.p = x5 y.p = x.p // el padre de x ahora es padre de y6 if x.p == T.nil7 T.root = y8 elseif x == x.p.left9 x.p.left = y10 else x.p.right = y11 y.left = x // x pasa a ser hijo izquierdo de y12 x.p = yRotación Derecha
RIGHT-ROTATE(T, x)1 y = x.left // y es el hijo izquierdo de x2 x.left = y.right // el subárbol der de y pasa a ser subárbol izq de x3 if y.right ≠ T.nil4 y.right.p = x5 y.p = x.p // el padre de x ahora es padre de y6 if x.p == T.nil7 T.root = y8 elseif x == x.p.right9 x.p.right = y10 else x.p.left = y11 y.right = x // x pasa a ser hijo derecho de y12 x.p = yInsertar
RB-INSERT(T, z)1 y = T.nil2 x = T.root3 while x ≠ T.nil4 y = x5 if z.key < x.key6 x = x.left7 else x = x.right8 z.p = y9 if y == T.nil10 T.root = z11 elseif z.key < y.key12 y.left = z13 else y.right = z14 z.left = T.nil15 z.right = T.nil16 z.color = RED17 RB-INSERT-FIXUP(T, z)Insertar Fixup
RB-INSERT-FIXUP(T, z)1 while z.p.color == RED2 if z.p == z.p.p.left3 y = z.p.p.right // y es el tío de z4 if y.color == RED // Caso 15 z.p.color = BLACK6 y.color = BLACK7 z.p.p.color = RED8 z = z.p.p9 else if z == z.p.right // Caso 210 z = z.p11 LEFT-ROTATE(T, z)12 z.p.color = BLACK // Caso 313 z.p.p.color = RED14 RIGHT-ROTATE(T, z.p.p)15 else (igual que arriba con "right" y "left" intercambiados)16 T.root.color = BLACKTransplantar
RB-TRANSPLANT(T, u, v)1 if u.p == T.nil2 T.root = v3 elseif u == u.p.left4 u.p.left = v5 else u.p.right = v6 v.p = u.pBorrar
RB-DELETE(T, z)1 y = z2 y-original-color = y.color3 if z.left == T.nil4 x = z.right5 RB-TRANSPLANT(T, z, z.right)6 elseif z.right == T.nil7 x = z.left8 RB-TRANSPLANT(T, z, z.left)9 else y = TREE-MINIMUM(z.right)10 y-original-color = y.color11 x = y.right12 if y.p == z13 x.p = y14 else RB-TRANSPLANT(T, y, y.right)15 y.right = z.right16 y.right.p = y17 RB-TRANSPLANT(T, z, y)18 y.left = z.left19 y.left.p = y20 y.color = z.color21 if y-original-color == BLACK22 RB-DELETE-FIXUP(T, x)Borrar Fixup
RB-DELETE-FIXUP(T, x)1 while x ≠ T.root and x.color == BLACK2 if x == x.p.left3 w = x.p.right // w es hermano de x4 if w.color == RED // Caso 15 w.color = BLACK6 x.p.color = RED7 LEFT-ROTATE(T, x.p)8 w = x.p.right9 if w.left.color == BLACK and w.right.color == BLACK // Caso 210 w.color = RED11 x = x.p12 else if w.right.color == BLACK // Caso 313 w.left.color = BLACK14 w.color = RED15 RIGHT-ROTATE(T, w)16 w = x.p.right17 w.color = x.p.color // Caso 418 x.p.color = BLACK19 w.right.color = BLACK20 LEFT-ROTATE(T, x.p)21 x = T.root22 else (igual que arriba con "right" y "left" intercambiados)23 x.color = BLACKImplementación en Python
ROJO = 0NEGRO = 1
class NodoRN: def __init__(self, key): self.key = key self.izq = None self.der = None self.p = None self.color = ROJO
class ArbolRojoNegro: """ Implementación de Árbol Rojo-Negro basada en el libro de Cormen (CLRS).
Propiedades del Árbol Rojo-Negro: 1. Cada nodo es rojo o negro. 2. La raíz es negra. 3. Cada hoja (NIL) es negra. 4. Si un nodo es rojo, entonces ambos hijos son negros. 5. Para cada nodo, todos los caminos desde el nodo hasta las hojas descendientes contienen el mismo número de nodos negros. """
def __init__(self): # Crear el nodo centinela NIL self.NIL = NodoRN(None) self.NIL.color = NEGRO self.NIL.izq = None self.NIL.der = None self.NIL.p = None self.raiz = self.NIL
def rotar_izquierda(self, x): y = x.der x.der = y.izq
if y.izq != self.NIL: y.izq.p = x
y.p = x.p
if x.p == self.NIL: self.raiz = y elif x == x.p.izq: x.p.izq = y else: x.p.der = y
y.izq = x x.p = y
def rotar_derecha(self, x): y = x.izq x.izq = y.der
if y.der != self.NIL: y.der.p = x
y.p = x.p
if x.p == self.NIL: self.raiz = y elif x == x.p.der: x.p.der = y else: x.p.izq = y
y.der = x x.p = y
def insertar(self, key): z = NodoRN(key) z.izq = self.NIL z.der = self.NIL
y = self.NIL x = self.raiz
while x != self.NIL: y = x if z.key < x.key: x = x.izq else: x = x.der
z.p = y
if y == self.NIL: self.raiz = z elif z.key < y.key: y.izq = z else: y.der = z
z.color = ROJO self._insertar_fixup(z)
def _insertar_fixup(self, z): while z.p.color == ROJO: if z.p == z.p.p.izq: y = z.p.p.der # tío
if y.color == ROJO: # Caso 1: el tío es rojo z.p.color = NEGRO y.color = NEGRO z.p.p.color = ROJO z = z.p.p else: if z == z.p.der: # Caso 2: z es hijo derecho z = z.p self.rotar_izquierda(z) # Caso 3: z es hijo izquierdo z.p.color = NEGRO z.p.p.color = ROJO self.rotar_derecha(z.p.p) else: y = z.p.p.izq # tío
if y.color == ROJO: # Caso 1: el tío es rojo z.p.color = NEGRO y.color = NEGRO z.p.p.color = ROJO z = z.p.p else: if z == z.p.izq: # Caso 2: z es hijo izquierdo z = z.p self.rotar_derecha(z) # Caso 3: z es hijo derecho z.p.color = NEGRO z.p.p.color = ROJO self.rotar_izquierda(z.p.p)
self.raiz.color = NEGRO
def _transplantar(self, u, v): if u.p == self.NIL: self.raiz = v elif u == u.p.izq: u.p.izq = v else: u.p.der = v v.p = u.p
def borrar(self, z): y = z y_color_original = y.color
if z.izq == self.NIL: x = z.der self._transplantar(z, z.der) elif z.der == self.NIL: x = z.izq self._transplantar(z, z.izq) else: y = self.minimo(z.der) y_color_original = y.color x = y.der
if y.p == z: x.p = y else: self._transplantar(y, y.der) y.der = z.der y.der.p = y
self._transplantar(z, y) y.izq = z.izq y.izq.p = y y.color = z.color
if y_color_original == NEGRO: self._borrar_fixup(x)
def _borrar_fixup(self, x): while x != self.raiz and x.color == NEGRO: if x == x.p.izq: w = x.p.der # hermano
if w.color == ROJO: # Caso 1 w.color = NEGRO x.p.color = ROJO self.rotar_izquierda(x.p) w = x.p.der
if w.izq.color == NEGRO and w.der.color == NEGRO: # Caso 2 w.color = ROJO x = x.p else: if w.der.color == NEGRO: # Caso 3 w.izq.color = NEGRO w.color = ROJO self.rotar_derecha(w) w = x.p.der # Caso 4 w.color = x.p.color x.p.color = NEGRO w.der.color = NEGRO self.rotar_izquierda(x.p) x = self.raiz else: w = x.p.izq # hermano
if w.color == ROJO: # Caso 1 w.color = NEGRO x.p.color = ROJO self.rotar_derecha(x.p) w = x.p.izq
if w.der.color == NEGRO and w.izq.color == NEGRO: # Caso 2 w.color = ROJO x = x.p else: if w.izq.color == NEGRO: # Caso 3 w.der.color = NEGRO w.color = ROJO self.rotar_izquierda(w) w = x.p.izq # Caso 4 w.color = x.p.color x.p.color = NEGRO w.izq.color = NEGRO self.rotar_derecha(x.p) x = self.raiz
x.color = NEGRO
def borrar_por_llave(self, k): nodo = self.buscar_nodo(k) if nodo != self.NIL: self.borrar(nodo) return True return False
def buscar(self, x, k): if x == self.NIL or k == x.key: return x if k < x.key: return self.buscar(x.izq, k) else: return self.buscar(x.der, k)
def buscar_nodo(self, k): return self.buscar(self.raiz, k)
def minimo(self, x=None): if x is None: x = self.raiz while x.izq != self.NIL: x = x.izq return x
def maximo(self, x=None): if x is None: x = self.raiz while x.der != self.NIL: x = x.der return x
def sucesor(self, x): if x.der != self.NIL: return self.minimo(x.der) y = x.p while y != self.NIL and x == y.der: x = y y = y.p return y
def predecesor(self, x): if x.izq != self.NIL: return self.maximo(x.izq) y = x.p while y != self.NIL and x == y.izq: x = y y = y.p return y
def recorrido_inorder(self, x): if x != self.NIL: self.recorrido_inorder(x.izq) color = "R" if x.color == ROJO else "N" print(f"{x.key}({color})", end=' ') self.recorrido_inorder(x.der)
def imprimir_inorder(self): self.recorrido_inorder(self.raiz) print()
def crear_desde_lista(self, lista_llaves): for llave in lista_llaves: self.insertar(llave) return self
def crear_arbol_desde_lista(lista_llaves): arbol = ArbolRojoNegro() arbol.crear_desde_lista(lista_llaves) return arbolDemostración con Entradas Aleatorias
Se crearon tres árboles rojo-negro con entradas aleatorias de 8 elementos cada uno. Posteriormente, se borró un nodo aleatorio de cada árbol para demostrar que las propiedades del árbol rojo-negro se mantienen.
import randomfrom arbol_rojo_negro import ArbolRojoNegro
def main(): print("DEMOSTRACIÓN DE ÁRBOL ROJO-NEGRO")
# Generar las entradas aleatorias entradas = [ random.sample(range(1, 50), 8), random.sample(range(1, 50), 8), random.sample(range(1, 50), 8) ]
print("\nPASO 1: INSERCIÓN CON ENTRADAS ALEATORIAS")
arboles = [] for i, entrada in enumerate(entradas, 1): print(f"\nÁrbol {i} - Entrada: {entrada}") arbol = ArbolRojoNegro() for valor in entrada: arbol.insertar(valor)
arbol.generar_imagen(f"arbol_{i}_insercion", f"Árbol {i} - Inserción: {entrada}") arboles.append((arbol, entrada))
print("\nPASO 2: BORRADO ALEATORIO")
for i, (arbol, entrada) in enumerate(arboles, 1): nodo_a_borrar = random.choice(entrada) print(f"\nÁrbol {i} - Borrando: {nodo_a_borrar}")
arbol.borrar_por_llave(nodo_a_borrar)
nodos_restantes = sorted([n for n in entrada if n != nodo_a_borrar])
arbol.generar_imagen(f"arbol_{i}_borrado", f"Árbol {i} - Después de borrar {nodo_a_borrar}") print(f"Nodos restantes: {nodos_restantes}")
if __name__ == "__main__": main()Resultados
Ejecución del programa
Al ejecutar el código de demostración con entradas aleatorias, se obtuvieron los siguientes resultados:
DEMOSTRACIÓN DE ÁRBOL ROJO-NEGRO
PASO 1: INSERCIÓN CON ENTRADAS ALEATORIAS
Árbol 1 - Entrada: [16, 27, 21, 32, 34, 22, 28, 7]Imagen: arbol_1_insercion.png
Árbol 2 - Entrada: [8, 40, 30, 29, 31, 33, 13, 16]Imagen: arbol_2_insercion.png
Árbol 3 - Entrada: [40, 19, 31, 28, 23, 35, 8, 14]Imagen: arbol_3_insercion.png
PASO 2: BORRADO ALEATORIO
Árbol 1 - Borrando: 22Imagen: arbol_1_borrado.pngNodos restantes: [7, 16, 21, 27, 28, 32, 34]
Árbol 2 - Borrando: 33Imagen: arbol_2_borrado.pngNodos restantes: [8, 13, 16, 29, 30, 31, 40]
Árbol 3 - Borrando: 23Imagen: arbol_3_borrado.pngNodos restantes: [8, 14, 19, 28, 31, 35, 40]Visualización de los Árboles
Árbol 1
Después de inserción con entrada [16, 27, 21, 32, 34, 22, 28, 7]:

Después de borrar el nodo 22:

Árbol 2
Después de inserción con entrada [8, 40, 30, 29, 31, 33, 13, 16]:

Después de borrar el nodo 33:

Árbol 3
Después de inserción con entrada [40, 19, 31, 28, 23, 35, 8, 14]:

Después de borrar el nodo 23:

Análisis de las Visualizaciones
En las imágenes generadas se puede observar cómo los nodos rojos aparecen en color rojo y los nodos negros en color negro, mientras que los nodos centinela NIL se representan como rectángulos grises. Después de cada operación de inserción y borrado, el árbol mantiene correctamente las cinco propiedades rojo-negro gracias a los procedimientos de fixup que realizan las rotaciones y cambios de color necesarios para preservar el balance de la estructura.
Conclusión
El desarrollo de esta tarea permitió comprender de manera práctica el funcionamiento de los árboles rojo-negro y su importancia como estructura de datos auto-balanceada. Aunque los procedimientos de fixup presentan múltiples casos a considerar, la lógica subyacente se centra en preservar las cinco propiedades fundamentales del árbol mediante rotaciones y cambios de color. La implementación en Python, junto con la generación de visualizaciones gráficas, facilitó la verificación del correcto funcionamiento de las operaciones de inserción y borrado, demostrando que el árbol mantiene su balance después de cada modificación.
Referencias
Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to algorithms (3ra ed.). MIT Press. Capítulo 13: Red-Black Trees.