Árvores AVL e vermelho-pretas
Rotações que conservam a ordem e invariantes que limitam a altura.
Uma BST simples pode ter altura linear. Uma árvore equilibrada conserva a ordem de pesquisa e acrescenta um invariante que força altura logarítmica. Uma rotação, ou seja, um rearranjo local que troca uma raiz com um filho, corrige a forma sem mudar a sequência em-ordem.
A escolha da rotação depende do lado pesado do filho. Se o filho está pesado do lado exterior, uma rotação chega; se está pesado do lado interior, são precisas duas. A tabela seguinte fixa essa decisão antes dos exemplos.
Rotações e ordem
Numa rotação à direita, a raiz local z tem filho esquerdo y. Chamamos A à subárvore esquerda de y, B à direita de y e C à direita de z. Na animação, as três subárvores são não vazias e cada triângulo representa todos os seus nós.
As chaves de B são maiores que y, porque B está à sua direita. Também são menores que z, porque B pertence à subárvore esquerda de z. Por isso, podes passar B de y.right para z.left sem perder a ordem de pesquisa. Depois, y torna-se a raiz local e z fica como seu filho direito. A continua à esquerda de y e C à direita de z.
Não foi possível carregar a animação. Abre o vídeo ou acompanha a descrição abaixo.
Antes e depois, a sequência em-ordem visita os nós de A, depois y, os nós de B, z e os nós de C. A rotação também funciona se alguma destas subárvores for vazia. A rotação à esquerda é simétrica.
Uma rotação mexe num número constante de ligações, logo custa . Se guardas alturas ou tamanhos nos nós, atualiza primeiro a antiga raiz e depois a nova, porque o segundo cálculo depende do primeiro. Também tens de ligar a nova raiz ao pai ou à raiz da árvore.
Invariante AVL
Usamos altura de vazio -1 e fator de equilíbrio . Cada nó AVL satisfaz . Não é necessário que as duas subárvores tenham o mesmo número de nós.
Depois de inserires uma folha como numa BST, sobe pelo caminho de inserção, atualiza alturas e trata o primeiro desequilíbrio:
| Desequilíbrio | Caminho mais alto | Correção |
|---|---|---|
b=2, esquerda-esquerda | filho esquerdo pesado à esquerda | rotação à direita |
b=2, esquerda-direita | filho esquerdo pesado à direita | esquerda no filho, direita no nó |
b=-2, direita-direita | filho direito pesado à direita | rotação à esquerda |
b=-2, direita-esquerda | filho direito pesado à esquerda | direita no filho, esquerda no nó |
Ao inserires 30, 10 e 20, a raiz 30 fica com fator 2 e o filho 10 com fator -1. Uma rotação à esquerda em 10 cria a cadeia com 20 acima de 10; uma rotação à direita em 30 coloca 20 na raiz, com filhos 10 e 30. A em-ordem continua 10, 20, 30.
A primeira rotação transforma o ramo interior em exterior; a segunda repõe o equilíbrio. Compara a em-ordem dos três desenhos, que é sempre 10,20,30.
#include <algorithm>
#include <iostream>
class AVL {
struct No { int x, h = 0; No* e = nullptr; No* d = nullptr; };
No* raiz = nullptr;
static int altura(No* p) { return p ? p->h : -1; }
static void atualizar(No* p) { p->h = 1 + std::max(altura(p->e), altura(p->d)); }
static No* direita(No* p) {
No* q = p->e; p->e = q->d; q->d = p;
atualizar(p); atualizar(q); return q;
}
static No* esquerda(No* p) {
No* q = p->d; p->d = q->e; q->e = p;
atualizar(p); atualizar(q); return q;
}
static No* inserir(No* p, int x) {
if (!p) return new No{x};
if (x < p->x) p->e = inserir(p->e, x);
else if (p->x < x) p->d = inserir(p->d, x);
else return p;
atualizar(p);
const int b = altura(p->e) - altura(p->d);
if (b > 1) {
if (x > p->e->x) p->e = esquerda(p->e);
return direita(p);
}
if (b < -1) {
if (x < p->d->x) p->d = direita(p->d);
return esquerda(p);
}
return p;
}
static void destruir(No* p) { if (p) { destruir(p->e); destruir(p->d); delete p; } }
static void mostrar(No* p, bool& primeiro) {
if (!p) return;
mostrar(p->e, primeiro);
std::cout << (primeiro ? "" : " ") << p->x; primeiro = false;
mostrar(p->d, primeiro);
}
public:
AVL() = default;
AVL(const AVL&) = delete;
AVL& operator=(const AVL&) = delete;
~AVL() { destruir(raiz); }
void inserir(int x) { raiz = inserir(raiz, x); }
void mostrar() const { bool primeiro = true; mostrar(raiz, primeiro); std::cout << '\n'; }
int altura() const { return altura(raiz); }
};
int main() {
AVL a;
for (int x : {30, 10, 20, 40, 50, 20}) a.inserir(x);
a.mostrar();
std::cout << "altura: " << a.altura() << '\n';
AVL vazia; std::cout << "vazia: " << vazia.altura() << '\n';
}Executa o código para ver a saída e os gráficos.
O programa ignora duplicados e mantém alturas durante a inserção. Não implementa remoção. Depois de rodar no primeiro nó desequilibrado, a subárvore recupera a altura anterior à inserção; continua a atualizar os antepassados, mas não precisa de outra reparação.
Na remoção, começa por retirar como numa BST e verifica todos os antepassados. O pressuposto muda: uma reparação pode deixar a subárvore mais baixa e propagar o desequilíbrio para cima, por isso um único arranjo pode não chegar. A decisão usa o fator do filho, porque a chave apagada já não identifica o ramo pesado. Com b=2 e filho de fator zero ou positivo, usa uma rotação simples à direita; com filho de fator negativo, usa uma dupla. O caso b=-2 é simétrico. O efeito com filho de fator zero merece atenção: a rotação simples repõe o equilíbrio, mas a altura da subárvore não diminui, por isso continua a verificar os antepassados acima.
Porque a altura AVL é logarítmica
Prova do limite logarítmico
Se N(h) é o menor número de nós numa AVL de altura h, então
Para atingir altura h, uma subárvore tem de ter altura h-1; o invariante obriga a outra a ter pelo menos h-2. Esta recorrência cresce como os números de Fibonacci, portanto exponencialmente em h. Invertendo a relação, .
Pesquisa, inserção e remoção custam ; as rotações não acrescentam um fator logarítmico por nível.
Invariantes vermelho-pretos
Uma árvore vermelho-preta é uma BST com uma cor por nó e folhas vazias NIL consideradas pretas. A raiz é preta; um nó vermelho não pode ter filho vermelho; todos os caminhos de um nó até folhas NIL têm o mesmo número de nós pretos, segundo uma convenção fixa de contagem.
Chama altura preta ao número de nós pretos num caminho descendente, excluindo o nó inicial e incluindo a folha NIL. Uma subárvore com altura preta b tem pelo menos nós internos. Como não há dois vermelhos consecutivos, a altura é no máximo duas vezes a altura preta, dando . O equilíbrio é menos restrito que na AVL, mas continua a garantir operações logarítmicas.
Inserção e reparação vermelho-preta
Insere o nó como numa BST e pinta-o de vermelho. Assim não alteras a altura preta dos caminhos. Se o pai for preto, não há conflito. Se o pai for vermelho, o avô existe porque a raiz é preta:
- Com tio vermelho, pinta pai e tio de preto e avô de vermelho. O número de pretos nos caminhos mantém-se; o conflito pode subir para o avô.
- Com tio preto exterior, roda no avô e troca as cores do pai e do avô: o novo topo fica preto e os filhos vermelhos deixam de ser consecutivos.
- Com tio preto, uma configuração interior precisa primeiro de uma rotação no pai para ficar exterior. Depois roda no avô e troca as cores do pai e do avô. O novo topo local fica preto e os filhos vermelhos deixam de ser consecutivos.
No fim, pinta a raiz de preto. As configurações têm casos simétricos para o pai ser filho esquerdo ou direito. Não decidas uma rotação apenas por três valores: as cores e o lado do tio também importam.
A remoção vermelho-preta fica aqui como enquadramento: a fonte de 2025/26 indica que não detalha esse procedimento, por isso não a uses como receita de exame. Na remoção, retirar um nó vermelho não reduz a altura preta. Retirar um nó preto pode deixar um ramo com um preto a menos. A reparação compara o irmão e os seus filhos: um irmão vermelho converte-se por rotação e recoloração para um caso com irmão preto; com filhos do irmão pretos, recolore e propaga a falta; com um filho vermelho adequado, rotações e recolorações restauram a altura preta. Estes passos devem respeitar as folhas NIL, que não são valores do conjunto.
AVL conserva altura mais apertada e pode favorecer pesquisa. Vermelho-preta admite mais variação e pode requerer menos reparações em alterações. A escolha prática depende do padrão de operações e da implementação; a notação assintótica não prevê qual será mais rápida numa medição concreta.
No modelo de armazenamento dos slides, uma cor precisa de um bit e os três fatores AVL precisam de dois bits. Guardar a altura completa num inteiro, como no programa desta página, é outra escolha. Os bytes reais por nó dependem dos campos, do alinhamento e da implementação; a comparação de bits de informação não determina esse tamanho em memória.
Exercícios
As justificações e os programas em C++ são de autoavaliação: compara-os com os critérios da solução. Nas questões numéricas, a verificação automática confirma apenas o valor.
Corrigir uma inserção interior
Insere 50, 20 e 35 numa AVL vazia. Usa b=altura(esq)-altura(dir). Indica o desequilíbrio, as rotações e a árvore final.
Pista
A raiz fica pesada à esquerda e o filho pesado à direita.
Mais uma pista
É o caso esquerda-direita.
Solução
Na raiz 50, b=2; no filho 20, b=-1, ou seja, é o caso esquerda-direita. Roda à esquerda em 20 e depois à direita em 50. A raiz final é 35 com filhos 20 e 50. As alturas dos três nós são 1, 0 e 0. A em-ordem mantém 20,35,50.
Confere a tua resposta:
- Obtive fatores 2 em 50 e -1 em 20.
- Rodei primeiro à esquerda em 20 e depois à direita em 50.
- Obtive raiz 35, filhos 20 e 50, alturas 1,0,0 e em-ordem preservada.
Erros frequentes
Uma rotação simples à direita não corrige esta configuração; atualizar primeiro a nova raiz pode usar alturas antigas.
Aplicar a recorrência da altura
Qual é o número mínimo de nós de uma AVL com altura 4, contando arestas e altura -1 para vazio?
Pista
Calcula a recorrência a partir de N(-1)=0 e N(0)=1.
Solução
N(4)=1+N(3)+N(2)=1+7+4=12. Uma árvore perfeita de altura 4 teria 31, mas AVL não exige perfeição. Ou seja, a diferença máxima de alturas obriga ao mínimo exponencial que dá altura logarítmica.
Erros frequentes
Não uses 2^(h+1)-1 como mínimo AVL; não alteres a convenção de altura a meio.
Recoloração com tio vermelho
Numa árvore vermelho-preta, a raiz 10 é preta e os filhos 5 e 15 são vermelhos. Insere 2 como filho esquerdo de 5. Explica a primeira reparação e a cor final da raiz.
Pista
Pai e tio são vermelhos.
Mais uma pista
Recolore os dois filhos de preto e o avô de vermelho.
Solução
O conflito vermelho 2–5 resolve-se pintando 5 e 15 de preto e 10 de vermelho, porque o tio é vermelho. A contagem de pretos nos caminhos abaixo desta região conserva-se. Como 10 é a raiz, pinta-o de preto no fim; a contagem de pretos, incluindo a raiz, aumenta igualmente em todos os caminhos. Com a convenção da lição, que exclui o nó inicial e inclui NIL, a altura preta de 10 passa de 1 na árvore inicial para 2 na final, porque 5 e 15 passaram a pretos. Pintar a própria raiz de preto não muda a sua altura preta, mas restaura a regra da raiz. Não é necessária rotação neste caso.
Confere a tua resposta:
- Identifiquei pai 5 e tio 15 vermelhos.
- Pintei 5 e 15 de preto e 10 de vermelho antes da correção da raiz.
- Mantive 10 como raiz preta final e expliquei o aumento uniforme da contagem preta.
Erros frequentes
Escolher automaticamente uma rotação esquerda-esquerda ignora a cor do tio; a raiz não pode ficar vermelha.
Remover a folha 40 e rodar à direita
Parte da AVL com raiz 30, filho esquerdo 20 com filho esquerdo 10 e filho direito 40. Remove 40 e repõe o invariante AVL. Mostra alturas, fatores e a rotação.
Pista
Mais uma pista
Solução
Antes: h(10)=0, h(20)=1, h(40)=0 e h(30)=2; fatores b(30)=1, b(20)=1 e b(40)=0. Após remover 40, h(30)=2 com b(30)=2 e o filho 20 com b=1, caso esquerda-esquerda. Uma rotação à direita em 30 coloca 20 na raiz, com filhos 10 e 30; a em-ordem continua 10,20,30 e todos os fatores ficam 0.
Confere a tua resposta:
- Calculei alturas e fatores antes da remoção.
- Identifiquei b=2 na raiz com filho b=1 e apliquei rotação simples à direita.
- Verifiquei fatores em {-1,0,1} e em-ordem 10,20,30.
Erros frequentes
Decidir só pela chave apagada escolhe o lado errado após a remoção; é preciso ler o fator do filho.
