Saltar al contenido principal

Árboles autobalanceados AVL

Un Árbol AVL es un tipo de árbol binario de búsqueda autobalanceado, propuesto en 1962 por G. M. Adelson-Velsky y E. M. Landis. Su característica principal es que mantiene el equilibrio de alturas entre los subárboles izquierdo y derecho de cada nodo, lo que garantiza una complejidad de búsqueda, inserción y eliminación en tiempo O(logn)O(log n).

¿Qué es el equilibrio?

Para cada nodo, se define un factor de balance como:

balanceFactor=altura(izquierdo)altura(derecho)balanceFactor = altura(izquierdo) - altura(derecho)

El árbol está balanceado si balanceFactor ∈ {-1, 0, 1} para todos los nodos.

Razonamiento

Si un árbol binario de búsqueda se desbalancea (por ejemplo, por muchas inserciones ordenadas), se vuelve una estructura lineal y pierde eficiencia.

Los árboles AVL realizan rotaciones para mantener el equilibrio.

Rotaciones

Los árboles AVL se autobalancean después de cada operación, y para hacerlo, utilizan rotaciones que reorganizan la estructura del árbol sin perder la propiedad del árbol binario de búsqueda (BST).

Cuando un nodo se desbalancea, es porque su balanceFactor está fuera del rango [-1, 0, 1]. Para corregirlo, se identifican cuatro casos posibles:

  • Rotación simple izquierda-izquierda (LL Rotation)
  • Rotación simple derecha-derecha (RR Rotation)
  • Rotación doble izquierda-derecha (LR Rotation)
  • Rotación doble derecha-izquierda (RL Rotation)

El nombre de la rotación describe la causa del desequilibrio en un árbol AVL, no el tipo de rotación que se realiza.

Rotación simple Izquierda-Izquierda (LL Rotation)

Ocurre cuando se inserta un nodo en el subárbol izquierdo del hijo izquierdo del nodo desbalanceado. Por ejemplo:

Antes de la rotación (desbalance a la izquierda):

30
/
20
/
10

Después de la rotación:

20
/ \
10 30

Para aplicar la rotación se realiza la siguiente operación:

  • Se eleva el hijo izquierdo.
  • El nodo desbalanceado pasa a ser hijo derecho del nuevo nodo raíz.
  1. El usuario inserta un valor que causa un desbalance hacia la izquierda. (Ejemplo: insertar 30, 20, 10 en ese orden).
  2. El árbol detecta que balanceFactor > 1 y que el valor se insertó en el subárbol izquierdo del hijo izquierdo (LL).
  3. Se ejecuta una rotación simple a la derecha (rotateRight):
    • El hijo izquierdo (X) se convierte en la nueva raíz del subárbol.
    • El nodo desbalanceado (Y) pasa a ser hijo derecho de X.
    • El subárbol derecho de X (T2) se reasigna como hijo izquierdo de Y.
  4. Se actualizan las alturas y el subárbol queda balanceado nuevamente.
Antes:
Y(30)
/
X(20)
/
Z(10)

Después de rotación:
X(20)
/ \
Z(10) Y(30)

Rotación simple Derecha-Derecha (RR Rotation)

Ocurre cuando se inserta un nodo en el subárbol derecho del hijo derecho del nodo desbalanceado. Por ejemplo:

Antes:

10
\
20
\
30

Después:

20
/ \
10 30

Para aplicar la rotación se realiza la siguiente operación:

  • Se eleva el hijo derecho
  • El nodo desbalanceado se convierte en hijo izquierdo del nuevo nodo raíz.
  1. El usuario inserta un valor en el subárbol derecho del hijo derecho, provocando un desbalance hacia la derecha.
  2. El balanceFactor del nodo (Y) es menor que -1, y se detecta un patrón RR.
  3. Se ejecuta rotateLeft(Y):
    • El hijo derecho (X) se eleva como nueva raíz del subárbol.
    • El nodo desbalanceado (Y) se convierte en hijo izquierdo de X.
    • El subárbol izquierdo de X (T2) se reasigna como hijo derecho de Y.
  4. Se actualizan las alturas y se retorna el subárbol balanceado.
Antes:
Y(10)
\
X(20)
\
Z(30)

Después de rotación:
X(20)
/ \
Y(10) Z(30)

Rotation doble Izquierda-Derecha (LR Rotation)

Ocurre cuando se inserta un nodo en el subárbol derecho del hijo izquierdo del nodo desbalanceado. Por ejemplo:

Antes:

30
/
10
\
20

Después (rotación doble):

20
/ \
10 30

Para aplicar la rotación se realiza la siguiente operación:

  • Rotación simple a la izquierda sobre el hijo izquierdo (10 → 20).
  • Luego, rotación simple a la derecha sobre el nodo desbalanceado (30 → 20).
  1. Se inserta un nodo (Z) que va al subárbol derecho del hijo izquierdo (X) de un nodo (Y) desbalanceado.
  2. Se detecta que es un caso LR.
  3. Primero, se hace una rotación izquierda sobre XZ se convierte en nuevo subárbol izquierdo de Y.
  4. Luego, se hace una rotación derecha sobre YZ se convierte en raíz del subárbol balanceado.
Antes de la rotación:

Y(30)
/
X(10)
\
Z(20)

Después de rotación doble:

Z(20)
/ \
X(10) Y(30)

Rotación doble Derecha-Izquierda (RL Rotation)

Ocurre cuando se inserta un nodo en el subárbol izquierdo del hijo derecho del nodo desbalanceado. Por ejemplo:

Antes:

10
\
30
/
20

Después:

20
/ \
10 30

Para aplicar la rotación se realiza la siguiente operación:

  • Rotación simple a la derecha sobre el hijo derecho (30 → 20).
  • Luego, rotación simple a la izquierda sobre el nodo desbalanceado (10 → 20).
  1. Se inserta un nodo (Z) que cae en el subárbol izquierdo del hijo derecho (X) de un nodo (Y) desbalanceado.
  2. Se detecta el patrón RL.
  3. Primero se hace una rotación derecha sobre XZ sube.
  4. Luego se hace una rotación izquierda sobre YZ se convierte en la nueva raíz del subárbol balanceado.
  5. Se actualizan las alturas.
Antes de la rotación:

Y(10)
\
X(30)
/
Z(20)

Después de rotación doble:

Z(20)
/ \
Y(10) X(30)

Reglas prácticas para detectar la rotación necesaria

Balance Factor del nodoDirección de inserciónTipo de Rotación
> +1subárbol izquierdoLL
> +1subárbol derechoLR
< -1subárbol derechoRR
< -1subárbol izquierdoRL

Ejemplo Técnico

AVLNode.java
package edu.usta.domain.avl;

public class AVLNode {
public int value;
public int height;
public AVLNode left;
public AVLNode right;

public AVLNode(int value) {
this.value = value;
this.height = 1;
}
}
AVLTree.java
package edu.usta.domain.avl;

public class AVLTree {

private AVLNode root;

public void insert(int value) {
root = insert(root, value);
}

public AVLNode getRoot() {
return root;
}

private AVLNode insert(AVLNode node, int value) {
if (node == null) return new AVLNode(value);

if (value < node.value)
node.left = insert(node.left, value);
else if (value > node.value)
node.right = insert(node.right, value);
else
return node; // No duplicates

updateHeight(node);
return balance(node);
}

private void updateHeight(AVLNode node) {
int leftHeight = height(node.left);
int rightHeight = height(node.right);
node.height = 1 + Math.max(leftHeight, rightHeight);
}

private int height(AVLNode node) {
return node != null ? node.height : 0;
}

private int getBalance(AVLNode node) {
return (node == null) ? 0 : height(node.left) - height(node.right);
}

private AVLNode balance(AVLNode node) {
int balance = getBalance(node);

// LL Case
if (balance > 1 && getBalance(node.left) >= 0)
return rotateRight(node);

// RR Case
if (balance < -1 && getBalance(node.right) <= 0)
return rotateLeft(node);

// LR Case
if (balance > 1 && getBalance(node.left) < 0) {
node.left = rotateLeft(node.left);
return rotateRight(node);
}

// RL Case
if (balance < -1 && getBalance(node.right) > 0) {
node.right = rotateRight(node.right);
return rotateLeft(node);
}

return node;
}

private AVLNode rotateLeft(AVLNode y) {
AVLNode x = y.right;
AVLNode T2 = x.left;

x.left = y;
y.right = T2;

updateHeight(y);
updateHeight(x);

return x;
}

private AVLNode rotateRight(AVLNode y) {
AVLNode x = y.left;
AVLNode T2 = x.right;

x.right = y;
y.left = T2;

updateHeight(y);
updateHeight(x);

return x;
}

// In-order traversal for testing
public String inOrder() {
StringBuilder sb = new StringBuilder();
inOrder(root, sb);
return sb.toString().trim();
}

private void inOrder(AVLNode node, StringBuilder sb) {
if (node != null) {
inOrder(node.left, sb);
sb.append(node.value).append(" ");
inOrder(node.right, sb);
}
}
}

Aplicaciones reales

  • Bases de datos (cuando no se usa un árbol B)
  • Indexación en estructuras de ficheros
  • Sistemas de archivos (por ejemplo, ext4 usa AVL internamente)
  • Compiladores (símbolos equilibrados)

Referencias