Arvore binaria AVL
Implementação de uma Arvore binaria AVL
Implementação completa e comentada em português, seguindo o material da aula.
Roteiro:
Fonte dos diagramas conceituais: adaptado de Takenaka (2021), conforme a Aula 04.
As Árvores AVL são uma forma especializada de árvores binárias de busca.
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.
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ção | Altura |
|---|---|
| Árvore/subárvore vazia | -1 |
| Nó folha | 0 |
| Nó interno | 1 + max(altura(esquerda), altura(direita)) |
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:
fb | Significado |
|---|---|
-1, 0, +1 | Nó balanceado (valores válidos em uma AVL) |
< -1 | Nó pesado à esquerda → casos LL ou LR |
> +1 | Nó pesado à direita → casos RR ou RL |
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.
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.
| Conceito | Descrição |
|---|---|
| Fator de Balanceamento | Diferenç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ções | Operaçõ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. |
| Balanceamento | Quando uma inserção ou remoção desbalanceia a árvore, é necessário aplicar rotações para restaurar a propriedade AVL. |
| Altura Balanceada | Garante que a altura das subárvores de qualquer nó difira no máximo em 1. |
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})"NoAVLno_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)
ArvoreAVLA 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_nosaltura(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ó, enquantoarvore.altura(no)é o método da árvore. Eles convivem sem conflito porque pertencem a classes diferentes; o método existe para tratar o casono 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.alturaatualizar_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)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)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:
| Caso | Situação | Correção |
|---|---|---|
| RR (Right-Right) | Desbalanceio à direita, filho direito também pesado à direita | 1 rotação à esquerda |
| LL (Left-Left) | Desbalanceio à esquerda, filho esquerdo também pesado à esquerda | 1 rotação à direita |
| LR (Left-Right) | Desbalanceio à esquerda, filho esquerdo pesado à direita | rotação à esquerda no filho + rotação à direita no nó |
| RL (Right-Left) | Desbalanceio à direita, filho direito pesado à esquerda | rotaçã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.
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_topoSituaçã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_topoSituaçã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)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)rebalancear(no) — a tabela de decisãoEste é 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) | Caso | Ação |
|---|---|---|---|
> +1 | >= 0 (filho direito) | RR | rotacao_esquerda |
> +1 | < 0 (filho direito) | RL | rotacao_dupla_direita_esquerda |
< -1 | <= 0 (filho esquerdo) | LL | rotacao_direita |
< -1 | > 0 (filho esquerdo) | LR | rotacao_dupla_esquerda_direita |
entre -1 e +1 | — | balanceado | nenhuma |
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 noApó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:
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)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 NoneAs exclusões em árvores AVL removem elementos da estrutura, mantendo-a balanceada. Assim como nas inserções, existem os seguintes casos:
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.
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_atualremover(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)| Percurso | Ordem de visita | Uso típico |
|---|---|---|
| Em ordem (in-order) | esquerda → raiz → direita | devolve as chaves ordenadas |
| Pré-ordem (pre-order) | raiz → esquerda → direita | copiar/serializar a árvore |
| Pós-ordem (post-order) | esquerda → direita → raiz | liberar/destruir a árvore |
| Por nível (BFS) | nível a nível, da raiz às folhas | visualizar 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 niveisO 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 + " ")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))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
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
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
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())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")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())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())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())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}")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:
| Operação | Tempo (pior caso) | Espaço |
|---|---|---|
| Busca | O(log n) | O(1) iterativa |
| Inserção | O(log n) — no máximo 1 rotação | O(log n) (pilha de recursão) |
| Remoção | O(log n) — até O(log n) rotações | O(log n) |
| Percurso completo | O(n) | O(n) |
"Não se esqueça de praticar: use a atividade prática proposta para exercitar o que foi visto."
[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.self.contador_de_rotacoes).h = 4. Compare com a sequência de Fibonacci.remover_com_predecessor, usando o maior nó da subárvore esquerda
no lugar do sucessor. O resultado final é o mesmo? A árvore fica igual?percorrer_em_ordem sem recursão, usando uma pilha.# 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")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.