Notebooks

Arvore binaria AVL

Implementação de uma Arvore binaria AVL

Kernel inativo

Estrutura de Dados — Aula 04

Árvores AVL

Implementação completa e comentada em português, seguindo o material da aula.

Roteiro:

  1. Introdução a árvores AVL
  2. Principais conceitos de árvores AVL
  3. Rotações (RR, LL, LR e RL) com diagramas
  4. Inserção
  5. Busca
  6. Exclusão (remoção)
  7. Percursos
  8. Demonstrações práticas
  9. Aplicações de árvores AVL
  10. Exercícios propostos

Fonte dos diagramas conceituais: adaptado de Takenaka (2021), conforme a Aula 04.

1. Introdução — o que são Árvores AVL?

As Árvores AVL são uma forma especializada de árvores binárias de busca.

  • Foram desenvolvidas por Adelson-Velsky e Landis — a sigla AVL é uma homenagem aos criadores.
  • Garantem a altura balanceada, proporcionando operações mais eficientes de busca, inserção e remoção.
  • O balanceamento é alcançado através de rotações simples e duplas, mantendo a propriedade AVL:

A diferença de altura entre as subárvores esquerda e direita de cada nó é, no máximo, 1.

Como consequência dessa propriedade, a altura da árvore é sempre proporcional a log(n), e busca, inserção e remoção custam O(log n) no pior caso.

1.1 O que é a altura de uma árvore?

A altura de uma árvore é determinada pela distância mais longa da raiz até um nó folha, contabilizando o número de arestas desse caminho.

Convenções adotadas neste notebook (coerentes com a definição por arestas):

SituaçãoAltura
Árvore/subárvore vazia-1
Nó folha0
Nó interno1 + max(altura(esquerda), altura(direita))

Fator de balanceamento

Conforme a aula, o balanceamento de um nó u é obtido pela diferença entre a altura da subárvore direita e a altura da subárvore esquerda:

fb = h_d(u) - h_e(u)

Interpretação do valor de fb:

fbSignificado
-1, 0, +1balanceado (valores válidos em uma AVL)
< -1pesado à esquerda → casos LL ou LR
> +1pesado à direita → casos RR ou RL

1.2 Preparação do ambiente

Nenhuma biblioteca externa é necessária: usamos apenas a biblioteca padrão do Python.

# Importações da biblioteca padrão
from collections import deque   # usada no percurso por nível (fila)
import math                     # usada apenas nas demonstrações de complexidade

print("Ambiente pronto.")
Ambiente pronto.

Recurso didático: construindo a classe em várias células

Para que cada método fique isolado em sua própria célula (com a explicação em Markdown logo acima), usaremos um pequeno decorador auxiliar que anexa uma função a uma classe já existente.

Isso é apenas um recurso didático: o resultado final é exatamente o mesmo de escrever todos os métodos dentro do corpo da classe.

def adicionar_metodo(classe):
    '''Decorador didático: anexa a função decorada à classe informada como um método.

    Uso:
        @adicionar_metodo(ArvoreAVL)
        def meu_metodo(self, ...):
            ...
    '''
    def decorador(funcao):
        setattr(classe, funcao.__name__, funcao)
        return funcao
    return decorador


print("Decorador 'adicionar_metodo' definido.")
Decorador 'adicionar_metodo' definido.

2. Principais conceitos de Árvores AVL

ConceitoDescrição
Fator de BalanceamentoDiferença entre as alturas da subárvore direita e da subárvore esquerda de um nó. Em uma AVL, deve ser -1, 0 ou 1 para cada nó.
RotaçõesOperações fundamentais para manter a árvore balanceada. Existem quatro tipos: rotação simples à direita, rotação simples à esquerda, rotação dupla à direita e rotação dupla à esquerda.
BalanceamentoQuando uma inserção ou remoção desbalanceia a árvore, é necessário aplicar rotações para restaurar a propriedade AVL.
Altura BalanceadaGarante que a altura das subárvores de qualquer nó difira no máximo em 1.

3. A classe NoAVL (o vértice da árvore)

Cada nó armazena:

  • chave — o valor guardado no vértice;
  • esquerda e direita — referências para as subárvores (ou None);
  • altura — a altura do próprio nó, mantida atualizada a cada operação.

Guardar a altura dentro do nó evita recalculá-la percorrendo a árvore inteira, mantendo o custo das operações em O(log n).

class NoAVL:
    '''Representa um vértice (nó) de uma árvore AVL.'''

    def __init__(self, chave):
        self.chave = chave        # valor armazenado no nó
        self.esquerda = None      # subárvore esquerda (chaves menores)
        self.direita = None       # subárvore direita (chaves maiores)
        self.altura = 0           # folha recém-criada tem altura 0 (contagem por arestas)

    def eh_folha(self):
        '''Retorna True se o nó não possui filhos.'''
        return self.esquerda is None and self.direita is None

    def quantidade_de_filhos(self):
        '''Retorna quantos filhos (0, 1 ou 2) o nó possui.'''
        return (1 if self.esquerda is not None else 0) + (1 if self.direita is not None else 0)

    def __repr__(self):
        return f"NoAVL(chave={self.chave}, altura={self.altura})"

Teste rápido da classe NoAVL

no_de_teste = NoAVL(10)
no_de_teste.esquerda = NoAVL(5)

print(no_de_teste)
print("É folha?", no_de_teste.eh_folha())
print("Quantidade de filhos:", no_de_teste.quantidade_de_filhos())
print("Filho da esquerda:", no_de_teste.esquerda)
NoAVL(chave=10, altura=0)
É folha? False
Quantidade de filhos: 1
Filho da esquerda: NoAVL(chave=5, altura=0)

4. A classe ArvoreAVL

A classe guarda apenas a raiz e um contador de nós. Todos os demais comportamentos serão adicionados método a método nas próximas células.

class ArvoreAVL:
    '''Árvore binária de busca balanceada (AVL).

    Invariante mantida: para todo nó u, |h_d(u) - h_e(u)| <= 1.
    '''

    def __init__(self):
        self.raiz = None              # raiz da árvore (None = árvore vazia)
        self.quantidade_de_nos = 0    # total de chaves armazenadas

    def esta_vazia(self):
        '''Retorna True se a árvore não possui nenhum nó.'''
        return self.raiz is None

    def __len__(self):
        return self.quantidade_de_nos

4.1 Método altura(no)

Retorna a altura de uma subárvore de forma segura, inclusive quando ela é vazia.

Atenção à leitura do código: no.altura é o atributo guardado dentro do nó, enquanto arvore.altura(no) é o método da árvore. Eles convivem sem conflito porque pertencem a classes diferentes; o método existe para tratar o caso no is None.

@adicionar_metodo(ArvoreAVL)
def altura(self, no):
    '''Altura da subárvore enraizada em 'no'. Subárvore vazia tem altura -1.'''
    if no is None:
        return -1
    return no.altura

4.2 Método atualizar_altura(no)

Recalcula a altura de um nó a partir das alturas dos seus filhos. Deve ser chamado sempre que a estrutura abaixo do nó mudar (inserção, remoção ou rotação).

@adicionar_metodo(ArvoreAVL)
def atualizar_altura(self, no):
    '''Recalcula a altura do nó: 1 + a maior altura entre os filhos.'''
    altura_esquerda = self.altura(no.esquerda)
    altura_direita = self.altura(no.direita)
    no.altura = 1 + max(altura_esquerda, altura_direita)

4.3 Método fator_balanceamento(no)

Implementa diretamente a fórmula da aula:

fb = h_d(u) - h_e(u)
@adicionar_metodo(ArvoreAVL)
def fator_balanceamento(self, no):
    '''Fator de balanceamento: altura da subárvore direita menos a da esquerda.'''
    if no is None:
        return 0
    return self.altura(no.direita) - self.altura(no.esquerda)

5. Rotações

Rotações são operações locais (mexem em poucos ponteiros, custo O(1)) que reorganizam três nós e suas subárvores para restaurar o balanceamento, sem quebrar a ordem da árvore binária de busca.

Os quatro casos possíveis:

CasoSituaçãoCorreção
RR (Right-Right)Desbalanceio à direita, filho direito também pesado à direita1 rotação à esquerda
LL (Left-Left)Desbalanceio à esquerda, filho esquerdo também pesado à esquerda1 rotação à direita
LR (Left-Right)Desbalanceio à esquerda, filho esquerdo pesado à direitarotação à esquerda no filho + rotação à direita no nó
RL (Right-Left)Desbalanceio à direita, filho direito pesado à esquerdarotação à direita no filho + rotação à esquerda no nó

Nas próximas células, cada caso aparece primeiro como diagrama e depois como código.

5.1 Caso RR — rotação simples à ESQUERDA

Situação: fb(v1) = +2 e o filho direito também está pesado à direita.

        CASO RR (RIGHT RIGHT - DIREITA DIREITA)

              v1  (fb = +2)                                v2
             /  \                                        /    \
           s1    v2  (fb = +1)                         v1      v3
                /  \            ROTAÇÃO de v1         /  \    /  \
              s2    v3          PARA A ESQUERDA     s1   s2  s3   s4
                   /  \         ============>
                 s3    s4

  A subárvore s2 "troca de pai": deixa de ser filha esquerda de v2
  e passa a ser filha direita de v1 — a ordem s1 < v1 < s2 < v2 < s3 < v3 < s4
  é preservada em ambos os lados do diagrama.

Fonte: adaptado de Takenaka (2021).

@adicionar_metodo(ArvoreAVL)
def rotacao_esquerda(self, no_desbalanceado):
    '''Rotação simples à ESQUERDA — corrige o caso RR.

    Recebe o nó desbalanceado (v1) e devolve a nova raiz da subárvore (v2).
    '''
    novo_topo = no_desbalanceado.direita          # v2 sobe e vira a raiz da subárvore
    subarvore_transferida = novo_topo.esquerda    # s2 muda de pai

    # Reorganiza os ponteiros
    novo_topo.esquerda = no_desbalanceado
    no_desbalanceado.direita = subarvore_transferida

    # As alturas mudaram: atualiza de baixo para cima
    self.atualizar_altura(no_desbalanceado)
    self.atualizar_altura(novo_topo)

    return novo_topo

5.2 Caso LL — rotação simples à DIREITA

Situação: fb(v3) = -2 e o filho esquerdo também está pesado à esquerda. É o espelho exato do caso RR.

        CASO LL (LEFT LEFT - ESQUERDA ESQUERDA)

              v3  (fb = -2)                                v2
             /  \                                        /    \
           v2    s4  (fb = -1)                         v1      v3
          /  \                  ROTAÇÃO de v3         /  \    /  \
        v1    s3                PARA A DIREITA      s1   s2  s3   s4
       /  \                     ============>
     s1    s2

  Aqui quem troca de pai é s3: deixa de ser filha direita de v2
  e passa a ser filha esquerda de v3.

Fonte: adaptado de Takenaka (2021).

@adicionar_metodo(ArvoreAVL)
def rotacao_direita(self, no_desbalanceado):
    '''Rotação simples à DIREITA — corrige o caso LL.

    Recebe o nó desbalanceado (v3) e devolve a nova raiz da subárvore (v2).
    '''
    novo_topo = no_desbalanceado.esquerda         # v2 sobe e vira a raiz da subárvore
    subarvore_transferida = novo_topo.direita     # s3 muda de pai

    # Reorganiza os ponteiros
    novo_topo.direita = no_desbalanceado
    no_desbalanceado.esquerda = subarvore_transferida

    # As alturas mudaram: atualiza de baixo para cima
    self.atualizar_altura(no_desbalanceado)
    self.atualizar_altura(novo_topo)

    return novo_topo

5.3 Caso LR — rotação DUPLA à esquerda e à direita

Situação: fb(v3) = -2, mas o filho esquerdo está pesado à direita. Uma única rotação não resolve: é preciso primeiro transformar o caso LR em um caso LL.

        CASO LR (LEFT RIGHT - ESQUERDA DIREITA)

           v3 (fb = -2)                    v3  (agora é CASO LL)              v2
          /  \                            /  \                              /    \
        v1    s4                        v2    s4                          v1      v3
       /  \          Girar v1          /  \            Girar v3          /  \    /  \
     s1    v2        para           v1    s3           para            s1   s2  s3   s4
          /  \       ESQUERDA      /  \                DIREITA
        s2    s3     ========>   s1    s2              ========>

  Passo 1: rotação à esquerda em v1 (o filho esquerdo)  -> vira o caso LL
  Passo 2: rotação à direita em v3 (o nó desbalanceado) -> restaura o equilíbrio

Fonte: adaptado de Takenaka (2021).

@adicionar_metodo(ArvoreAVL)
def rotacao_dupla_esquerda_direita(self, no_desbalanceado):
    '''Rotação DUPLA à esquerda e à direita — corrige o caso LR.

    Passo 1: rotação à esquerda no filho esquerdo (transforma LR em LL).
    Passo 2: rotação à direita no próprio nó desbalanceado.
    '''
    no_desbalanceado.esquerda = self.rotacao_esquerda(no_desbalanceado.esquerda)
    return self.rotacao_direita(no_desbalanceado)

5.4 Caso RL — rotação DUPLA à direita e à esquerda

Situação: fb(v1) = +2, mas o filho direito está pesado à esquerda. Espelho do caso LR: primeiro transformamos o caso RL em um caso RR.

        CASO RL (RIGHT LEFT - DIREITA ESQUERDA)

           v1 (fb = +2)                    v1  (agora é CASO RR)              v2
          /  \                            /  \                              /    \
        s1    v3                        s1    v2                          v1      v3
             /  \      Girar v3              /  \       Girar v1         /  \    /  \
           v2    s4    para                s2    v3     para           s1   s2  s3   s4
          /  \         DIREITA                  /  \    ESQUERDA
        s2    s3       ========>              s3    s4  ========>

  Passo 1: rotação à direita em v3 (o filho direito)     -> vira o caso RR
  Passo 2: rotação à esquerda em v1 (o nó desbalanceado) -> restaura o equilíbrio

Fonte: adaptado de Takenaka (2021).

@adicionar_metodo(ArvoreAVL)
def rotacao_dupla_direita_esquerda(self, no_desbalanceado):
    '''Rotação DUPLA à direita e à esquerda — corrige o caso RL.

    Passo 1: rotação à direita no filho direito (transforma RL em RR).
    Passo 2: rotação à esquerda no próprio nó desbalanceado.
    '''
    no_desbalanceado.direita = self.rotacao_direita(no_desbalanceado.direita)
    return self.rotacao_esquerda(no_desbalanceado)

5.5 Método rebalancear(no) — a tabela de decisão

Este é o cérebro da AVL: depois de qualquer inserção ou remoção, ele atualiza a altura do nó, consulta o fator de balanceamento e escolhe a rotação correta.

fb(no)fb(filho)CasoAção
> +1>= 0 (filho direito)RRrotacao_esquerda
> +1< 0 (filho direito)RLrotacao_dupla_direita_esquerda
< -1<= 0 (filho esquerdo)LLrotacao_direita
< -1> 0 (filho esquerdo)LRrotacao_dupla_esquerda_direita
entre -1 e +1balanceadonenhuma

O caso fb(filho) == 0 só ocorre em remoções e é tratado como rotação simples.

@adicionar_metodo(ArvoreAVL)
def rebalancear(self, no):
    '''Atualiza a altura do nó e aplica a rotação necessária, se houver desbalanceio.

    Devolve a (possivelmente nova) raiz da subárvore.
    '''
    self.atualizar_altura(no)
    fator = self.fator_balanceamento(no)

    # Pesado à DIREITA
    if fator > 1:
        if self.fator_balanceamento(no.direita) >= 0:
            return self.rotacao_esquerda(no)                     # caso RR
        else:
            return self.rotacao_dupla_direita_esquerda(no)       # caso RL

    # Pesado à ESQUERDA
    if fator < -1:
        if self.fator_balanceamento(no.esquerda) <= 0:
            return self.rotacao_direita(no)                      # caso LL
        else:
            return self.rotacao_dupla_esquerda_direita(no)       # caso LR

    # Já está balanceado (fb em {-1, 0, +1})
    return no

6. Inserção em Árvores AVL

Após a adição de um novo elemento, é necessário atualizar as alturas de todos os nós afetados e verificar se a árvore mantém as propriedades de uma AVL. Se a estrutura não estiver conforme os critérios de uma AVL, torna-se essencial realizar rotações específicas para restaurar o balanceamento.

Algoritmo:

  1. Inserir a chave como em uma árvore binária de busca comum (descendo até uma posição vazia).
  2. Ao voltar da recursão, atualizar a altura de cada nó do caminho.
  3. Ainda na volta, verificar o fator de balanceamento e aplicar a rotação necessária.

Cada tipo de inserção é determinado pelo fator de balanceamento dos nós envolvidos na operação.

Custo: O(log n) — em uma inserção, no máximo uma rotação (simples ou dupla) é necessária.

@adicionar_metodo(ArvoreAVL)
def inserir(self, chave):
    '''Insere uma chave na árvore, mantendo-a balanceada. Chaves duplicadas são ignoradas.'''
    self.raiz = self.inserir_recursivo(self.raiz, chave)


@adicionar_metodo(ArvoreAVL)
def inserir_recursivo(self, no, chave):
    '''Função auxiliar recursiva da inserção. Devolve a raiz da subárvore atualizada.'''
    # 1) Caso base: encontramos a posição vazia -> cria o novo nó
    if no is None:
        self.quantidade_de_nos += 1
        return NoAVL(chave)

    # 2) Descida da árvore binária de busca
    if chave < no.chave:
        no.esquerda = self.inserir_recursivo(no.esquerda, chave)
    elif chave > no.chave:
        no.direita = self.inserir_recursivo(no.direita, chave)
    else:
        return no   # chave já existe: nada muda

    # 3) Na volta da recursão: atualiza altura e rebalanceia
    return self.rebalancear(no)

7. Busca

A busca é idêntica à de uma árvore binária de busca comum — o balanceamento não muda a lógica, apenas garante que o caminho percorrido tenha no máximo log(n) níveis.

@adicionar_metodo(ArvoreAVL)
def buscar(self, chave):
    '''Procura uma chave e devolve o NoAVL correspondente, ou None se não existir.'''
    no_atual = self.raiz
    while no_atual is not None:
        if chave == no_atual.chave:
            return no_atual
        elif chave < no_atual.chave:
            no_atual = no_atual.esquerda
        else:
            no_atual = no_atual.direita
    return None


@adicionar_metodo(ArvoreAVL)
def contem(self, chave):
    '''Retorna True se a chave estiver presente na árvore.'''
    return self.buscar(chave) is not None

8. Tipos de Exclusões em Árvores AVL

As exclusões em árvores AVL removem elementos da estrutura, mantendo-a balanceada. Assim como nas inserções, existem os seguintes casos:

  1. Remoção de um vértice folha — basta desligá-lo do pai.
  2. Remoção de um vértice com um filho — o filho ocupa o lugar do pai removido.
  3. Remoção de um vértice com dois filhos — copia-se a chave do sucessor em ordem (o menor nó da subárvore direita) para o nó atual e remove-se o sucessor, que sempre recai no caso 1 ou 2.

Cada tipo de exclusão é determinado pelo fator de balanceamento dos nós envolvidos na operação.

Diferença importante em relação à inserção: na remoção, uma rotação pode diminuir a altura da subárvore e propagar o desbalanceio para cima. Por isso o rebalanceamento é aplicado em todos os nós do caminho de volta — podem ser necessárias até O(log n) rotações.

8.1 Método auxiliar menor_no(no)

Encontra o sucessor em ordem: o nó mais à esquerda de uma subárvore.

@adicionar_metodo(ArvoreAVL)
def menor_no(self, no):
    '''Devolve o nó de menor chave da subárvore (o mais à esquerda).'''
    no_atual = no
    while no_atual.esquerda is not None:
        no_atual = no_atual.esquerda
    return no_atual


@adicionar_metodo(ArvoreAVL)
def maior_no(self, no):
    '''Devolve o nó de maior chave da subárvore (o mais à direita).'''
    no_atual = no
    while no_atual.direita is not None:
        no_atual = no_atual.direita
    return no_atual

8.2 Método remover(chave)

@adicionar_metodo(ArvoreAVL)
def remover(self, chave):
    '''Remove uma chave da árvore, mantendo-a balanceada.'''
    self.raiz = self.remover_recursivo(self.raiz, chave)


@adicionar_metodo(ArvoreAVL)
def remover_recursivo(self, no, chave):
    '''Função auxiliar recursiva da remoção. Devolve a raiz da subárvore atualizada.'''
    # 1) Chave não encontrada
    if no is None:
        return None

    # 2) Descida da árvore binária de busca
    if chave < no.chave:
        no.esquerda = self.remover_recursivo(no.esquerda, chave)
    elif chave > no.chave:
        no.direita = self.remover_recursivo(no.direita, chave)
    else:
        # 3) Encontrou o nó a ser removido

        # CASO 1 - vértice folha
        if no.esquerda is None and no.direita is None:
            self.quantidade_de_nos -= 1
            return None

        # CASO 2 - vértice com apenas um filho
        if no.esquerda is None:
            self.quantidade_de_nos -= 1
            return no.direita
        if no.direita is None:
            self.quantidade_de_nos -= 1
            return no.esquerda

        # CASO 3 - vértice com dois filhos:
        # copia a chave do sucessor em ordem e remove o sucessor da subárvore direita
        sucessor = self.menor_no(no.direita)
        no.chave = sucessor.chave
        no.direita = self.remover_recursivo(no.direita, sucessor.chave)

    # 4) Na volta da recursão: atualiza altura e rebalanceia
    return self.rebalancear(no)

9. Percursos (travessias)

PercursoOrdem de visitaUso típico
Em ordem (in-order)esquerda → raiz → direitadevolve as chaves ordenadas
Pré-ordem (pre-order)raiz → esquerda → direitacopiar/serializar a árvore
Pós-ordem (post-order)esquerda → direita → raizliberar/destruir a árvore
Por nível (BFS)nível a nível, da raiz às folhasvisualizar a forma da árvore
@adicionar_metodo(ArvoreAVL)
def percorrer_em_ordem(self, no="__raiz__", resultado=None):
    '''Percurso EM ORDEM: esquerda -> raiz -> direita (devolve a lista ordenada).'''
    if isinstance(no, str) and no == "__raiz__":
        no = self.raiz
    if resultado is None:
        resultado = []
    if no is not None:
        self.percorrer_em_ordem(no.esquerda, resultado)
        resultado.append(no.chave)
        self.percorrer_em_ordem(no.direita, resultado)
    return resultado
@adicionar_metodo(ArvoreAVL)
def percorrer_pre_ordem(self, no="__raiz__", resultado=None):
    '''Percurso PRÉ-ORDEM: raiz -> esquerda -> direita.'''
    if isinstance(no, str) and no == "__raiz__":
        no = self.raiz
    if resultado is None:
        resultado = []
    if no is not None:
        resultado.append(no.chave)
        self.percorrer_pre_ordem(no.esquerda, resultado)
        self.percorrer_pre_ordem(no.direita, resultado)
    return resultado


@adicionar_metodo(ArvoreAVL)
def percorrer_pos_ordem(self, no="__raiz__", resultado=None):
    '''Percurso PÓS-ORDEM: esquerda -> direita -> raiz.'''
    if isinstance(no, str) and no == "__raiz__":
        no = self.raiz
    if resultado is None:
        resultado = []
    if no is not None:
        self.percorrer_pos_ordem(no.esquerda, resultado)
        self.percorrer_pos_ordem(no.direita, resultado)
        resultado.append(no.chave)
    return resultado
@adicionar_metodo(ArvoreAVL)
def percorrer_por_nivel(self):
    '''Percurso POR NÍVEL (busca em largura), usando uma fila.

    Devolve uma lista de listas: uma lista de chaves para cada nível da árvore.
    '''
    if self.raiz is None:
        return []

    niveis = []
    fila = deque([self.raiz])

    while fila:
        chaves_do_nivel = []
        for _ in range(len(fila)):          # processa exatamente um nível por vez
            no_atual = fila.popleft()
            chaves_do_nivel.append(no_atual.chave)
            if no_atual.esquerda is not None:
                fila.append(no_atual.esquerda)
            if no_atual.direita is not None:
                fila.append(no_atual.direita)
        niveis.append(chaves_do_nivel)

    return niveis

10. Visualização da árvore

O método imprimir_arvore() desenha a árvore girada 90° à esquerda: a raiz aparece à esquerda, a subárvore direita em cima e a esquerda embaixo. Cada nó mostra sua altura (h) e seu fator de balanceamento (fb).

@adicionar_metodo(ArvoreAVL)
def imprimir_arvore(self, titulo=None):
    '''Imprime a árvore girada 90°: raiz à esquerda, subárvore direita acima.'''
    if titulo is not None:
        print(titulo)
        print("-" * len(titulo))
    if self.raiz is None:
        print("(árvore vazia)\n")
        return
    self.desenhar_no(self.raiz, "")
    print()


@adicionar_metodo(ArvoreAVL)
def desenhar_no(self, no, recuo):
    '''Função auxiliar recursiva do desenho da árvore.'''
    if no is None:
        return
    self.desenhar_no(no.direita, recuo + "        ")
    print(f"{recuo}{no.chave}  [h={no.altura}, fb={self.fator_balanceamento(no):+d}]")
    self.desenhar_no(no.esquerda, recuo + "        ")

10.1 Validador da propriedade AVL

Método de apoio para conferir, a qualquer momento, se as duas propriedades (ordem da árvore binária de busca e balanceamento AVL) continuam válidas.

@adicionar_metodo(ArvoreAVL)
def esta_balanceada(self):
    '''Verifica se a árvore respeita a propriedade AVL em todos os nós.'''
    def verificar(no):
        if no is None:
            return True
        if abs(self.fator_balanceamento(no)) > 1:
            return False
        return verificar(no.esquerda) and verificar(no.direita)
    return verificar(self.raiz)


@adicionar_metodo(ArvoreAVL)
def eh_arvore_de_busca_valida(self):
    '''Verifica se as chaves estão em ordem crescente no percurso em ordem.'''
    chaves = self.percorrer_em_ordem()
    return all(chaves[i] < chaves[i + 1] for i in range(len(chaves) - 1))

11. Demonstrações práticas

11.1 Caso RR — inserindo 10, 20, 30

arvore_rr = ArvoreAVL()

for chave in [10, 20, 30]:
    arvore_rr.inserir(chave)
    print(f"Após inserir {chave}: {arvore_rr.percorrer_por_nivel()}")

print()
arvore_rr.imprimir_arvore("Resultado (caso RR -> rotação à esquerda)")
print("Raiz:", arvore_rr.raiz.chave, "| Balanceada?", arvore_rr.esta_balanceada())
Após inserir 10: [[10]]
Após inserir 20: [[10], [20]]
Após inserir 30: [[20], [10, 30]]
Resultado (caso RR -> rotação à esquerda)
-----------------------------------------
        30  [h=0, fb=+0]
20  [h=1, fb=+0]
        10  [h=0, fb=+0]
Raiz: 20 | Balanceada? True

11.2 Caso LL — inserindo 30, 20, 10

arvore_ll = ArvoreAVL()

for chave in [30, 20, 10]:
    arvore_ll.inserir(chave)
    print(f"Após inserir {chave}: {arvore_ll.percorrer_por_nivel()}")

print()
arvore_ll.imprimir_arvore("Resultado (caso LL -> rotação à direita)")
print("Raiz:", arvore_ll.raiz.chave, "| Balanceada?", arvore_ll.esta_balanceada())
Após inserir 30: [[30]]
Após inserir 20: [[30], [20]]
Após inserir 10: [[20], [10, 30]]
Resultado (caso LL -> rotação à direita)
----------------------------------------
        30  [h=0, fb=+0]
20  [h=1, fb=+0]
        10  [h=0, fb=+0]
Raiz: 20 | Balanceada? True

11.3 Caso LR — inserindo 30, 10, 20

arvore_lr = ArvoreAVL()

for chave in [30, 10, 20]:
    arvore_lr.inserir(chave)
    print(f"Após inserir {chave}: {arvore_lr.percorrer_por_nivel()}")

print()
arvore_lr.imprimir_arvore("Resultado (caso LR -> rotação dupla esquerda/direita)")
print("Raiz:", arvore_lr.raiz.chave, "| Balanceada?", arvore_lr.esta_balanceada())
Após inserir 30: [[30]]
Após inserir 10: [[30], [10]]
Após inserir 20: [[20], [10, 30]]
Resultado (caso LR -> rotação dupla esquerda/direita)
-----------------------------------------------------
        30  [h=0, fb=+0]
20  [h=1, fb=+0]
        10  [h=0, fb=+0]
Raiz: 20 | Balanceada? True

11.4 Caso RL — inserindo 10, 30, 20

arvore_rl = ArvoreAVL()

for chave in [10, 30, 20]:
    arvore_rl.inserir(chave)
    print(f"Após inserir {chave}: {arvore_rl.percorrer_por_nivel()}")

print()
arvore_rl.imprimir_arvore("Resultado (caso RL -> rotação dupla direita/esquerda)")
print("Raiz:", arvore_rl.raiz.chave, "| Balanceada?", arvore_rl.esta_balanceada())

11.5 Construção passo a passo com várias chaves

Acompanhe a árvore se reorganizando a cada inserção.

arvore = ArvoreAVL()
chaves_para_inserir = [50, 25, 75, 10, 35, 60, 90, 5, 15, 30, 40, 1]

for chave in chaves_para_inserir:
    arvore.inserir(chave)
    print(f"Inserida a chave {chave:>3} | altura da árvore = {arvore.altura(arvore.raiz)} "
          f"| níveis = {arvore.percorrer_por_nivel()}")

print()
arvore.imprimir_arvore("Árvore final")

11.6 Percursos e busca na árvore construída

print("Em ordem  :", arvore.percorrer_em_ordem())
print("Pré-ordem :", arvore.percorrer_pre_ordem())
print("Pós-ordem :", arvore.percorrer_pos_ordem())
print("Por nível :", arvore.percorrer_por_nivel())
print()

for chave_procurada in [35, 99]:
    encontrado = arvore.buscar(chave_procurada)
    if encontrado is not None:
        print(f"Chave {chave_procurada}: ENCONTRADA -> {encontrado}")
    else:
        print(f"Chave {chave_procurada}: não encontrada")

print()
print("Total de nós:", len(arvore))
print("Ordem válida?", arvore.eh_arvore_de_busca_valida(), "| Balanceada?", arvore.esta_balanceada())

11.7 Exemplo de remoção da aula

A aula apresenta a árvore abaixo e pede a remoção do nó 2 (um vértice folha):

        4                                4                             6
       / \        Remover o nó "2"        \      Operação de          /   \
      2   6       ==============>          6     balanceamento      4     7
           \                                \    ==============>
            7                                7

Repare que, após a remoção, o nó 4 fica com fb = +2 e seu filho direito com fb = +1: é o caso RR, corrigido por uma rotação simples à esquerda.

arvore_exemplo = ArvoreAVL()
for chave in [4, 2, 6, 7]:
    arvore_exemplo.inserir(chave)

arvore_exemplo.imprimir_arvore("Antes da remoção")

arvore_exemplo.remover(2)
arvore_exemplo.imprimir_arvore('Depois de remover o nó "2" (já rebalanceada)')

print("Por nível:", arvore_exemplo.percorrer_por_nivel())
print("Balanceada?", arvore_exemplo.esta_balanceada())

11.8 Remoções nos três casos

Removendo, da árvore construída no item 11.5, um vértice folha, um com um filho e um com dois filhos.

for chave_removida in [1, 5, 25]:
    quantidade_de_filhos = arvore.buscar(chave_removida).quantidade_de_filhos()
    arvore.remover(chave_removida)
    print(f"Removida a chave {chave_removida:>3} (tinha {quantidade_de_filhos} filho(s)) "
          f"| altura = {arvore.altura(arvore.raiz)} | balanceada = {arvore.esta_balanceada()}")

print()
arvore.imprimir_arvore("Árvore após as remoções")
print("Em ordem:", arvore.percorrer_em_ordem())

11.9 Por que o balanceamento importa?

Inserindo chaves já ordenadas (o pior caso para uma árvore binária de busca comum), uma BST não balanceada degeneraria em uma lista encadeada, com altura n - 1. A AVL mantém a altura próxima de log2(n).

arvore_grande = ArvoreAVL()
quantidade = 1000

for chave in range(1, quantidade + 1):   # inserção em ordem crescente = pior caso
    arvore_grande.inserir(chave)

altura_avl = arvore_grande.altura(arvore_grande.raiz)
altura_bst_degenerada = quantidade - 1

print(f"Chaves inseridas (em ordem crescente): {quantidade}")
print(f"Altura da AVL...................: {altura_avl}")
print(f"Altura de uma BST degenerada....: {altura_bst_degenerada}")
print(f"Referência log2({quantidade}).............: {math.log2(quantidade):.2f}")
print(f"Ainda balanceada?...............: {arvore_grande.esta_balanceada()}")
print()
print(f"Comparações no pior caso -> AVL: ~{altura_avl + 1} | BST degenerada: ~{quantidade}")

12. Aplicações de Árvores AVL

As Árvores AVL são uma poderosa estrutura de dados que oferece um equilíbrio entre eficiência e simplicidade. Seu algoritmo de balanceamento garante operações de busca, inserção e remoção em tempo logarítmico.

Entre suas diversas aplicações:

  • Bancos de dados — índices que exigem busca por faixa e ordenação;
  • Compiladores — tabelas de símbolos;
  • Sistemas de arquivos — indexação de diretórios e metadados;
  • Jogos eletrônicos — estruturas de cenário e detecção de colisões.

Resumo de complexidade

OperaçãoTempo (pior caso)Espaço
BuscaO(log n)O(1) iterativa
InserçãoO(log n) — no máximo 1 rotaçãoO(log n) (pilha de recursão)
RemoçãoO(log n) — até O(log n) rotaçõesO(log n)
Percurso completoO(n)O(n)

13. Exercícios propostos

"Não se esqueça de praticar: use a atividade prática proposta para exercitar o que foi visto."

  1. Rotações na mão. Insira, nesta ordem, as chaves [15, 10, 20, 8, 12, 5] desenhando a árvore no papel a cada passo. Identifique qual caso (LL, RR, LR ou RL) ocorreu e em qual nó. Depois confira com o código.
  2. Contagem de rotações. Modifique a classe para contar quantas rotações de cada tipo foram executadas (dica: crie um dicionário self.contador_de_rotacoes).
  3. Altura mínima e máxima. Descubra experimentalmente qual o menor número de nós necessário para que uma AVL tenha altura h = 4. Compare com a sequência de Fibonacci.
  4. Predecessor. Implemente remover_com_predecessor, usando o maior nó da subárvore esquerda no lugar do sucessor. O resultado final é o mesmo? A árvore fica igual?
  5. Chaves repetidas. Adapte a classe para aceitar chaves duplicadas, armazenando um contador de ocorrências em cada nó.
  6. Percurso em ordem iterativo. Reescreva percorrer_em_ordem sem recursão, usando uma pilha.
  7. Comparação empírica. Implemente uma BST comum (sem balanceamento) e compare a altura das duas estruturas para 10.000 chaves aleatórias e para 10.000 chaves ordenadas.
# Espaço para os exercícios
# Exercício 1 - confira aqui a sua resposta feita no papel:

arvore_exercicio = ArvoreAVL()
for chave in [15, 10, 20, 8, 12, 5]:
    arvore_exercicio.inserir(chave)
    print(f"Após inserir {chave:>2}: {arvore_exercicio.percorrer_por_nivel()}")

print()
arvore_exercicio.imprimir_arvore("Sua árvore")

Referências

  • Aula 04 — Estrutura de Dados: Árvores AVL.
  • Diagramas de rotação adaptados de Takenaka (2021).
  • ADELSON-VELSKY, G. M.; LANDIS, E. M. An algorithm for the organization of information, 1962.

Execução em Python roda no seu navegador via Pyodide (WebAssembly). Pacotes como numpy, pandas e matplotlib são suportados; outros exigem wheels puro-Python via micropip.