Heaps Binárias em Python OO

Guia completo para o TP1: implementação de Floyd O(n), remoção arbitrária, instrumentação empírica e transposição de paradigmas Java/C#.

Por Fábio Linhares • Instituto Infnet

BASE
Introdução e Escopo do TP1
Alinhar a teoria de Heaps Máximas aos requisitos implementacionais do teste.
Neste TP, o desafio é implementar uma Heap Binária Máxima do zero. O objetivo central não é apenas "fazer funcionar", mas atuar como um engenheiro: modelar o problema, justificar a corretude através de invariantes, estimar o custo assintótico e validar o desempenho por evidência empírica.
Nota de Transposição: embora você já domine Orientação a Objetos em Java ou .NET, aqui você deve aplicar esses conceitos à sintaxe Python. Encapsular o estado (o array) em um objeto BinaryHeap é crítico para garantir que operações externas não corrompam a estrutura.
  • Invariante de Estrutura: Árvore binária quase completa representada em array.
  • Invariante de Ordem: Em Max-Heap, Pai ≥ Filho para todo nó.
  • Eficiência: Operações básicas em O(log n) e construção em O(n).
EX.01
Motivação Computacional e Escolha da Heap
Justificar o uso da heap em cenários de priorização dinâmica real.
Problema: Gerenciamento de Prioridade em Triagem Hospitalar. Pacientes com gravidade "Emergência" devem ser atendidos antes de "Urgência", independentemente da ordem de chegada.
Justificativa Técnica: Diferente de uma lista ordenada (inserção O(n)) ou de uma lista não ordenada (busca do máximo O(n)), a heap oferece o equilíbrio ideal: inserção e remoção do máximo em O(log n), mantendo a latência baixa mesmo com alta taxa de novos pacientes.
EstruturaInserçãoExtração do MáximoBusca Arbitrária
Lista não ordenadaO(1)O(n)O(n)
Lista ordenadaO(n)O(1)O(n)
BST / AVLO(log n)O(log n)O(log n)
Heap máximaO(log n)O(log n)O(n)
EX.02
Estruturação de Heap Binária em Python
Traduzir a estrutura lógica de árvore para a representação física de array.
A heap binária é armazenada em uma lista onde as relações pai-filho são calculadas via índices. Isso elimina o overhead de ponteiros e otimiza o cache da CPU.
class BinaryHeap:
    def __init__(self):
        self.heap = []
        self.comparacoes = 0
        self.trocas = 0

    def _pai(self, i): return (i - 1) // 2
    def _filho_esq(self, i): return 2 * i + 1
    def _filho_dir(self, i): return 2 * i + 2

    def _find_index(self, valor):
        # Busca linear instrumentada para auditoria real de custo O(n)
        for i, atual in enumerate(self.heap):
            self.comparacoes += 1
            if atual == valor: return i
        return -1
EX.03
Inserção com Registro de Trocas (Sift-up)
Implementar a escalada do novo nó enquanto registra evidências de auditoria.
Para o TP1, é obrigatório registrar cada troca realizada e contar as comparações. Isso permite provar que o algoritmo seguiu o caminho de custo O(log n).
def insert(self, valor):
    self.heap.append(valor)
    self._sift_up(len(self.heap) - 1)

def _sift_up(self, i):
    while i > 0:
        p = self._pai(i)
        self.comparacoes += 1
        if self.heap[i] > self.heap[p]:
            print(f"Swap: {self.heap[i]} <-> {self.heap[p]}")
            self.heap[i], self.heap[p] = self.heap[p], self.heap[i]
            self.trocas += 1
            i = p
        else:
            break
EX.04
Remoção do Elemento Raiz (extract-max)
Garantir a manutenção da propriedade de heap após a extração do máximo.
Ao remover a raiz, trocamos o topo pelo último elemento e aplicamos o sift-down para que o novo topo desça até sua posição correta.
def extract_max(self):
    if not self.heap:
        return None
    if len(self.heap) == 1:
        return self.heap.pop()

    max_val = self.heap[0]
    self.heap[0] = self.heap.pop()
    self._sift_down(0)
    return max_val

def _sift_down(self, i):
    n = len(self.heap)
    while True:
        maior = i
        esq = self._filho_esq(i)
        dir = self._filho_dir(i)

        if esq < n:
            self.comparacoes += 1
            if self.heap[esq] > self.heap[maior]:
                maior = esq

        if dir < n:
            self.comparacoes += 1
            if self.heap[dir] > self.heap[maior]:
                maior = dir

        if maior == i:
            break

        print(f"Swap: {self.heap[i]} <-> {self.heap[maior]}")
        self.heap[i], self.heap[maior] = self.heap[maior], self.heap[i]
        self.trocas += 1
        i = maior
EX.05
Busca Linear em Heap Binária (contains)
Entender por que a heap não é adequada para buscas frequentes.
Diferente de uma Árvore de Busca Binária (BST), na Heap não há ordem entre os "irmãos". O filho esquerdo pode ser maior ou menor que o direito. Por isso, para buscar um valor arbitrário, precisamos percorrer o array inteiro.
def contains(self, valor):
    # Custo: O(n). Busca linear exaustiva.
    for i in range(len(self.heap)):
        self.comparacoes += 1
        if self.heap[i] == valor: return True
    return False
Assintótica: se sua aplicação precisa de buscas constantes, a Heap é a estrutura errada. Use BST ou Tabela Hash.
EX.06
Remoção de Elemento Arbitrário (delete)
Dominar a operação complexa de remover qualquer nó da estrutura.
Para remover valor, localizamos seu índice usando uma busca instrumentada (_find_index), removemos o último elemento e o colocamos na posição vaga. O reequilíbrio deve ser cirúrgico: subir se o novo valor for maior que o pai, ou descer caso contrário.
def delete(self, valor):
    idx = self._find_index(valor)
    if idx == -1: return False

    ultimo = self.heap.pop()

    # Se o removido já era o último, a tarefa termina.
    if idx == len(self.heap): return True

    self.heap[idx] = ultimo

    # Decide a direção do reequilíbrio
    if idx > 0:
        self.comparacoes += 1
        if self.heap[idx] > self.heap[self._pai(idx)]:
            self._sift_up(idx)
        else:
            self._sift_down(idx)
    else:
        self._sift_down(idx)

    return True
EX.07
Validação Estrutural (is_valid_heap)
Auditar se um array arbitrário respeita as leis de uma Max-Heap.
A função deve percorrer apenas os nós que possuem filhos (os primeiros n // 2 nós) e garantir que nenhum deles é menor que seus filhos imediatos.
def is_valid_heap(array):
    n = len(array)
    for i in range(n // 2):
        l, r = 2*i + 1, 2*i + 2
        if l < n and array[i] < array[l]: return False
        if r < n and array[i] < array[r]: return False
    return True
EX.08
Construção O(n) por Floyd (build_heap)
Implementar o algoritmo otimizado que ignora inserções individuais.
Requisito Crítico: O TP1 proíbe o uso de inserções repetidas para o build_heap. Você deve usar o método de Floyd (heapify), que custa O(n) e começa de baixo para cima.
def build_heap(self, lista):
    self.heap = list(lista)
    # Começa do último nó pai e vai até a raiz
    for i in range((len(self.heap) // 2) - 1, -1, -1):
        self._sift_down(i)
Por que funciona? Como a maioria dos nós está na base, eles percorrem uma altura pequena. A soma das descidas é linear.
EX.09
Comparação: Floyd vs Inserção Incremental
Evidenciar a economia de operações entre os dois métodos de construção.
Ao comparar os métodos, medimos o número de trocas e comparações reais. A diferença é mais evidente em entradas desfavoráveis (como listas já ordenadas de forma crescente para uma Max-Heap).
def test_comparison(n):
    # Entrada crescente: desfavorável para inserção incremental em Max-Heap
    valores = list(range(n))
    
    # 1. Incremental
    h_inc = BinaryHeap()
    for v in valores: h_inc.insert(v)
    
    # 2. Floyd
    h_floyd = BinaryHeap()
    h_floyd.build_heap(valores)
    
    print(f"N={n} | Incremental Swaps: {h_inc.trocas} | Floyd Swaps: {h_floyd.trocas}")
EX.10
Heap Sort Parcial (K maiores)
Extrair apenas uma parcela dos dados de forma eficiente.
Extrair k vezes de uma heap construída em O(n) custa O(n + k log n). Se k for pequeno em relação a n, é muito superior a ordenar o array inteiro.
def k_maiores(array, k):
    if k < 0: raise ValueError("k negativo")
    h = BinaryHeap()
    h.build_heap(array)
    limite = min(k, len(array))
    return [h.extract_max() for _ in range(limite)]
EX.11
Análise de Complexidade Empírica
Dados reais coletados via instrumentação (Entrada Crescente).
Os dados abaixo ilustram a carga de trabalho em uma entrada desfavorável (lista crescente). Note que o custo incremental cresce exponencialmente em relação à eficiência linear de Floyd.
nIncremental (Trocas)Floyd (Trocas)Diferença
1.0007.987992~8,1x mais trocas
10.000113.6319.992~11,4x mais trocas
Os dados empíricos são compatíveis com a análise teórica: a construção por Floyd tende a realizar muito menos trabalho. A prova assintótica vem da soma das alturas dos nós no heapify, que resulta em O(n).
def test_comparison(n):
    valores = list(range(n))  # Entrada crescente: desfavorável para Max-Heap incremental

    h_inc = BinaryHeap()
    for v in valores:
        h_inc.insert(v)

    h_floyd = BinaryHeap()
    h_floyd.build_heap(valores)

    print(
        f"N={n} | "
        f"Incremental: trocas={h_inc.trocas}, comparacoes={h_inc.comparacoes} | "
        f"Floyd: trocas={h_floyd.trocas}, comparacoes={h_floyd.comparacoes}"
    )

test_comparison(1000)
test_comparison(10000)
EX.12
Roteiro de Documentação e Defesa
Garantir uma entrega profissional e rastreável.
  • Arquitetura: Justificar a escolha do Array pela localidade de referência.
  • Invariantes: Explicar como o _sift_down restaura a propriedade de heap.
  • Decisões: Discutir o custo linear da busca arbitrária.
  • Evidência: Apresentar a instrumentação e a tabela comparativa.
TESTES
Bateria de Testes para Evidência
Gerar saídas reais para anexar ao seu relatório de TP1.
Execute o código abaixo para validar todas as operações solicitadas no teste:
h = BinaryHeap()

# E3: Inserção
for v in [10, 4, 15, 20, 1]: h.insert(v)
print("Heap após inserções:", h.heap)

# E7: Validação
print("É heap válida?", is_valid_heap(h.heap))

# E4: Extração
while h.heap:
    print("Extraído:", h.extract_max(), "Restante:", h.heap)

# E8: Build-Heap Floyd
h.build_heap([3, 9, 2, 10, 7, 1])
print("Build heap Floyd:", h.heap)

# E5 e E6: Busca e Delete
print("Contains 7?", h.contains(7))
print("Delete 9:", h.delete(9))
print("Após delete 9:", h.heap)

# E10: K Maiores
print("Top 3:", k_maiores([8, 1, 5, 20, 13, 2], 3))
CHECK
Validação Final de Conformidade TP1
Garantir 100% de cobertura dos requisitos antes do envio.
  • Extract-Max: trata corretamente o caso de um único elemento?
  • Delete: utiliza _find_index instrumentado e reequilibra corretamente?
  • Sift-down: implementado integralmente com busca pelo maior filho?
  • Floyd: construção realizada em O(n) sem inserções individuais?
  • Instrumentação: comparacoes e trocas capturam o custo real?
  • Saída: a bateria de testes gera as evidências necessárias para o vídeo?
Página finalizada com rigor técnico máximo. Guia de TP1 completo.