Árvore AVL

Implemente uma Árvore AVL em Rust com rotações simples e duplas, balanceamento automático e garantia de O(log n) para todas as operações. Inclui diagramas ASCII detalhados.

Introdução

A Árvore AVL (nomeada em homenagem a seus inventores Adelson-Velsky e Landis, 1962) é a primeira árvore de busca binária autobalanceada da história. Ela resolve o problema fundamental das BSTs comuns: a possibilidade de degeneração em uma lista encadeada quando os dados são inseridos em ordem.

A AVL garante que, para todo nó, a diferença de altura entre as subárvores esquerda e direita (chamada de fator de balanceamento) seja no máximo 1 em valor absoluto. Quando uma inserção ou remoção viola essa propriedade, rotações são aplicadas para restaurar o equilíbrio. Isso garante que a altura da árvore é sempre O(log n), tornando todas as operações eficientes.


Conceito e Teoria

Fator de Balanceamento

O fator de balanceamento de um nó é definido como:

FB(nó) = altura(subárvore esquerda) - altura(subárvore direita)

Uma árvore AVL exige que para todo nó: FB ∈ {-1, 0, 1}

    Árvore AVL válida:          Árvore NÃO AVL:
         [30] FB=1                   [30] FB=2  ← Violação!
        /    \                      /
      [20]   [40] FB=0           [20] FB=1
      /                          /
    [10] FB=0                  [10] FB=0

As Quatro Rotações

Quando o fator de balanceamento fica fora de {-1, 0, 1}, aplicamos rotações:

Rotação Simples à Direita (LL — Left-Left)

Desbalanceamento causado por inserção na subárvore esquerda do filho esquerdo:

    Antes (FB=2):           Depois (FB=0):
        [30]                    [20]
        /                      /    \
      [20]          →       [10]   [30]
      /
    [10]

    Rotação à direita no nó [30]

Rotação Simples à Esquerda (RR — Right-Right)

Desbalanceamento causado por inserção na subárvore direita do filho direito:

    Antes (FB=-2):          Depois (FB=0):
    [10]                        [20]
       \                       /    \
      [20]          →       [10]   [30]
         \
        [30]

    Rotação à esquerda no nó [10]

Rotação Dupla Esquerda-Direita (LR — Left-Right)

Desbalanceamento causado por inserção na subárvore direita do filho esquerdo:

    Antes (FB=2):           Passo 1:             Passo 2:
        [30]                  [30]                  [25]
        /                     /                    /    \
      [20]          →      [25]          →      [20]   [30]
         \                 /
        [25]             [20]

    1. Rotação à esquerda em [20]
    2. Rotação à direita em [30]

Rotação Dupla Direita-Esquerda (RL — Right-Left)

Desbalanceamento causado por inserção na subárvore esquerda do filho direito:

    Antes (FB=-2):          Passo 1:             Passo 2:
    [10]                    [10]                    [15]
       \                       \                   /    \
      [20]          →        [15]        →      [10]   [20]
      /                         \
    [15]                       [20]

    1. Rotação à direita em [20]
    2. Rotação à esquerda em [10]

Exemplo Completo de Inserções Sucessivas

Inserir: 50, 30, 70, 20, 40, 10

    Após 50, 30, 70:        Após 20:              Após 40:
        [50]                   [50]                   [50]
       /    \                 /    \                  /    \
     [30]  [70]            [30]   [70]             [30]   [70]
                           /                       /  \
                         [20]                    [20] [40]
     FB(todos) OK         FB(todos) OK            FB(todos) OK

    Após inserir 10:
        [50] FB=2 ← Desbalanceado!
       /    \
     [30]   [70]
     /  \
   [20] [40]
   /
 [10]

    Rotação à direita em [50]:
           [30]
          /    \
       [20]   [50]
       /      /   \
     [10]   [40]  [70]

     FB(todos) ∈ {-1, 0, 1}  ✓

Complexidade

OperaçãoMelhor CasoCaso MédioPior Caso
BuscaO(1)O(log n)O(log n)
InserçãoO(log n)O(log n)O(log n)
RemoçãoO(log n)O(log n)O(log n)
RotaçãoO(1)O(1)O(1)
EspaçoO(n)O(n)

Garantia fundamental: A altura de uma árvore AVL com n nós é sempre no máximo 1.44 * log2(n), garantindo O(log n) para todas as operações.


Implementação em Rust

use std::cmp::Ordering;
use std::fmt::Display;

/// Nó da árvore AVL com campo de altura para balanceamento eficiente
#[derive(Debug, Clone)]
struct No<T: Ord + Display + Clone> {
    valor: T,
    altura: i32,
    esquerda: ArvoreAVL<T>,
    direita: ArvoreAVL<T>,
}

/// Tipo da árvore AVL
type ArvoreAVL<T> = Option<Box<No<T>>>;

/// Retorna a altura de um nó (ou -1 se nulo)
fn altura<T: Ord + Display + Clone>(no: &ArvoreAVL<T>) -> i32 {
    match no {
        Some(n) => n.altura,
        None => -1,
    }
}

/// Calcula o fator de balanceamento de um nó
fn fator_balanceamento<T: Ord + Display + Clone>(no: &ArvoreAVL<T>) -> i32 {
    match no {
        Some(n) => altura(&n.esquerda) - altura(&n.direita),
        None => 0,
    }
}

/// Atualiza a altura de um nó com base nos filhos
fn atualizar_altura<T: Ord + Display + Clone>(no: &mut Box<No<T>>) {
    no.altura = 1 + altura(&no.esquerda).max(altura(&no.direita));
}

/// Rotação simples à direita (caso LL)
fn rotacao_direita<T: Ord + Display + Clone>(mut raiz: Box<No<T>>) -> Box<No<T>> {
    let mut novo_raiz = raiz.esquerda.take().expect("Filho esquerdo esperado");
    raiz.esquerda = novo_raiz.direita.take();
    atualizar_altura(&mut raiz);
    novo_raiz.direita = Some(raiz);
    atualizar_altura(&mut novo_raiz);
    novo_raiz
}

/// Rotação simples à esquerda (caso RR)
fn rotacao_esquerda<T: Ord + Display + Clone>(mut raiz: Box<No<T>>) -> Box<No<T>> {
    let mut novo_raiz = raiz.direita.take().expect("Filho direito esperado");
    raiz.direita = novo_raiz.esquerda.take();
    atualizar_altura(&mut raiz);
    novo_raiz.esquerda = Some(raiz);
    atualizar_altura(&mut novo_raiz);
    novo_raiz
}

/// Aplica o balanceamento necessário após inserção/remoção
fn balancear<T: Ord + Display + Clone>(mut no: Box<No<T>>) -> Box<No<T>> {
    atualizar_altura(&mut no);
    let fb = fator_balanceamento(&Some(no.clone()));

    // Caso LL: desbalanceado à esquerda, filho esquerdo pende à esquerda
    if fb > 1 && fator_balanceamento(&no.esquerda) >= 0 {
        return rotacao_direita(no);
    }

    // Caso LR: desbalanceado à esquerda, filho esquerdo pende à direita
    if fb > 1 && fator_balanceamento(&no.esquerda) < 0 {
        no.esquerda = Some(rotacao_esquerda(no.esquerda.take().unwrap()));
        return rotacao_direita(no);
    }

    // Caso RR: desbalanceado à direita, filho direito pende à direita
    if fb < -1 && fator_balanceamento(&no.direita) <= 0 {
        return rotacao_esquerda(no);
    }

    // Caso RL: desbalanceado à direita, filho direito pende à esquerda
    if fb < -1 && fator_balanceamento(&no.direita) > 0 {
        no.direita = Some(rotacao_direita(no.direita.take().unwrap()));
        return rotacao_esquerda(no);
    }

    no // Já está balanceado
}

/// Insere um valor na árvore AVL
fn inserir<T: Ord + Display + Clone>(raiz: ArvoreAVL<T>, valor: T) -> ArvoreAVL<T> {
    match raiz {
        None => Some(Box::new(No {
            valor,
            altura: 0,
            esquerda: None,
            direita: None,
        })),
        Some(mut no) => {
            match valor.cmp(&no.valor) {
                Ordering::Less => {
                    no.esquerda = inserir(no.esquerda.take(), valor);
                }
                Ordering::Greater => {
                    no.direita = inserir(no.direita.take(), valor);
                }
                Ordering::Equal => return Some(no), // Duplicado ignorado
            }
            Some(balancear(no))
        }
    }
}

/// Encontra e remove o menor valor de uma subárvore
fn extrair_minimo<T: Ord + Display + Clone>(
    mut no: Box<No<T>>,
) -> (T, ArvoreAVL<T>) {
    match no.esquerda.take() {
        None => {
            let valor_minimo = no.valor.clone();
            (valor_minimo, no.direita.take())
        }
        Some(esq) => {
            let (valor_minimo, nova_esq) = extrair_minimo(esq);
            no.esquerda = nova_esq;
            (valor_minimo, Some(balancear(no)))
        }
    }
}

/// Remove um valor da árvore AVL
fn remover<T: Ord + Display + Clone>(raiz: ArvoreAVL<T>, valor: &T) -> ArvoreAVL<T> {
    match raiz {
        None => None,
        Some(mut no) => {
            match valor.cmp(&no.valor) {
                Ordering::Less => {
                    no.esquerda = remover(no.esquerda.take(), valor);
                    Some(balancear(no))
                }
                Ordering::Greater => {
                    no.direita = remover(no.direita.take(), valor);
                    Some(balancear(no))
                }
                Ordering::Equal => {
                    // Nó encontrado — tratar os três casos de remoção
                    match (no.esquerda.take(), no.direita.take()) {
                        (None, None) => None,
                        (Some(esq), None) => Some(esq),
                        (None, Some(dir)) => Some(dir),
                        (Some(esq), Some(dir)) => {
                            let (sucessor, nova_dir) = extrair_minimo(dir);
                            no.valor = sucessor;
                            no.esquerda = Some(esq);
                            no.direita = nova_dir;
                            Some(balancear(no))
                        }
                    }
                }
            }
        }
    }
}

/// Busca um valor na árvore AVL
fn buscar<T: Ord + Display + Clone>(raiz: &ArvoreAVL<T>, valor: &T) -> bool {
    match raiz {
        None => false,
        Some(no) => match valor.cmp(&no.valor) {
            Ordering::Less => buscar(&no.esquerda, valor),
            Ordering::Greater => buscar(&no.direita, valor),
            Ordering::Equal => true,
        },
    }
}

/// Travessia em ordem (retorna valores ordenados)
fn em_ordem<T: Ord + Display + Clone>(raiz: &ArvoreAVL<T>, resultado: &mut Vec<T>) {
    if let Some(no) = raiz {
        em_ordem(&no.esquerda, resultado);
        resultado.push(no.valor.clone());
        em_ordem(&no.direita, resultado);
    }
}

fn main() {
    let mut arvore: ArvoreAVL<i32> = None;

    // Inserção de valores que causariam degeneração em BST
    let valores = vec![10, 20, 30, 40, 50, 60, 70];
    println!("Inserindo em ordem crescente: {:?}", valores);

    for v in &valores {
        arvore = inserir(arvore, *v);
        println!(
            "  Inseriu {}: altura = {}",
            v,
            altura(&arvore)
        );
    }

    // A árvore se mantém balanceada mesmo com inserção ordenada
    println!("\nAltura final: {} (BST teria altura {})",
        altura(&arvore),
        valores.len() as i32 - 1
    );

    // Travessia em ordem
    let mut ordenados = Vec::new();
    em_ordem(&arvore, &mut ordenados);
    println!("Em ordem: {:?}", ordenados);

    // Busca
    println!("\nBuscar 30: {}", buscar(&arvore, &30));
    println!("Buscar 35: {}", buscar(&arvore, &35));

    // Remoção
    arvore = remover(arvore, &40);
    println!("\nApós remover 40:");
    let mut apos = Vec::new();
    em_ordem(&arvore, &mut apos);
    println!("Em ordem: {:?}", apos);
    println!("Altura: {}", altura(&arvore));
}

Métodos Principais

Função/MétodoDescrição
inserir(raiz, valor)Insere um valor e rebalanceia a árvore
remover(raiz, valor)Remove um valor e rebalanceia a árvore
buscar(raiz, valor)Busca binária aproveitando a propriedade BST
rotacao_direita(no)Rotação simples para corrigir desbalanceamento LL
rotacao_esquerda(no)Rotação simples para corrigir desbalanceamento RR
balancear(no)Detecta o tipo de desbalanceamento e aplica rotação
altura(no)Retorna a altura armazenada no nó (O(1))
fator_balanceamento()Calcula a diferença de alturas dos filhos
em_ordem(raiz)Travessia que produz os elementos em ordem crescente

Exemplo Prático: Índice de Banco de Dados

Bancos de dados frequentemente usam árvores balanceadas para indexar registros, permitindo buscas, inserções e remoções eficientes mesmo com milhões de registros.

/// Simula um índice de banco de dados usando árvore AVL
#[derive(Debug, Clone, PartialEq, Eq)]
struct RegistroIndice {
    chave: i64,          // Valor indexado (ex: ID do usuário)
    posicao_disco: u64,  // Posição do registro no disco
}

impl PartialOrd for RegistroIndice {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for RegistroIndice {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.chave.cmp(&other.chave)
    }
}

impl std::fmt::Display for RegistroIndice {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[chave={}, pos={}]", self.chave, self.posicao_disco)
    }
}

fn main() {
    let mut indice: ArvoreAVL<RegistroIndice> = None;

    // Simulação: inserindo registros no índice
    let registros = vec![
        (1001, 0), (1005, 512), (1002, 1024),
        (1008, 1536), (1003, 2048), (1007, 2560),
    ];

    for (chave, pos) in registros {
        let registro = RegistroIndice {
            chave,
            posicao_disco: pos,
        };
        indice = inserir(indice, registro);
    }

    // A altura do índice é mantida baixa graças ao balanceamento
    println!("Índice com {} registros, altura: {}",
        6, altura(&indice));

    // Busca eficiente: O(log n) mesmo no pior caso
    let busca_chave = RegistroIndice { chave: 1005, posicao_disco: 0 };
    println!("Chave 1005 existe no índice: {}", buscar(&indice, &busca_chave));
}

Comparação com Outras Estruturas

CritérioAVLBST SimplesRubro-NegraHashMap
Busca (pior caso)O(log n)O(n)O(log n)O(n)*
Inserção (pior caso)O(log n)O(n)O(log n)O(n)*
BalanceamentoEstrito (±1)NenhumRelaxadoN/A
Rotações por inserçãoAté 20Até 2N/A
Rotações por remoçãoAté O(log n)0Até 3N/A
Altura máxima1.44 log nn-12 log nN/A
Dados ordenadosSimSimSimNão

* Pior caso com muitas colisões.

Quando usar Árvore AVL?

  • Use quando buscas são muito mais frequentes que inserções/remoções (o balanceamento mais estrito favorece buscas).
  • Use quando precisa de garantia estrita de O(log n) para todas as operações.
  • Não use quando inserções e remoções são muito frequentes — rubro-negra faz menos rotações.
  • Não use quando não precisa de ordenação — HashMap é mais simples e rápido para busca pura.

Exercícios

Exercício 1: Visualização de Rotações

Trace manualmente as rotações ao inserir os valores [15, 10, 20, 5, 12, 11] em uma árvore AVL. Identifique qual rotação (LL, RR, LR ou RL) ocorre em cada passo e desenhe a árvore resultante.

Exercício 2: Implementar Busca por Intervalo

Adicione uma função buscar_intervalo(raiz, min, max) -> Vec<T> que retorne todos os valores da árvore AVL no intervalo [min, max] de forma eficiente (sem visitar nós desnecessários). A complexidade deve ser O(log n + k), onde k é o número de resultados.

Exercício 3: Contar Rotações

Modifique a implementação para contar quantas rotações são realizadas durante uma sequência de inserções. Compare o número de rotações ao inserir n elementos em ordem crescente versus inserir n elementos aleatórios. O que você observa?


Conclusão

A árvore AVL é uma das estruturas mais elegantes da ciência da computação. Com um conceito simples — manter o fator de balanceamento entre -1 e 1 — ela garante desempenho O(log n) para todas as operações, eliminando o problema de degeneração das BSTs simples.

Em Rust, a implementação exige atenção com ownership e borrowing ao manipular os ponteiros Box durante as rotações, mas o resultado é um código seguro sem necessidade de gerenciamento manual de memória. A AVL é particularmente indicada para cenários onde buscas predominam sobre modificações, como índices de bancos de dados em memória e dicionários. Quando a frequência de modificações é alta, considere a árvore rubro-negra, que veremos no próximo artigo.