Notebooks

Arvore AVL - Pokémons

Estrutura de Dados - Aula 04 (Arvores AVL) Atividade proposta: sistema de gerenciamento de Pokémon com Arvore AVL

Kernel inativo

Estrutura de Dados — Atividade Proposta

Sistema de Gerenciamento de Pokémon com Árvore AVL

Enunciado. Implementar um sistema eficiente de gerenciamento de Pokémon utilizando uma Árvore AVL. Cada Pokémon é caracterizado por um nome e um valor de força (inteiro).

Objetivos:

  1. Implementação da Árvore AVL — os nós devem conter o nome do Pokémon e seu valor de força.
  2. Funcionalidade de Busca — buscar Pokémon pelo nome, retornando suas informações.
  3. Funcionalidade de Listagem — listar todos os Pokémon em ordem decrescente de força.
  4. Busca e Remoção — remover Pokémon pelo nome, mantendo as propriedades da AVL.

Requisitos adicionais: a árvore deve se reequilibrar automaticamente após inserções e remoções, e as operações devem manter complexidade logarítmica.

1. Análise do problema — qual é a chave da árvore?

Este é o ponto central do exercício. Uma árvore binária de busca é ordenada por uma única chave, e o enunciado pede duas coisas que competem entre si:

RequisitoChave necessáriaSe a chave for o nomeSe a chave for a força
Buscar pelo nomenomeO(log n)O(n) ✘ (varre a árvore toda)
Listar por força decrescenteforçaO(n log n) ✘ (precisa ordenar)O(n) ✔ (percurso em ordem inversa)
Remover pelo nomenomeO(log n)O(n)

Ou seja, uma única árvore não atende aos dois requisitos com custo logarítmico.

Solução adotada: dois índices AVL sobre os mesmos objetos

Mantemos duas Árvores AVL que compartilham os mesmos objetos Pokemon na memória:

  • arvore_por_nome — chave = nome normalizado → busca e remoção em O(log n);
  • arvore_por_forca — chave = (força, nome) → listagem decrescente com um percurso em ordem inversa.

A chave da segunda árvore é uma tupla porque dois Pokémon podem ter a mesma força; o nome entra como critério de desempate e garante que a chave seja única.

   +------------------------------+          +-----------------------------------+
   |   arvore_por_nome            |          |   arvore_por_forca                |
   |   chave = "pikachu"          |          |   chave = (55, "pikachu")         |
   +--------------+---------------+          +-----------------+-----------------+
                  |                                            |
                  +---------->  Pokemon("Pikachu", 55)  <-------+
                                 (o MESMO objeto na memoria)

O custo é O(n) de memória extra para os ponteiros do segundo índice — e cada inserção ou remoção passa a fazer duas operações O(log n), o que continua sendo O(log n).

Observação honesta sobre a listagem: nenhuma listagem completa pode ser mais rápida que O(n), já que é preciso escrever os n elementos na saída. O que a segunda árvore evita é o custo extra de ordenar (O(n log n)): as chaves já estão ordenadas, e um percurso em ordem inversa (direita → raiz → esquerda) devolve a lista pronta em O(n).

2. Preparação

Apenas biblioteca padrão. O decorador adicionar_metodo é um recurso didático que permite escrever um método por célula, com a explicação em Markdown logo acima. O resultado é idêntico a declarar todos os métodos dentro do corpo da classe (é assim que aparecem no arquivo .py de entrega).

import random
import math

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

print("Ambiente pronto.")

3. Objetivo 1a — a entidade Pokemon

Cada Pokémon guarda nome e força. A classe também sabe produzir as duas chaves usadas pelos índices:

  • chave_nome()"pikachu" (usa casefold() para que a busca não diferencie maiúsculas de minúsculas e para que a ordem alfabética não coloque todos os nomes maiúsculos antes);
  • chave_forca()(55, "pikachu") — tupla comparada primeiro pela força e, em caso de empate, pelo nome.
class Pokemon:
    '''Representa um Pokemon do jogo: nome e valor de forca (inteiro).'''

    def __init__(self, nome, forca):
        self.nome = nome
        self.forca = int(forca)

    def chave_nome(self):
        '''Chave usada na arvore indexada por nome (normalizada, sem diferenciar maiusculas).'''
        return self.nome.casefold()

    def chave_forca(self):
        '''Chave composta usada na arvore indexada por forca; o nome desempata forcas iguais.'''
        return (self.forca, self.chave_nome())

    def __repr__(self):
        return f"{self.nome} (forca {self.forca})"
# Teste rapido da entidade
pikachu = Pokemon("Pikachu", 55)

print(pikachu)
print("Chave para o indice por nome :", pikachu.chave_nome())
print("Chave para o indice por forca:", pikachu.chave_forca())

# Tuplas sao comparadas elemento a elemento: primeiro a forca, depois o nome
print("(55,'eevee') < (55,'pikachu') ?", (55, "eevee") < (55, "pikachu"))
print("(55,'pikachu') < (84,'charizard') ?", (55, "pikachu") < (84, "charizard"))

4. Objetivo 1b — a classe NoAVL

Diferente do nó da aula (que guardava só uma chave), aqui o nó guarda dois campos de dados:

  • chave — o que ordena a árvore (o nome, ou a tupla (força, nome));
  • valor — o objeto Pokemon associado.

Essa separação é o que permite reaproveitar a mesma classe de árvore nos dois índices.

class NoAVL:
    '''Vertice da arvore AVL: guarda a chave de ordenacao e o valor (o Pokemon) associado.'''

    def __init__(self, chave, valor):
        self.chave = chave        # o que ordena a arvore
        self.valor = valor        # o objeto Pokemon
        self.esquerda = None
        self.direita = None
        self.altura = 0           # folha tem altura 0 (contagem por arestas)

    def eh_folha(self):
        return self.esquerda is None and self.direita is None

    def quantidade_de_filhos(self):
        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!r}, valor={self.valor!r})"

5. A classe ArvoreAVL (genérica)

A árvore não sabe nada sobre Pokémon: ela apenas ordena chaves comparáveis. Isso é proposital — é o que permite usar a mesma implementação para o índice por nome (chaves str) e para o índice por força (chaves tuple).

Convenções da aula mantidas:

  • altura contada em arestas: subárvore vazia = -1, folha = 0;
  • fator de balanceamento fb = h_d(u) - h_e(u) (direita menos esquerda).
class ArvoreAVL:
    '''Arvore binaria de busca balanceada (AVL), generica em chave e valor.'''

    def __init__(self):
        self.raiz = None
        self.quantidade_de_nos = 0

    def esta_vazia(self):
        return self.raiz is None

    def __len__(self):
        return self.quantidade_de_nos

5.1 Alturas e fator de balanceamento

@adicionar_metodo(ArvoreAVL)
def altura(self, no):
    '''Altura da subarvore; subarvore vazia tem altura -1.'''
    if no is None:
        return -1
    return no.altura


@adicionar_metodo(ArvoreAVL)
def atualizar_altura(self, no):
    '''Recalcula a altura do no a partir das alturas dos filhos.'''
    no.altura = 1 + max(self.altura(no.esquerda), self.altura(no.direita))


@adicionar_metodo(ArvoreAVL)
def fator_balanceamento(self, no):
    '''fb = altura(subarvore direita) - altura(subarvore esquerda).'''
    if no is None:
        return 0
    return self.altura(no.direita) - self.altura(no.esquerda)

5.2 Rotações — os quatro casos

   CASO RR (fb = +2, filho direito pesado a direita)  ->  1 rotacao a ESQUERDA

          v1                                    v2
         /  \                                 /    \
       s1    v2         =========>          v1      v3
            /  \                           /  \    /  \
          s2    v3                       s1   s2  s3   s4
               /  \
             s3    s4


   CASO LL (fb = -2, filho esquerdo pesado a esquerda)  ->  1 rotacao a DIREITA

              v3                                 v2
             /  \                              /    \
           v2    s4       =========>         v1      v3
          /  \                              /  \    /  \
        v1    s3                          s1   s2  s3   s4
       /  \
     s1    s2


   CASO LR (fb = -2, filho esquerdo pesado a direita)  ->  gira o filho a ESQUERDA, depois o no a DIREITA
   CASO RL (fb = +2, filho direito pesado a esquerda)  ->  gira o filho a DIREITA, depois o no a ESQUERDA

Diagramas adaptados de Takenaka (2021), conforme a Aula 04.

@adicionar_metodo(ArvoreAVL)
def rotacao_esquerda(self, no):
    '''Rotacao simples a ESQUERDA - corrige o caso RR. Devolve a nova raiz da subarvore.'''
    novo_topo = no.direita
    no.direita = novo_topo.esquerda      # a subarvore do meio troca de pai
    novo_topo.esquerda = no
    self.atualizar_altura(no)
    self.atualizar_altura(novo_topo)
    return novo_topo


@adicionar_metodo(ArvoreAVL)
def rotacao_direita(self, no):
    '''Rotacao simples a DIREITA - corrige o caso LL. Devolve a nova raiz da subarvore.'''
    novo_topo = no.esquerda
    no.esquerda = novo_topo.direita      # a subarvore do meio troca de pai
    novo_topo.direita = no
    self.atualizar_altura(no)
    self.atualizar_altura(novo_topo)
    return novo_topo
@adicionar_metodo(ArvoreAVL)
def rotacao_dupla_esquerda_direita(self, no):
    '''Rotacao DUPLA - corrige o caso LR (transforma LR em LL e resolve).'''
    no.esquerda = self.rotacao_esquerda(no.esquerda)
    return self.rotacao_direita(no)


@adicionar_metodo(ArvoreAVL)
def rotacao_dupla_direita_esquerda(self, no):
    '''Rotacao DUPLA - corrige o caso RL (transforma RL em RR e resolve).'''
    no.direita = self.rotacao_direita(no.direita)
    return self.rotacao_esquerda(no)

5.3 rebalancear — a tabela de decisão

fb(nó)fb(filho)CasoAção
> +1>= 0RRrotacao_esquerda
> +1< 0RLrotacao_dupla_direita_esquerda
< -1<= 0LLrotacao_direita
< -1> 0LRrotacao_dupla_esquerda_direita
@adicionar_metodo(ArvoreAVL)
def rebalancear(self, no):
    '''Atualiza a altura e aplica a rotacao necessaria, se houver desbalanceio.'''
    self.atualizar_altura(no)
    fator = self.fator_balanceamento(no)

    if fator > 1:                                            # pesado a direita
        if self.fator_balanceamento(no.direita) >= 0:
            return self.rotacao_esquerda(no)                 # caso RR
        return self.rotacao_dupla_direita_esquerda(no)       # caso RL

    if fator < -1:                                           # pesado a esquerda
        if self.fator_balanceamento(no.esquerda) <= 0:
            return self.rotacao_direita(no)                  # caso LL
        return self.rotacao_dupla_esquerda_direita(no)       # caso LR

    return no

5.4 Inserção

Igual à da aula, com uma diferença: além da chave, carregamos o valor (o Pokémon). Se a chave já existir, o valor é atualizado em vez de duplicar o nó.

@adicionar_metodo(ArvoreAVL)
def inserir(self, chave, valor):
    '''Insere (ou atualiza) um par chave/valor. Custo O(log n).'''
    self.raiz = self.inserir_recursivo(self.raiz, chave, valor)


@adicionar_metodo(ArvoreAVL)
def inserir_recursivo(self, no, chave, valor):
    if no is None:                                   # posicao vazia encontrada
        self.quantidade_de_nos += 1
        return NoAVL(chave, valor)

    if chave < no.chave:
        no.esquerda = self.inserir_recursivo(no.esquerda, chave, valor)
    elif chave > no.chave:
        no.direita = self.inserir_recursivo(no.direita, chave, valor)
    else:
        no.valor = valor                             # chave ja existe: atualiza
        return no

    return self.rebalancear(no)                      # na volta da recursao

5.5 Objetivo 2 — Busca

Implementada de forma iterativa: gasta O(1) de memória e desce no máximo log(n) níveis, exatamente o que o enunciado pede.

@adicionar_metodo(ArvoreAVL)
def buscar_no(self, chave):
    '''Devolve o NoAVL correspondente a chave, ou None. Custo O(log n).'''
    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 buscar(self, chave):
    '''Devolve o valor (o Pokemon) associado a chave, ou None.'''
    no_encontrado = self.buscar_no(chave)
    return None if no_encontrado is None else no_encontrado.valor

5.6 Objetivo 4 — Remoção

Os três casos da aula:

  1. vértice folha — desligado do pai;
  2. vértice com um filho — o filho toma o lugar;
  3. vértice com dois filhos — copia-se chave e valor do sucessor em ordem (menor nó da subárvore direita) e remove-se o sucessor.

Na volta da recursão, todos os nós do caminho são rebalanceados.

@adicionar_metodo(ArvoreAVL)
def menor_no(self, no):
    '''Sucessor em ordem: o no mais a esquerda da subarvore.'''
    while no.esquerda is not None:
        no = no.esquerda
    return no


@adicionar_metodo(ArvoreAVL)
def remover(self, chave):
    '''Remove a chave; devolve True se ela existia. Custo O(log n).'''
    if self.buscar_no(chave) is None:
        return False
    self.raiz = self.remover_recursivo(self.raiz, chave)
    return True
@adicionar_metodo(ArvoreAVL)
def remover_recursivo(self, no, chave):
    if no is None:
        return None

    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:
        # CASO 1 - vertice folha
        if no.esquerda is None and no.direita is None:
            self.quantidade_de_nos -= 1
            return None

        # CASO 2 - vertice 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 - vertice com dois filhos (copia chave E valor do sucessor)
        sucessor = self.menor_no(no.direita)
        no.chave = sucessor.chave
        no.valor = sucessor.valor
        no.direita = self.remover_recursivo(no.direita, sucessor.chave)

    return self.rebalancear(no)

5.7 Objetivo 3 — Percursos

A chave da listagem decrescente está aqui:

PercursoOrdem de visitaResultado
em_ordemesquerda → raiz → direitachaves em ordem crescente
em_ordem_inversadireita → raiz → esquerdachaves em ordem decrescente

Aplicando em_ordem_inversa no índice por força, os Pokémon saem do mais forte para o mais fraco sem nenhuma ordenação adicional.

@adicionar_metodo(ArvoreAVL)
def em_ordem(self, no="__raiz__", resultado=None):
    '''Percurso em ordem CRESCENTE de chave. Custo O(n).'''
    if isinstance(no, str):
        no = self.raiz
    if resultado is None:
        resultado = []
    if no is not None:
        self.em_ordem(no.esquerda, resultado)
        resultado.append(no.valor)
        self.em_ordem(no.direita, resultado)
    return resultado


@adicionar_metodo(ArvoreAVL)
def em_ordem_inversa(self, no="__raiz__", resultado=None):
    '''Percurso em ordem DECRESCENTE de chave: direita -> raiz -> esquerda. Custo O(n).'''
    if isinstance(no, str):
        no = self.raiz
    if resultado is None:
        resultado = []
    if no is not None:
        self.em_ordem_inversa(no.direita, resultado)
        resultado.append(no.valor)
        self.em_ordem_inversa(no.esquerda, resultado)
    return resultado

5.8 Métodos de apoio (verificação e visualização)

@adicionar_metodo(ArvoreAVL)
def esta_balanceada(self, no="__raiz__"):
    '''Verifica se |fb| <= 1 em todos os nos.'''
    if isinstance(no, str):
        no = self.raiz
    if no is None:
        return True
    if abs(self.fator_balanceamento(no)) > 1:
        return False
    return self.esta_balanceada(no.esquerda) and self.esta_balanceada(no.direita)


@adicionar_metodo(ArvoreAVL)
def imprimir_arvore(self, titulo=None):
    '''Desenha a arvore girada 90 graus: raiz a esquerda, subarvore direita acima.'''
    if titulo is not None:
        print(titulo)
        print("-" * len(titulo))
    if self.raiz is None:
        print("(arvore vazia)\n")
        return
    self.desenhar_no(self.raiz, "")
    print()


@adicionar_metodo(ArvoreAVL)
def desenhar_no(self, no, recuo):
    if no is None:
        return
    self.desenhar_no(no.direita, recuo + "        ")
    print(f"{recuo}{no.valor}  [h={no.altura}, fb={self.fator_balanceamento(no):+d}]")
    self.desenhar_no(no.esquerda, recuo + "        ")

6. A classe GerenciadorPokemon

É a camada que o "jogo" enxerga. Ela esconde a existência dos dois índices e garante que os dois sejam sempre atualizados juntos — se um Pokémon for removido de apenas um deles, o sistema fica inconsistente.

MétodoO que fazCusto
inserir_pokemon(nome, forca)cadastra ou atualiza a forçaO(log n)
buscar_por_nome(nome)devolve o Pokémon (nome e força)O(log n)
remover_por_nome(nome)remove dos dois índicesO(log n)
listar_por_forca_decrescente()do mais forte ao mais fracoO(n)
listar_por_nome()ordem alfabéticaO(n)
class GerenciadorPokemon:
    '''Sistema de gerenciamento de Pokemon apoiado em duas Arvores AVL.'''

    def __init__(self):
        self.arvore_por_nome = ArvoreAVL()    # chave: nome normalizado
        self.arvore_por_forca = ArvoreAVL()   # chave: (forca, nome normalizado)

    def __len__(self):
        return len(self.arvore_por_nome)

6.1 Cadastro

Se o Pokémon já existir, a força é atualizada. Nesse caso é preciso remover e reinserir no índice por força: a chave (força, nome) mudou, e alterar a chave de um nó sem reposicioná-lo quebraria a ordenação da árvore.

@adicionar_metodo(GerenciadorPokemon)
def inserir_pokemon(self, nome, forca):
    '''Cadastra um Pokemon novo ou atualiza a forca de um ja existente. Custo O(log n).'''
    ja_cadastrado = self.arvore_por_nome.buscar(nome.casefold())

    if ja_cadastrado is not None:
        # a chave do indice por forca vai mudar: remove com a chave antiga...
        self.arvore_por_forca.remover(ja_cadastrado.chave_forca())
        ja_cadastrado.forca = int(forca)
        # ...e reinsere com a nova
        self.arvore_por_forca.inserir(ja_cadastrado.chave_forca(), ja_cadastrado)
        return ja_cadastrado

    pokemon = Pokemon(nome, forca)
    self.arvore_por_nome.inserir(pokemon.chave_nome(), pokemon)
    self.arvore_por_forca.inserir(pokemon.chave_forca(), pokemon)
    return pokemon

6.2 Objetivo 2 — busca por nome

@adicionar_metodo(GerenciadorPokemon)
def buscar_por_nome(self, nome):
    '''Devolve o Pokemon com esse nome (com seu valor de forca), ou None. Custo O(log n).'''
    return self.arvore_por_nome.buscar(nome.casefold())

6.3 Objetivo 4 — remoção por nome

Primeiro localizamos o Pokémon pelo nome (O(log n)) — precisamos do objeto para reconstruir a chave (força, nome) e remover também do segundo índice.

@adicionar_metodo(GerenciadorPokemon)
def remover_por_nome(self, nome):
    '''Remove o Pokemon dos dois indices. Devolve True se ele existia. Custo O(log n).'''
    pokemon = self.arvore_por_nome.buscar(nome.casefold())
    if pokemon is None:
        return False
    self.arvore_por_nome.remover(pokemon.chave_nome())
    self.arvore_por_forca.remover(pokemon.chave_forca())
    return True

6.4 Objetivo 3 — listagens

@adicionar_metodo(GerenciadorPokemon)
def listar_por_forca_decrescente(self):
    '''Lista todos os Pokemon do mais forte para o mais fraco. Custo O(n).'''
    return self.arvore_por_forca.em_ordem_inversa()


@adicionar_metodo(GerenciadorPokemon)
def listar_por_nome(self):
    '''Lista todos os Pokemon em ordem alfabetica. Custo O(n).'''
    return self.arvore_por_nome.em_ordem()


@adicionar_metodo(GerenciadorPokemon)
def indices_balanceados(self):
    '''Confere se os dois indices continuam respeitando a propriedade AVL.'''
    return self.arvore_por_nome.esta_balanceada() and self.arvore_por_forca.esta_balanceada()

7. Testes

7.1 Cadastro da equipe

gerenciador = GerenciadorPokemon()

equipe = [
    ("Pikachu", 55), ("Charizard", 84), ("Bulbasaur", 49), ("Squirtle", 48),
    ("Mewtwo", 110), ("Snorlax", 110), ("Gengar", 65), ("Eevee", 55),
    ("Dragonite", 134), ("Machamp", 130), ("Alakazam", 50), ("Onix", 45),
]

for nome, forca in equipe:
    gerenciador.inserir_pokemon(nome, forca)

print(f"Pokemon cadastrados: {len(gerenciador)}")
print(f"Altura do indice por nome .: {gerenciador.arvore_por_nome.altura(gerenciador.arvore_por_nome.raiz)}")
print(f"Altura do indice por forca : {gerenciador.arvore_por_forca.altura(gerenciador.arvore_por_forca.raiz)}")
print(f"Altura minima teorica (log2 de 12): {math.log2(12):.2f}")
print(f"Indices balanceados? {gerenciador.indices_balanceados()}")

7.2 Como ficaram as duas árvores

Repare que as mesmas 12 informações aparecem organizadas de formas completamente diferentes: o primeiro índice está ordenado alfabeticamente, o segundo por força.

gerenciador.arvore_por_nome.imprimir_arvore("INDICE POR NOME (ordem alfabetica)")
gerenciador.arvore_por_forca.imprimir_arvore("INDICE POR FORCA (ordem crescente de forca)")

7.3 Teste do Objetivo 2 — busca por nome

Inclui um nome inexistente e variações de maiúsculas/minúsculas.

for nome_procurado in ["Gengar", "pikachu", "MEWTWO", "Dragonite", "Ditto"]:
    encontrado = gerenciador.buscar_por_nome(nome_procurado)
    if encontrado is None:
        print(f"  {nome_procurado:<12} -> nao encontrado na arvore")
    else:
        print(f"  {nome_procurado:<12} -> ENCONTRADO: nome={encontrado.nome}, forca={encontrado.forca}")

7.4 Teste do Objetivo 3 — listagem em ordem decrescente de força

Note o empate entre Mewtwo e Snorlax (força 110): a chave composta resolve o desempate pelo nome, e nenhum dos dois é perdido.

ranking = gerenciador.listar_por_forca_decrescente()

print("RANKING - do mais forte ao mais fraco")
print("-" * 38)
for posicao, pokemon in enumerate(ranking, start=1):
    barra = "#" * (pokemon.forca // 5)
    print(f"  {posicao:>2}. {pokemon.nome:<12} {pokemon.forca:>4}  {barra}")

# Verificacao automatica da ordem
forcas = [pokemon.forca for pokemon in ranking]
print("\nOrdem decrescente correta?", forcas == sorted(forcas, reverse=True))
print("Todos os Pokemon aparecem?", len(ranking) == len(gerenciador))

7.5 Teste do Objetivo 4 — remoção nos três casos

Removemos um vértice com um filho, um vértice folha, um vértice com dois filhos e, por fim, um Pokémon inexistente. A cada passo verificamos se os dois índices continuam balanceados e sincronizados.

print(f"{'Pokemon':<12} {'caso':<24} {'resultado':<12} {'restam':<7} balanceado")
print("-" * 70)

descricao_do_caso = {0: "vertice folha", 1: "vertice com um filho", 2: "vertice com dois filhos"}

for nome_removido in ["Squirtle", "Onix", "Charizard", "Ditto"]:
    no_do_indice = gerenciador.arvore_por_nome.buscar_no(nome_removido.casefold())
    caso = descricao_do_caso[no_do_indice.quantidade_de_filhos()] if no_do_indice else "nao cadastrado"

    foi_removido = gerenciador.remover_por_nome(nome_removido)

    print(f"{nome_removido:<12} {caso:<24} {('removido' if foi_removido else 'inexistente'):<12} "
          f"{len(gerenciador):<7} {gerenciador.indices_balanceados()}")
gerenciador.arvore_por_nome.imprimir_arvore("INDICE POR NOME apos as remocoes")
print("Ranking atualizado:", [f"{p.nome} ({p.forca})" for p in gerenciador.listar_por_forca_decrescente()])

7.6 Consistência entre os dois índices

Um erro comum nessa modelagem é remover de um índice e esquecer do outro. Este teste garante que os dois contêm exatamente o mesmo conjunto de Pokémon.

nomes_no_indice_de_nome = sorted(p.nome for p in gerenciador.listar_por_nome())
nomes_no_indice_de_forca = sorted(p.nome for p in gerenciador.listar_por_forca_decrescente())

print("Indice por nome :", nomes_no_indice_de_nome)
print("Indice por forca:", nomes_no_indice_de_forca)
print("\nOs dois indices contem o mesmo conjunto?", nomes_no_indice_de_nome == nomes_no_indice_de_forca)
print("Mesma quantidade de nos?",
      len(gerenciador.arvore_por_nome) == len(gerenciador.arvore_por_forca))

7.7 Atualização de força

Ao treinar, um Pokémon fica mais forte — e precisa mudar de posição no ranking sem duplicar registros nem quebrar a busca por nome.

print("Antes :", [f"{p.nome}({p.forca})" for p in gerenciador.listar_por_forca_decrescente()])

gerenciador.inserir_pokemon("Pikachu", 150)   # Pikachu treinou muito

print("Depois:", [f"{p.nome}({p.forca})" for p in gerenciador.listar_por_forca_decrescente()])
print()
print("Total de Pokemon (nao pode ter duplicado):", len(gerenciador))
print("Busca por nome ainda funciona:", gerenciador.buscar_por_nome("pikachu"))
print("Indices balanceados?", gerenciador.indices_balanceados())

7.8 Teste de eficiência

Cadastramos 2.000 Pokémon com nomes gerados em ordem alfabética crescente — o pior caso para uma árvore binária de busca comum, que degeneraria em uma lista encadeada de altura 1.999.

random.seed(42)

pokedex = GerenciadorPokemon()
quantidade = 2000

for indice in range(quantidade):
    pokedex.inserir_pokemon(f"pokemon_{indice:05d}", random.randint(1, 300))

altura_avl = pokedex.arvore_por_nome.altura(pokedex.arvore_por_nome.raiz)

print(f"Pokemon cadastrados .............: {len(pokedex)}")
print(f"Altura do indice AVL ............: {altura_avl}")
print(f"Altura de uma BST degenerada ....: {quantidade - 1}")
print(f"Referencia log2({quantidade}) ...........: {math.log2(quantidade):.2f}")
print(f"Comparacoes na busca (pior caso) : ~{altura_avl + 1} em vez de ~{quantidade}")
print(f"Indices balanceados? {pokedex.indices_balanceados()}")

ranking_completo = pokedex.listar_por_forca_decrescente()
forcas = [p.forca for p in ranking_completo]
print(f"\nListagem com {len(ranking_completo)} Pokemon em ordem decrescente correta? "
      f"{forcas == sorted(forcas, reverse=True)}")
print("Top 5:", [f"{p.nome}({p.forca})" for p in ranking_completo[:5]])

7.9 Bateria final de verificações

verificacoes = {
    "Objetivo 1 - arvore mantem-se balanceada": pokedex.indices_balanceados(),
    "Objetivo 2 - busca encontra um Pokemon existente": pokedex.buscar_por_nome("pokemon_00042") is not None,
    "Objetivo 2 - busca devolve None para inexistente": pokedex.buscar_por_nome("mewthree") is None,
    "Objetivo 3 - listagem em ordem decrescente": forcas == sorted(forcas, reverse=True),
    "Objetivo 4 - remocao de existente devolve True": pokedex.remover_por_nome("pokemon_00042") is True,
    "Objetivo 4 - remocao de inexistente devolve False": pokedex.remover_por_nome("mewthree") is False,
    "Objetivo 4 - Pokemon removido nao e mais encontrado": pokedex.buscar_por_nome("pokemon_00042") is None,
    "Consistencia - indices com o mesmo tamanho": len(pokedex.arvore_por_nome) == len(pokedex.arvore_por_forca),
    "Consistencia - arvore balanceada apos remocoes": pokedex.indices_balanceados(),
}

for descricao, resultado in verificacoes.items():
    print(f"  [{'OK ' if resultado else 'FALHOU'}] {descricao}")

print("\nTodos os testes passaram!" if all(verificacoes.values()) else "\nHa testes falhando.")

8. Conclusão

OperaçãoComplexidadeJustificativa
InserçãoO(log n)duas inserções AVL, cada uma percorrendo um caminho de altura log n
Busca por nomeO(log n)descida única no índice por nome
Remoção por nomeO(log n)uma busca + duas remoções AVL
Listagem por forçaO(n)percurso em ordem inversa, sem ordenação adicional
MemóriaO(n)o segundo índice guarda ponteiros para os mesmos objetos

A propriedade AVL (|fb| ≤ 1 em todo nó) garante que a altura permaneça em O(log n) mesmo no pior caso de inserção — como demonstrado no item 7.8, com 2.000 chaves inseridas em ordem alfabética crescente a altura ficou em torno de log₂(2000) ≈ 11, e não em 1.999.

Possíveis extensões: adicionar um terceiro índice (por tipo do Pokémon), permitir consultas por faixa de força (todos entre 80 e 120, que a AVL resolve em O(log n + k)) e persistir a pokédex em arquivo com um percurso em pré-ordem.


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.