Árvore Rubro-Negra (Red-Black Tree)

Entenda as 5 propriedades da árvore rubro-negra, as regras de coloração e rebalanceamento, e sua relação com BTreeMap da biblioteca padrão de Rust.

Introdução

A árvore rubro-negra (Red-Black Tree) é uma árvore de busca binária autobalanceada que utiliza um esquema de coloração — cada nó é marcado como vermelho ou negro — para manter o balanceamento. Inventada por Rudolf Bayer em 1972 e refinada por Leonidas Guibas e Robert Sedgewick em 1978, ela é uma das estruturas de dados mais utilizadas na prática.

A grande vantagem da árvore rubro-negra sobre a AVL é que ela requer menos rotações durante inserções e remoções, tornando-a mais eficiente em cenários com muitas modificações. Por essa razão, ela é a estrutura escolhida para a maioria das implementações de mapas e conjuntos ordenados em linguagens de programação, incluindo a implementação interna do BTreeMap em Rust (que usa uma variante B-Tree, mas o conceito rubro-negro é fundamental na ciência da computação).


Conceito e Teoria

As 5 Propriedades Fundamentais

Uma árvore rubro-negra é uma BST que satisfaz todas as seguintes propriedades:

  1. Todo nó é vermelho ou negro.
  2. A raiz é sempre negra.
  3. Toda folha (NIL/nula) é considerada negra.
  4. Se um nó é vermelho, ambos os seus filhos devem ser negros (não pode haver dois vermelhos consecutivos).
  5. Para cada nó, todos os caminhos dele até qualquer folha descendente contêm o mesmo número de nós negros (black-height).
    Árvore Rubro-Negra válida:

              [13:N]                 N = Negro
             /      \                V = Vermelho
          [8:V]    [17:V]
         /    \       \
      [1:N]  [11:N]  [25:N]
        \              /
       [6:V]        [22:V]

    Verificação da propriedade 5 (black-height = 2 para raiz):
    13 → 8 → 1 → NIL    : negros = {13, 1}     = 2 ✓
    13 → 8 → 1 → 6 → NIL: negros = {13, 1}     = 2 ✓
    13 → 8 → 11 → NIL   : negros = {13, 11}    = 2 ✓
    13 → 17 → 25 → NIL  : negros = {13, 25}    = 2 ✓
    13 → 17 → 25 → 22   : negros = {13, 25}    = 2 ✓

Por que essas propriedades funcionam?

A combinação das propriedades 4 e 5 garante que o caminho mais longo da raiz até uma folha é no máximo 2 vezes o caminho mais curto. Isso porque:

  • O caminho mais curto pode ter apenas nós negros.
  • O caminho mais longo alterna entre negros e vermelhos.
  • Como a propriedade 5 garante o mesmo número de negros, o caminho mais longo tem no máximo o dobro de nós.

Portanto, a altura é no máximo 2 * log2(n+1), garantindo O(log n) para todas as operações.

Casos de Inserção

Ao inserir um nó (sempre colorido de vermelho inicialmente), podem surgir violações que são corrigidas por recoloração ou rotações:

Caso 1 — Tio é vermelho (recoloração):

       [G:N]                    [G:V]
      /     \                  /     \
   [P:V]   [T:V]    →      [P:N]   [T:N]
   /                        /
 [X:V]                    [X:V]

 Recolore P e T para negro, G para vermelho.
 Continue verificando a partir de G.

Caso 2 — Tio é negro, X é filho interno (rotação + caso 3):

       [G:N]                    [G:N]
      /     \                  /     \
   [P:V]   [T:N]    →      [X:V]   [T:N]
      \                     /
     [X:V]               [P:V]

 Rotação à esquerda em P. Agora trata como Caso 3.

Caso 3 — Tio é negro, X é filho externo (rotação + recoloração):

       [G:N]                    [P:N]
      /     \                  /     \
   [P:V]   [T:N]    →      [X:V]   [G:V]
   /                                   \
 [X:V]                               [T:N]

 Rotação à direita em G. Troca cores de P e G.

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ções/inserçãoO(1)O(1)2
Rotações/remoçãoO(1)O(1)3
EspaçoO(n)O(n)

Destaque: A inserção requer no máximo 2 rotações e a remoção no máximo 3 rotações, independente do tamanho da árvore. Isso é significativamente melhor que a AVL, que pode fazer até O(log n) rotações na remoção.


Implementação em Rust

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

/// Cores dos nós: vermelho ou negro
#[derive(Debug, Clone, Copy, PartialEq)]
enum Cor {
    Vermelho,
    Negro,
}

/// Nó da árvore rubro-negra
#[derive(Debug, Clone)]
struct No<T: Ord + Display + Clone> {
    valor: T,
    cor: Cor,
    esquerda: ArvoreRN<T>,
    direita: ArvoreRN<T>,
}

/// Tipo da árvore rubro-negra
type ArvoreRN<T> = Option<Box<No<T>>>;

/// Verifica se um nó é vermelho
fn eh_vermelho<T: Ord + Display + Clone>(no: &ArvoreRN<T>) -> bool {
    match no {
        Some(n) => n.cor == Cor::Vermelho,
        None => false, // NIL é negro (propriedade 3)
    }
}

/// Rotação à esquerda (link vermelho à direita vai para esquerda)
fn rotacao_esquerda<T: Ord + Display + Clone>(mut no: Box<No<T>>) -> Box<No<T>> {
    let mut novo_raiz = no.direita.take().expect("Filho direito necessário");
    no.direita = novo_raiz.esquerda.take();
    novo_raiz.cor = no.cor;
    no.cor = Cor::Vermelho;
    novo_raiz.esquerda = Some(no);
    novo_raiz
}

/// Rotação à direita (link vermelho à esquerda sobe)
fn rotacao_direita<T: Ord + Display + Clone>(mut no: Box<No<T>>) -> Box<No<T>> {
    let mut novo_raiz = no.esquerda.take().expect("Filho esquerdo necessário");
    no.esquerda = novo_raiz.direita.take();
    novo_raiz.cor = no.cor;
    no.cor = Cor::Vermelho;
    novo_raiz.direita = Some(no);
    novo_raiz
}

/// Inverte as cores do nó e seus dois filhos
fn inverter_cores<T: Ord + Display + Clone>(no: &mut Box<No<T>>) {
    no.cor = if no.cor == Cor::Vermelho {
        Cor::Negro
    } else {
        Cor::Vermelho
    };
    if let Some(ref mut esq) = no.esquerda {
        esq.cor = if esq.cor == Cor::Vermelho {
            Cor::Negro
        } else {
            Cor::Vermelho
        };
    }
    if let Some(ref mut dir) = no.direita {
        dir.cor = if dir.cor == Cor::Vermelho {
            Cor::Negro
        } else {
            Cor::Vermelho
        };
    }
}

/// Corrige violações rubro-negras (implementação Left-Leaning Red-Black Tree)
fn corrigir<T: Ord + Display + Clone>(mut no: Box<No<T>>) -> Box<No<T>> {
    // Se o filho direito é vermelho e o esquerdo não, rotaciona à esquerda
    if eh_vermelho(&no.direita) && !eh_vermelho(&no.esquerda) {
        no = rotacao_esquerda(no);
    }
    // Se o filho esquerdo e o neto esquerdo são vermelhos, rotaciona à direita
    if eh_vermelho(&no.esquerda)
        && no
            .esquerda
            .as_ref()
            .map_or(false, |e| eh_vermelho(&e.esquerda))
    {
        no = rotacao_direita(no);
    }
    // Se ambos os filhos são vermelhos, inverte as cores
    if eh_vermelho(&no.esquerda) && eh_vermelho(&no.direita) {
        inverter_cores(&mut no);
    }
    no
}

/// Árvore Rubro-Negra (Left-Leaning Red-Black Tree)
#[derive(Debug)]
struct ArvoreRubroNegra<T: Ord + Display + Clone> {
    raiz: ArvoreRN<T>,
    tamanho: usize,
}

impl<T: Ord + Display + Clone> ArvoreRubroNegra<T> {
    /// Cria uma árvore rubro-negra vazia
    fn nova() -> Self {
        ArvoreRubroNegra {
            raiz: None,
            tamanho: 0,
        }
    }

    /// Retorna o número de elementos
    fn tamanho(&self) -> usize {
        self.tamanho
    }

    /// Insere um valor na árvore
    fn inserir(&mut self, valor: T) {
        self.raiz = Self::inserir_rec(self.raiz.take(), valor);
        // Propriedade 2: a raiz é sempre negra
        if let Some(ref mut raiz) = self.raiz {
            raiz.cor = Cor::Negro;
        }
        self.tamanho += 1;
    }

    fn inserir_rec(no: ArvoreRN<T>, valor: T) -> ArvoreRN<T> {
        match no {
            None => Some(Box::new(No {
                valor,
                cor: Cor::Vermelho, // Novo nó é sempre vermelho
                esquerda: None,
                direita: None,
            })),
            Some(mut atual) => {
                match valor.cmp(&atual.valor) {
                    Ordering::Less => {
                        atual.esquerda = Self::inserir_rec(atual.esquerda.take(), valor);
                    }
                    Ordering::Greater => {
                        atual.direita = Self::inserir_rec(atual.direita.take(), valor);
                    }
                    Ordering::Equal => {
                        return Some(atual); // Duplicado ignorado
                    }
                }
                Some(corrigir(atual))
            }
        }
    }

    /// Busca um valor na árvore
    fn buscar(&self, valor: &T) -> bool {
        let mut atual = &self.raiz;
        while let Some(no) = atual {
            match valor.cmp(&no.valor) {
                Ordering::Less => atual = &no.esquerda,
                Ordering::Greater => atual = &no.direita,
                Ordering::Equal => return true,
            }
        }
        false
    }

    /// Travessia em ordem (valores ordenados)
    fn em_ordem(&self) -> Vec<T> {
        let mut resultado = Vec::new();
        Self::em_ordem_rec(&self.raiz, &mut resultado);
        resultado
    }

    fn em_ordem_rec(no: &ArvoreRN<T>, resultado: &mut Vec<T>) {
        if let Some(atual) = no {
            Self::em_ordem_rec(&atual.esquerda, resultado);
            resultado.push(atual.valor.clone());
            Self::em_ordem_rec(&atual.direita, resultado);
        }
    }

    /// Calcula a black-height (número de nós negros até uma folha)
    fn black_height(&self) -> usize {
        let mut contagem = 0;
        let mut atual = &self.raiz;
        while let Some(no) = atual {
            if no.cor == Cor::Negro {
                contagem += 1;
            }
            atual = &no.esquerda;
        }
        contagem
    }

    /// Verifica se a árvore satisfaz todas as propriedades rubro-negras
    fn verificar_propriedades(&self) -> bool {
        // Propriedade 2: raiz é negra
        if eh_vermelho(&self.raiz) {
            println!("Violação: raiz é vermelha");
            return false;
        }
        // Verifica propriedades 4 e 5 recursivamente
        Self::verificar_rec(&self.raiz).is_some()
    }

    /// Retorna Some(black_height) se válida, None se inválida
    fn verificar_rec(no: &ArvoreRN<T>) -> Option<usize> {
        match no {
            None => Some(1), // NIL é negro
            Some(atual) => {
                // Propriedade 4: vermelhos não podem ter filhos vermelhos
                if atual.cor == Cor::Vermelho {
                    if eh_vermelho(&atual.esquerda) || eh_vermelho(&atual.direita) {
                        println!("Violação: nó vermelho {} tem filho vermelho", atual.valor);
                        return None;
                    }
                }
                let bh_esq = Self::verificar_rec(&atual.esquerda)?;
                let bh_dir = Self::verificar_rec(&atual.direita)?;
                // Propriedade 5: black-height igual em ambos os lados
                if bh_esq != bh_dir {
                    println!(
                        "Violação: black-height diferente no nó {} (esq={}, dir={})",
                        atual.valor, bh_esq, bh_dir
                    );
                    return None;
                }
                let incremento = if atual.cor == Cor::Negro { 1 } else { 0 };
                Some(bh_esq + incremento)
            }
        }
    }
}

fn main() {
    let mut arvore = ArvoreRubroNegra::nova();

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

    for v in &valores {
        arvore.inserir(*v);
    }

    println!("Tamanho: {}", arvore.tamanho());
    println!("Black-height: {}", arvore.black_height());
    println!("Propriedades válidas: {}", arvore.verificar_propriedades());
    println!("Em ordem: {:?}", arvore.em_ordem());

    // Busca
    println!("\nBuscar 50: {}", arvore.buscar(&50));
    println!("Buscar 55: {}", arvore.buscar(&55));
}

Métodos Principais

MétodoDescrição
nova()Cria uma árvore rubro-negra vazia
inserir(valor)Insere com recoloração e rotações automáticas
buscar(valor)Busca iterativa O(log n)
em_ordem()Retorna vetor com elementos em ordem crescente
black_height()Calcula a black-height (nós negros até folha)
verificar_propriedades()Verifica se todas as 5 propriedades são satisfeitas
rotacao_esquerda(no)Move link vermelho da direita para esquerda
rotacao_direita(no)Move link vermelho da esquerda para cima
inverter_cores(no)Inverte cores do nó e seus dois filhos
corrigir(no)Aplica as correções necessárias após inserção

Exemplo Prático: Agendamento de Intervalos

A árvore rubro-negra é excelente para manter intervalos de tempo ordenados, permitindo encontrar conflitos rapidamente.

use std::fmt;

/// Representa um intervalo de tempo (ex: reunião)
#[derive(Debug, Clone)]
struct Intervalo {
    inicio: u32,  // Hora de início (em minutos desde meia-noite)
    fim: u32,     // Hora de término
    descricao: String,
}

impl Intervalo {
    fn novo(inicio: u32, fim: u32, descricao: &str) -> Self {
        Intervalo {
            inicio,
            fim,
            descricao: descricao.to_string(),
        }
    }

    /// Verifica se dois intervalos se sobrepõem
    fn sobrepoe(&self, outro: &Intervalo) -> bool {
        self.inicio < outro.fim && outro.inicio < self.fim
    }

    /// Formata hora em formato HH:MM
    fn formato_hora(minutos: u32) -> String {
        format!("{:02}:{:02}", minutos / 60, minutos % 60)
    }
}

impl fmt::Display for Intervalo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}-{} {}",
            Self::formato_hora(self.inicio),
            Self::formato_hora(self.fim),
            self.descricao
        )
    }
}

impl PartialEq for Intervalo {
    fn eq(&self, other: &Self) -> bool {
        self.inicio == other.inicio
    }
}
impl Eq for Intervalo {}

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

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

fn main() {
    let mut agenda = ArvoreRubroNegra::nova();

    // Inserir compromissos
    agenda.inserir(Intervalo::novo(540, 600, "Reunião diária"));
    agenda.inserir(Intervalo::novo(600, 720, "Desenvolvimento"));
    agenda.inserir(Intervalo::novo(720, 780, "Almoço"));
    agenda.inserir(Intervalo::novo(840, 900, "Code review"));
    agenda.inserir(Intervalo::novo(960, 1020, "Retrospectiva"));

    println!("=== Agenda do Dia (ordenada) ===");
    for intervalo in agenda.em_ordem() {
        println!("  {}", intervalo);
    }

    // Verificar conflito com novo compromisso
    let novo = Intervalo::novo(570, 630, "Entrevista");
    println!("\nTentar agendar: {}", novo);

    let conflitos: Vec<_> = agenda
        .em_ordem()
        .iter()
        .filter(|i| i.sobrepoe(&novo))
        .cloned()
        .collect();

    if conflitos.is_empty() {
        println!("Sem conflitos! Pode agendar.");
    } else {
        println!("Conflitos encontrados:");
        for c in &conflitos {
            println!("  - {}", c);
        }
    }
}

Comparação com Outras Estruturas

CritérioRubro-NegraAVLBSTB-Tree
Busca (pior caso)O(log n)O(log n)O(n)O(log n)
Rotações por inserçãoAté 2Até 200*
Rotações por remoçãoAté 3Até O(log n)00*
Altura máxima2 log(n+1)1.44 log nn-1log_t(n)
Busca mais rápida?NãoSimDepende
Modificação mais rápida?SimNãoSim (disco)
Uso em stdlib RustBTreeMap

* B-Trees usam splits/merges ao invés de rotações.

Quando usar Árvore Rubro-Negra?

  • Use quando inserções e remoções são tão frequentes quanto buscas.
  • Use quando precisa de uma árvore balanceada com rebalanceamento rápido.
  • Na prática em Rust, use BTreeMap/BTreeSet para dados ordenados — são a escolha padrão da biblioteca padrão.
  • Não use se buscas predominam fortemente — AVL é levemente mais rápida em buscas.

Exercícios

Exercício 1: Implementar Remoção

Implemente o método remover(valor) para a árvore rubro-negra. A remoção é significativamente mais complexa que a inserção e requer tratar vários subcasos. Dica: use a abordagem de “mover vermelho para baixo” durante a busca pelo nó a ser removido.

Exercício 2: Contagem de Nós por Cor

Adicione métodos contar_vermelhos() e contar_negros() que retornem o número de nós vermelhos e negros na árvore, respectivamente. Verifique empiricamente que a proporção de nós vermelhos diminui conforme a árvore cresce.

Exercício 3: Comparação Empírica AVL vs Rubro-Negra

Implemente ambas as árvores e compare o número total de rotações ao inserir 10.000 elementos aleatórios seguidos de 5.000 remoções aleatórias. Meça também o tempo de execução usando std::time::Instant. Qual árvore é mais rápida para cada operação?


Conclusão

A árvore rubro-negra é uma das estruturas de dados mais importantes da computação moderna. Sua abordagem de coloração permite manter o balanceamento com um número limitado de rotações por operação, tornando-a ideal para cenários com muitas modificações.

Em Rust, a implementação Left-Leaning Red-Black Tree (LLRB) simplifica significativamente o código em comparação com a implementação clássica, mantendo as mesmas garantias de complexidade. Embora a biblioteca padrão de Rust use B-Trees para BTreeMap e BTreeSet, compreender árvores rubro-negras é fundamental para qualquer programador, pois elas aparecem em praticamente todas as linguagens de programação e sistemas operacionais. A compreensão profunda dessa estrutura prepara o terreno para entender B-Trees, que veremos a seguir.