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:
- Todo nó é vermelho ou negro.
- A raiz é sempre negra.
- Toda folha (NIL/nula) é considerada negra.
- Se um nó é vermelho, ambos os seus filhos devem ser negros (não pode haver dois vermelhos consecutivos).
- 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ção | Melhor Caso | Caso Médio | Pior Caso |
|---|---|---|---|
| Busca | O(1) | O(log n) | O(log n) |
| Inserção | O(log n) | O(log n) | O(log n) |
| Remoção | O(log n) | O(log n) | O(log n) |
| Rotações/inserção | O(1) | O(1) | 2 |
| Rotações/remoção | O(1) | O(1) | 3 |
| Espaço | — | O(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étodo | Descriçã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ério | Rubro-Negra | AVL | BST | B-Tree |
|---|---|---|---|---|
| Busca (pior caso) | O(log n) | O(log n) | O(n) | O(log n) |
| Rotações por inserção | Até 2 | Até 2 | 0 | 0* |
| Rotações por remoção | Até 3 | Até O(log n) | 0 | 0* |
| Altura máxima | 2 log(n+1) | 1.44 log n | n-1 | log_t(n) |
| Busca mais rápida? | Não | Sim | — | Depende |
| Modificação mais rápida? | Sim | Não | — | Sim (disco) |
| Uso em stdlib Rust | — | — | — | BTreeMap |
* 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/BTreeSetpara 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.