Estrutura de Dados I
Árvores

Árvores balanceadas

O que significa uma árvore estar balanceada, o fator de balanceamento e as rotações, a ferramenta comum das árvores AVL e rubro-negras.

Na árvore binária de busca, buscar, inserir e remover custam , e a altura depende só da ordem em que as chaves chegam: entre , no melhor caso, e , quando as chaves chegam ordenadas. E chaves ordenadas ou quase ordenadas são comuns na prática: matrículas, datas, códigos sequenciais, registros lidos de um arquivo já ordenado.

As árvores balanceadas resolvem esse problema: são árvores de busca que modificam a inserção e a remoção para que a altura fique sempre , qualquer que seja a ordem das operações. Esta página apresenta o que elas têm em comum; as próximas tratam de duas delas, a árvore AVL e a árvore rubro-negra (na variante LLRB).

O que é estar balanceada

Definição 1(Família balanceada)

Uma família de árvores binárias de busca é balanceada se existe uma constante tal que toda árvore da família com nós tem altura no máximo . Em outras palavras, a altura é sempre .

Repare que a definição fala de uma família de árvores, definida por alguma regra, e da garantia que a regra dá. Diferentes regras levam a diferentes famílias:

  • balanceamento por altura: em todo nó, as alturas da SAE e da SAD diferem de no máximo 1. É o critério das árvores AVL;
  • balanceamento por peso: em todo nó, o número de nós da SAE e o da SAD são iguais ou próximos;
  • critérios mais relaxados, como o das árvores rubro-negras: nenhum caminho da raiz até um NULL tem mais que o dobro do comprimento de outro.
Balanceada não é só |fb| ≤ 1

Na página de árvores binárias, chamamos de balanceada a árvore em que as alturas das subárvores de cada nó diferem de no máximo 1. Esse é o critério da AVL, e é o significado mais comum de "balanceada". Mas uma árvore rubro-negra pode ter nós com subárvores de alturas bem diferentes e, mesmo assim, ter altura . O que todas as árvores balanceadas têm em comum é a garantia de altura logarítmica, e não um critério específico.

Fator de balanceamento

Para medir o quanto um nó está "torto", comparamos as alturas das suas subárvores.

Definição 2(Fator de balanceamento)

O fator de balanceamento de um nó é

com a convenção de que a árvore vazia tem altura . Se , a SAE é mais alta; se , a SAD é mais alta.

Alguns livros usam a diferença ao contrário (SAD menos SAE), o que só troca os sinais. E a convenção de altura (folha com altura 0 ou 1) não muda nada: o fator é uma diferença de alturas.

Duas árvores com o fator de balanceamento anotado ao lado de cada nó. Em (a), as chaves 50, 30, 70, 20, 40, 80 e 35: os fatores são +1 em 50, -1 em 30, -1 em 70, +1 em 40 e 0 nas folhas, todos com módulo no máximo 1. Em (b), as chaves 50, 30, 70, 20 e 10: os nós 30 e 50 têm fator +2 e estão em vermelho
(a) |fb| ≤ 1 em todos os nós, sem a árvore ser cheia. (b) Inserir 10 depois de 20 desequilibra dois nós.

Dois detalhes da figura. Em (a), a árvore não é cheia (40 e 70 têm um filho só), e mesmo assim todos os nós têm : estar balanceada no critério da AVL não exige que a árvore seja perfeita. Em (b), o desequilíbrio não aparece em um nó só: ele se propaga para cima, atingindo o 30 e o seu ancestral 50.

Pergunta

Na árvore (b), reorganize apenas a subárvore com raiz 30, colocando 20 como raiz dela, com 10 à esquerda e 30 à direita. A árvore continua sendo de busca? Recalcule os fatores: o nó 50 continua desequilibrado?

Ver respostaEsconder resposta

Continua sendo de busca: em ordem, as chaves seguem 10, 20, 30, 50, 70. A nova subárvore com raiz 20 tem altura 1 e fatores 0 em todos os seus nós. O 50 passa a ter . Consertar o nó mais baixo que estava desequilibrado consertou também o ancestral: é exatamente isso que a AVL faz, e a reorganização usada aqui é uma rotação, que veremos adiante.

O programa abaixo insere as chaves da entrada em uma ABB comum e imprime, em ordem, a altura e o fator de cada nó. A entrada é a da árvore (b); troque por 7 e 50 30 70 20 40 80 35 para a árvore (a), ou por 7 e 1 2 3 4 5 6 7 para ver uma árvore degenerada.

fatores.cC
Pergunta

Esse programa calcula as alturas chamando altura em cada nó, e altura percorre a subárvore inteira. Quanto custa imprimeFatores em uma árvore degenerada com nós?

Ver respostaEsconder resposta

O nó de profundidade tem uma subárvore com nós, que é percorrida inteira (duas vezes, por fatorBalanceamento e por altura). O total é proporcional a , ou seja, . A saída é calcular as alturas de baixo para cima, numa única passada: é o primeiro exercício desta página. E é também o motivo pelo qual a AVL guarda a altura dentro de cada nó, para obter o fator em .

Rebalancear, mas localmente

A inserção e a remoção da ABB não olham para a forma da árvore. Basta inserir 10, 20 e 30, nessa ordem, para a raiz ficar com . Uma árvore balanceada precisa, depois de cada inserção ou remoção, detectar os nós desequilibrados e reorganizar a árvore, sem perder a ordem das chaves.

À esquerda, as chaves 10, 20 e 30 inseridas nessa ordem formam uma linha, com fator -2 na raiz 10. À direita, as mesmas chaves reorganizadas com 20 na raiz, 10 à esquerda e 30 à direita, todos com fator 0
As mesmas três chaves, antes e depois de uma reorganização.
Pergunta

Uma ideia ingênua: depois de cada inserção, copiar as chaves (em ordem simétrica) para um vetor ordenado e reconstruir a árvore inteira, sempre com o elemento do meio como raiz. Isso funciona? Quanto custa?

Ver respostaEsconder resposta

Funciona: a árvore reconstruída tem a menor altura possível. Mas cada reconstrução percorre e recria os nós, em , e inserções seguidas custam , pior que a própria ABB em ordem aleatória. Toda a vantagem de uma árvore balanceada se perde.

O desafio que a AVL e a rubro-negra resolvem é rebalancear com mudanças locais: apenas nos nós do caminho entre a raiz e o ponto da inserção ou da remoção. Como esse caminho tem nós (a árvore está balanceada antes da operação), a operação inteira, incluindo o rebalanceamento, continua custando . A ferramenta para essas mudanças locais é a rotação.

Rotações

Uma rotação reorganiza um nó e um dos seus filhos, trocando quem é pai de quem, sem mudar a ordem das chaves.

À esquerda, o nó y tem o filho esquerdo x e a subárvore C à direita; x tem as subárvores A e B. À direita, depois da rotação à direita em y, x é a raiz, com A à esquerda e y à direita; y tem B à esquerda e C à direita. A ligação da subárvore B, em vermelho, é a que muda de pai. A rotação à esquerda em x desfaz a operação. Em ordem, os dois lados são A, x, B, y, C
Rotação à direita em y e rotação à esquerda em x: uma desfaz a outra. Só a subárvore B muda de pai.

Na rotação à direita em , o filho esquerdo sobe e desce para a direita de . A subárvore , que estava à direita de , passa para a esquerda de : é o único lugar em que ela pode ficar, porque as suas chaves estão entre e . A rotação à esquerda é o espelho, e uma desfaz a outra.

/* Rotação à direita em y: o filho esquerdo x sobe e y vira filho direito
   de x. Devolve a nova raiz da subárvore (x). */
No *rotacaoDireita(No *y) {
    No *x = y->esq;
    y->esq = x->dir;        /* a subárvore do meio (B) muda de pai */
    x->dir = y;
    return x;
}

/* Rotação à esquerda em x: o simétrico da anterior */
No *rotacaoEsquerda(No *x) {
    No *y = x->dir;
    x->dir = y->esq;        /* a subárvore do meio (B) muda de pai */
    y->esq = x;
    return y;
}

São só três atribuições de ponteiros, e nenhuma delas toca nas chaves nem nos valores: os nós mudam de lugar levando o seu par chave-valor junto.

Teorema 1

Uma rotação aplicada a uma ABB produz uma ABB com as mesmas chaves e custa .

Demonstraçãotente antes de abrir

Pelo teorema da ordem simétrica, basta mostrar que a em-ordem não muda. Antes da rotação à direita, a em-ordem da subárvore é a de , depois , depois (a SAE de é com e ), depois , depois . Depois da rotação, é a de , depois , depois a da SAD de , que é com e : , , . A sequência é a mesma, . Fora da subárvore nada muda, e o pai de passa a apontar para , que ocupa a mesma posição na ordem que a subárvore inteira ocupava. Logo, a em-ordem da árvore toda continua crescente. A rotação à esquerda é simétrica. O custo é constante: três atribuições, qualquer que seja o tamanho das subárvores.

O que a rotação muda são as profundidades:

Proposição 1

Na rotação à direita em , todos os nós de sobem um nível, todos os de descem um nível, e os de ficam na mesma profundidade.

Demonstraçãotente antes de abrir

Seja a profundidade de antes da rotação. Antes, está em , e as raízes de e estão em ; a raiz de está em . Depois, está em e em : a raiz de , filha de , fica em (subiu um nível); a de , agora filha de , fica em (a mesma); e a de , filha de , fica em (desceu um nível). Cada subárvore se move inteira, então todos os seus nós mudam de profundidade junto com a raiz dela.

É por isso que a rotação conserta um nó com a SAE alta demais: a parte mais funda, em , sobe, à custa de , que era mais baixa. Na AVL, o caso em que a parte funda está em , que não sobe, pede duas rotações, e esse é o assunto da próxima página.

O programa abaixo monta uma árvore com a SAE mais alta, aplica uma rotação à direita na raiz (a altura cai de 3 para 2) e depois a desfaz com uma rotação à esquerda. A em-ordem é a mesma nos dois momentos.

rotacao.cC
Guarde o retorno

A rotação muda a raiz da subárvore, e quem chama precisa atualizar o ponteiro que apontava para ela: raiz = rotacaoDireita(raiz); ou r->esq = rotacaoDireita(r->esq);. É o mesmo cuidado da inserção recursiva na ABB, e esquecê-lo é o erro do Programa 2.

Panorama das árvores balanceadas

ÁrvoreComo mantém o balanceamentoAltura no pior casoOnde aparece
AVL em todo nó; rotaçõescerca de aplicações com muito mais buscas que alterações
Rubro-negracores nos nós e regras sobre elas; rotações e recoloraçõesstd::map do C++, TreeMap do Java, núcleo do Linux
2-3 e 2-3-4nós com 2, 3 (ou 4) filhos; todas as folhas no mesmo nívelbase conceitual das rubro-negras
B, B+nós com muitas chaves e muitos filhosmuito pequenabancos de dados e sistemas de arquivos

A AVL e a rubro-negra resolvem o mesmo problema com trocas diferentes: a AVL é balanceada com mais rigor e, por isso, um pouco mais baixa, o que favorece as buscas; a rubro-negra tolera mais desequilíbrio e, em troca, costuma reorganizar menos nas inserções e remoções. Nesta disciplina, estudaremos a AVL e a LLRB (left-leaning red-black tree), uma variante da rubro-negra proposta por Sedgewick em 2008, com código bem mais curto, que corresponde diretamente às árvores 2-3. As árvores B ficam para Estrutura de Dados II.

Exercícios

Encontre e corrija o erro

Programa 1

rotacao_meio.cC
Resposta

A rotação faz y->esq = NULL em vez de y->esq = x->dir: a subárvore do meio (, aqui o nó 30) é descartada. Ela some da árvore (a em-ordem não tem o 30) e o nó vaza (o AddressSanitizer acusa o vazamento). O filho direito de precisa passar para a esquerda de , antes de x->dir ser sobrescrito:

No *x = y->esq;
y->esq = x->dir;
x->dir = y;
return x;

Programa 2

rotacao_pai.cC
Resposta

A rotação em 30 é feita corretamente, mas o retorno é descartado: o 60 continua apontando para o 30, que agora é filho direito do 20 e ficou sem filhos. O 20 e o 10 desaparecem da árvore (e vazam). O ponteiro que apontava para a subárvore precisa receber a nova raiz:

raiz->esq = rotacaoDireita(raiz->esq);

Programa 3

balanceada.cC
Resposta

A função só verifica o fator da raiz, que é 0 (as duas subárvores têm altura 3), e responde "sim". Mas o critério vale para todo nó: o 30 tem . Depois de conferir a raiz, é preciso conferir as duas subárvores:

if (fb < -1 || fb > 1)
    return false;
return ehBalanceada(r->esq) && ehBalanceada(r->dir);

Implemente

Fatores de balanceamento em O(n). Grave o fator de balanceamento de todos os nós de uma árvore, na ordem simétrica, em . Os testes incluem árvores degeneradas com 40 mil nós, nas quais chamar uma função altura em cada nó não termina a tempo.

Como calcular todas as alturas de uma vez?

A altura de um nó depende das alturas dos filhos. Em que ordem os nós precisam ser processados para que as alturas dos filhos já estejam prontas?

Ver dica 1Esconder dica 1

De baixo para cima: uma função recursiva que devolve a altura da subárvore pode calcular o fator do nó com os dois valores que recebeu das chamadas nos filhos.

Ver dica 2Esconder dica 2

O fator de cada nó vai para a posição dele na ordem simétrica. Reserve essa posição (com um contador passado por ponteiro) entre as chamadas para a SAE e para a SAD, e grave o fator depois da segunda chamada.

Fatores de balanceamento em O(n)C
Executa o seu código uma vez, com os dados de entrada abaixo. Serve para experimentar; não conta como resposta.Executa o seu código em todos os casos de teste (os de exemplo e os ocultos) e mostra quais passaram.

Rotações. Implemente rotacaoDireita e rotacaoEsquerda. Os testes aplicam sequências de rotações em vários pontos de uma ABB (incluindo rotações sem subárvore do meio) e conferem a forma da árvore, a ordem das chaves e os valores. A última bateria faz 300 mil rotações aleatórias em uma árvore com 100 mil nós.

RotaçõesC
Executa o seu código uma vez, com os dados de entrada abaixo. Serve para experimentar; não conta como resposta.Executa o seu código em todos os casos de teste (os de exemplo e os ocultos) e mostra quais passaram.

Referências

  • W. Celes, R. Cerqueira e J. L. Rangel. Introdução a Estruturas de Dados: com técnicas de programação em C. 2ª ed. Grupo GEN, 2016.
  • A. Backes. Estrutura de Dados Descomplicada em Linguagem C. Elsevier, 2016.
  • R. Sedgewick e K. Wayne. Algorithms. 4ª ed. Addison-Wesley, 2011. Seção 3.3.
  • R. Sedgewick. Left-leaning Red-Black Trees. 2008.
  • D. Galles. Visualizações interativas: ABB, AVL e rubro-negra. University of San Francisco.

Nesta página