Heaps (Max-Heap) — do conceito à fila de prioridade (com corretude e Big-O)
Invariantes, operações, heapsort e fila de prioridade com desempate FIFO
Este conjunto de questões não é para decorar heap. É para aprender a pensar com heap: modelar o problema, justificar corretude, estimar custo e validar por evidência.
A progressão vai de fundamentos (árvore completa, ordem parcial, representação em array) até engenharia (fila de prioridade para triagem com desempate FIFO e decisão entre heap vs array ordenado).
Critério de saída: explicar com clareza qual é o invariante, por que as operações preservam/recuperam a propriedade, qual é o custo e quais casos-limite podem quebrar a solução.
Em heap, todos os níveis são completos, exceto possivelmente o último; e no último os nós são preenchidos da esquerda para a direita, sem buracos.
Essa forma fixa permite representar a árvore em array sem ponteiros, com relações por índice: pai (i-1)//2, filhos 2i+1 e 2i+2.
Heap não é BST. Em max-heap, a garantia é local: cada pai tem prioridade maior ou igual à dos filhos.
Por isso ele é parcialmente ordenado: a raiz é o maior, mas não existe ordem global entre irmãos ou entre subárvores.
- Insere no fim do array (preserva árvore completa).
- Aplica sift-up: enquanto o nó for maior que o pai, troca.
- Para ao chegar na raiz ou quando o pai já for maior/igual.
Cada troca corrige uma violação local no caminho até a raiz. A altura do heap é O(log N), então o custo é O(log N).
Critério: para todo índice pai i, vale key(a[i]) >= key(a[filho]) para cada filho existente.
def eh_heap(a, key=lambda x: x):
"""
Retorna True se o array 'a' pode representar um max-heap segundo a função 'key'.
"""
n = len(a)
for i in range(n):
l = 2*i + 1
r = 2*i + 2
if l < n and key(a[i]) < key(a[l]):
return False
if r < n and key(a[i]) < key(a[r]):
return False
return True
# testes rápidos
assert eh_heap([10, 9, 8, 6, 5, 7, 4, 2, 1, 3])
assert not eh_heap([9, 10]) # pai menor que filho
Tempo: O(N). O loop visita cada índice uma vez, com verificações O(1).
Memória extra: O(1), fora o array de entrada.
Se items já for heap válido, copia direto. Caso contrário, insere item a item para preservar propriedade.
class Heap:
"""
Max-heap baseado em array, com função de prioridade 'key'.
"""
def __init__(self, items=None, key=lambda x: x):
self._key = key
self._a = []
if items is None:
return
# Se já representa heap, copia (O(N)).
if eh_heap(items, key=self._key):
self._a = list(items)
else:
# Caso contrário, insere um a um (O(N log N)).
for it in items:
self.push(it)
def __len__(self):
return len(self._a)
def peek(self):
if not self._a:
raise IndexError("heap vazio")
return self._a[0]
def push(self, x):
self._a.append(x)
self._sift_up(len(self._a) - 1)
def pop(self):
if not self._a:
raise IndexError("heap vazio")
root = self._a[0]
last = self._a.pop()
if self._a:
self._a[0] = last
self._sift_down(0)
return root
def _sift_up(self, i):
a, key = self._a, self._key
while i > 0:
p = (i - 1) // 2
if key(a[p]) >= key(a[i]):
break
a[p], a[i] = a[i], a[p]
i = p
def _sift_down(self, i):
a, key = self._a, self._key
n = len(a)
while True:
l = 2*i + 1
r = 2*i + 2
if l >= n:
break
j = l
if r < n and key(a[r]) > key(a[l]):
j = r
if key(a[i]) >= key(a[j]):
break
a[i], a[j] = a[j], a[i]
i = j
def to_array(self):
return list(self._a)
def niveis(self):
"""
Retorna o número de níveis do heap em O(log N):
caminha sempre pelo filho esquerdo até sair do array.
"""
n = len(self._a)
if n == 0:
return 0
levels = 0
i = 0
while i < n:
levels += 1
i = 2*i + 1
return levels
pop(), trate corretamente heap com 1 elemento.Array da figura (ordem por níveis): [10, 9, 8, 6, 5, 7, 4, 2, 1, 3].
Como já é um heap válido, o construtor pode copiar diretamente.
h = Heap([10, 9, 8, 6, 5, 7, 4, 2, 1, 3])
assert h.to_array() == [10, 9, 8, 6, 5, 7, 4, 2, 1, 3]
Após push(11), o heap fica: [11, 10, 8, 6, 9, 7, 4, 2, 1, 3, 5].
h = Heap([10, 9, 8, 6, 5, 7, 4, 2, 1, 3])
h.push(11)
assert h.to_array() == [11, 10, 8, 6, 9, 7, 4, 2, 1, 3, 5]
Removendo a raiz (10), o heap atualizado fica: [9, 6, 8, 3, 5, 7, 4, 2, 1].
h = Heap([10, 9, 8, 6, 5, 7, 4, 2, 1, 3])
raiz = h.pop()
assert raiz == 10
assert h.to_array() == [9, 6, 8, 3, 5, 7, 4, 2, 1]
Inserções: 46, 13, 25, 1, 3, 90, 59.
Ordem de remoção em max-heap: [90, 59, 46, 25, 13, 3, 1].
Versão conceitual: constrói heap e extrai raízes para um array de saída.
Versão in-place com max-heap: heapify, troca raiz com fim, reduz heap ativo e faz sift-down na raiz, repetindo até ordenar.
Com max-heap, extração direta gera ordem decrescente; para crescente use min-heap ou preencha de trás para frente.
Heapsort tem pior caso O(N log N).
- Construção do heap:
O(N)(heapify/Floyd) ouO(N log N)por inserções repetidas. - Extrações:
Nremoções deO(log N), totalizandoO(N log N). - Memória extra:
O(1)na versão in-place.
Comparação típica: Bubble/Insertion/Selection O(N²); Merge O(N log N) com memória extra; Quick com média O(N log N) e pior caso O(N²).
Heapsort não é estável como ordenação, mas oferece pior caso garantido em O(N log N).
Para contar níveis, caminhe sempre para o filho esquerdo (2i+1) até sair do array. Cada passo desce um nível.
assert Heap([]).niveis() == 0
assert Heap([10]).niveis() == 1
assert Heap([10, 9]).niveis() == 2
assert Heap([10, 9, 8, 7]).niveis() == 3
log N.Regra: maior gravidade primeiro; em empate, menor seq primeiro. Em max-heap, use chave (gravidade, -seq).
class EmergencyQueueHeap:
def __init__(self):
self._heap = Heap(key=lambda p: (p["gravidade"], -p["seq"]))
def enqueue(self, paciente: dict):
self._heap.push(paciente)
def dequeue(self) -> dict:
return self._heap.pop()
def __len__(self):
return len(self._heap)
# exemplo
fila = EmergencyQueueHeap()
pacientes = [
{"seq": 1, "nome": "José", "gravidade": 2},
{"seq": 2, "nome": "Márcia","gravidade": 3},
{"seq": 3, "nome": "André", "gravidade": 2},
{"seq": 4, "nome": "Bruna", "gravidade": 5},
{"seq": 5, "nome": "Caio", "gravidade": 5},
]
for p in pacientes:
fila.enqueue(p)
assert fila.dequeue()["nome"] == "Bruna"
assert fila.dequeue()["nome"] == "Caio"
assert fila.dequeue()["nome"] == "Márcia"
assert fila.dequeue()["nome"] == "José"
assert fila.dequeue()["nome"] == "André"
Mantenha lista ordenada por prioridade e remova do final com pop() para ter remoção O(1). Inserção segue O(N) por deslocamentos.
Observação: esta abordagem assume chave totalmente ordenável. No exemplo, seq deve ser único para evitar empate completo de prioridade.
import bisect
class EmergencyQueueSortedArray:
def __init__(self):
self._a = [] # guarda tuplas (prioridade, paciente)
def _prio(self, p):
return (p["gravidade"], -p["seq"])
def enqueue(self, paciente: dict):
item = (self._prio(paciente), paciente)
# mantém em ordem crescente; maior prioridade fica no final
bisect.insort(self._a, item)
def dequeue(self) -> dict:
if not self._a:
raise IndexError("fila vazia")
return self._a.pop()[1]
def __len__(self):
return len(self._a)
fila = EmergencyQueueSortedArray()
for p in pacientes:
fila.enqueue(p)
assert fila.dequeue()["nome"] == "Bruna"
assert fila.dequeue()["nome"] == "Caio"
assert fila.dequeue()["nome"] == "Márcia"
Heap: enqueue O(log N), dequeue O(log N).
Array ordenado: busca de posição pode ser O(log N), mas inserção desloca elementos e custa O(N); dequeue no final é O(1).
Em triagem hospitalar, o perfil é dinâmico (muitas entradas e saídas). Nesse cenário, heap tende a ser a decisão mais previsível e equilibrada.