Heap: A Estrutura de Dados Que Quase Todo Desenvolvedor Ignora (Até Precisar Resolver um Problema Difícil)
Aprenda o que é uma Heap, como funciona a estrutura de dados, diferença entre Min Heap e Max Heap, complexidade, implementação em PHP e aplicações práticas em algoritmos e entrevistas técnicas.
Índice
- O que é uma Heap?
- Heap não significa "memória Heap"
- A propriedade Heap
- Min Heap
- Max Heap
- Heap é uma árvore... mas não exatamente
- Como a Heap fica armazenada em um Array
- Inserção (Heapify Up)
- Remoção (Heapify Down)
- Complexidade
- Heap vs Array Ordenado
- Heap vs Árvore Binária de Busca
- Priority Queue
- Heap no PHP
- Exemplos em PHP
- Exemplo equivalente em Python
- Problemas clássicos resolvidos com Heap
- Heap Sort
- Erros comuns
- Quando NÃO usar Heap
- Conclusão
A maioria dos desenvolvedores passa anos escrevendo código sem nunca implementar uma Heap.
Ainda assim, ela aparece em sistemas de roteamento, motores de busca, bancos de dados, sistemas operacionais, algoritmos de grafos, filas de processamento, jogos, inteligência artificial e praticamente toda plataforma que precisa decidir rapidamente qual elemento deve ser processado primeiro.
O curioso é que muitos profissionais conhecem arrays, listas, pilhas, filas, árvores e tabelas hash, mas nunca estudam Heap de verdade.
Até o dia em que aparece uma entrevista técnica ou um problema no LeetCode que simplesmente "grita" por uma Heap.
Nesse momento, tentar resolver usando apenas arrays normalmente leva a soluções lentas ou excessivamente complexas.
Neste artigo vamos entender profundamente o funcionamento dessa estrutura de dados, suas variantes, suas complexidades e como utilizá-la em PHP, sempre com exemplos práticos e aplicações do mundo real.
O que é uma Heap?
Uma Heap é uma estrutura de dados baseada em uma árvore binária completa cuja principal finalidade é manter o elemento de maior (ou menor) prioridade sempre facilmente acessível.
Diferentemente de uma árvore binária de busca (BST), em que cada nó mantém uma relação de ordenação com toda a sua subárvore, uma Heap garante apenas uma propriedade local entre pais e filhos. Essa característica é suficiente para que o elemento mais importante esteja sempre na raiz, tornando operações como obter ou remover esse elemento extremamente eficientes.
Em outras palavras, uma Heap não mantém todos os elementos ordenados, mas mantém o elemento de maior prioridade na posição correta.
Essa simples ideia faz da Heap uma das estruturas mais utilizadas em Ciência da Computação.
Ela aparece em algoritmos clássicos como:
- Dijkstra (menor caminho)
- Prim (árvore geradora mínima)
- A* (busca heurística)
- Heap Sort
- Merge K Sorted Lists
- Kth Largest Element
- Median Finder
Além disso, sistemas reais utilizam Heaps para implementar:
- filas de impressão;
- escalonadores de processos do sistema operacional;
- filas de tarefas assíncronas;
- sistemas de ranking;
- motores de busca;
- algoritmos de recomendação;
- simuladores de eventos.
Sempre que existe a necessidade de responder rapidamente à pergunta:
"Qual é o próximo elemento mais importante?"
há uma boa chance de uma Heap ser a estrutura mais adequada.
Heap não significa "memória Heap"
Ao pesquisar sobre o assunto, é comum encontrar referências à Heap Memory. Apesar do nome, ela não tem relação direta com a estrutura de dados Heap.
São dois conceitos completamente diferentes.
Heap (estrutura de dados)
É uma estrutura utilizada para organizar elementos de acordo com uma prioridade.
Seu objetivo é permitir operações eficientes como:
- inserir elementos;
- remover o maior ou menor elemento;
- consultar rapidamente o elemento prioritário.
Ela pode ser implementada utilizando arrays, árvores ou outras estruturas.
Heap Memory
Já a Heap Memory faz parte do gerenciamento de memória da linguagem de programação.
É nela que normalmente ficam armazenados objetos e estruturas alocadas dinamicamente.
Por exemplo, em PHP:
class User
{
public string $name = 'Thiago';
}
$user = new User();
O objeto User é criado na memória Heap da linguagem, enquanto a variável $user guarda apenas uma referência para esse objeto.
Da mesma forma, em Python:
numbers = [10, 20, 30]
A lista é alocada na memória Heap do interpretador.
Perceba que isso não significa que ela seja organizada como uma estrutura de dados Heap. Na verdade, trata-se apenas da região de memória destinada a objetos criados dinamicamente.
Em resumo:
| Heap (Estrutura de Dados) | Heap Memory |
|---|---|
| Estrutura para organizar prioridades | Região de memória |
| Faz parte de algoritmos | Faz parte do gerenciamento de memória |
| Utilizada em filas de prioridade | Utilizada para alocação dinâmica |
| Implementada pelo programador ou pela biblioteca | Implementada pela linguagem e pelo sistema operacional |
Embora compartilhem o mesmo nome, os dois conceitos possuem finalidades completamente diferentes.
A propriedade Heap
Toda Heap obedece a uma regra extremamente simples, conhecida como Heap Property.
Essa propriedade determina apenas a relação entre um nó e seus filhos imediatos.
Dependendo do tipo da Heap, existem duas possibilidades.
Em uma Max Heap
O valor do pai é sempre maior ou igual ao valor de seus filhos.
Por exemplo:
100
/ \
80 60
/ \ / \
40 50 20 10
Observe que:
- 100 é maior que 80 e 60;
- 80 é maior que 40 e 50;
- 60 é maior que 20 e 10.
Nada mais é garantido.
Por exemplo, o valor 50 pode ser maior que 60? Não.
Mas 40 pode ser maior que 20? Sim.
Isso não viola nenhuma regra da Heap.
A propriedade envolve apenas pais e filhos.
Em uma Min Heap
O raciocínio é exatamente o oposto.
O pai sempre possui valor menor ou igual ao de seus filhos.
10
/ \
20 30
/ \ / \
40 50 60 80
Novamente, não existe qualquer garantia sobre elementos que pertencem a ramos diferentes.
Essa é uma característica importante.
Uma Heap não é um conjunto ordenado.
Se percorrermos seus elementos em ordem de armazenamento, provavelmente veremos uma sequência aparentemente aleatória.
O único elemento cuja posição é sempre conhecida é a raiz.
É justamente essa propriedade que permite acessar o maior (ou menor) elemento em tempo constante (O(1)), sem o custo de manter toda a coleção ordenada.
Min Heap
Na Min Heap, o menor elemento da estrutura sempre permanece na raiz.
Isso significa que consultar o menor valor é uma operação extremamente rápida.
Por exemplo:
5
/ \
12 18
/ \ / \
30 25 40 50
O menor elemento é sempre 5.
Se inserirmos um novo elemento com valor 2, ele será inicialmente colocado na próxima posição livre da árvore. Em seguida, será comparado com seu pai e, se necessário, trocará de posição sucessivamente até restaurar a propriedade da Heap.
Esse processo é conhecido como Heapify Up (ou Sift Up).
Graças a esse mecanismo, a inserção possui complexidade O(log n).
As Min Heaps são muito utilizadas em problemas que envolvem encontrar repetidamente o menor elemento disponível, como:
- algoritmo de Dijkstra;
- algoritmo de Prim;
- Merge K Sorted Lists;
- agendadores de tarefas;
- simuladores de eventos;
- sistemas de filas por menor prioridade numérica.
Em Python, por exemplo, o módulo padrão heapq implementa exatamente uma Min Heap, tornando esse tipo de estrutura extremamente comum na linguagem.
Max Heap
Na Max Heap, ocorre o inverso.
O maior elemento permanece sempre na raiz.
Exemplo:
90
/ \
70 60
/ \ / \
40 30 20 10
Sempre que desejarmos o maior elemento da coleção, basta consultar a raiz.
Essa operação possui custo O(1).
Quando removemos o maior elemento, a Heap reorganiza automaticamente sua estrutura por meio do processo conhecido como Heapify Down (ou Sift Down), restaurando a propriedade da árvore em O(log n).
Max Heaps aparecem frequentemente em situações como:
- rankings;
- placares de jogos;
- sistemas de recomendação;
- gerenciamento de prioridades;
- seleção dos maiores valores de uma coleção;
- algoritmos que procuram continuamente o próximo maior elemento.
No PHP, a classe SplMaxHeap oferece uma implementação pronta desse tipo de estrutura, enquanto SplPriorityQueue permite trabalhar com prioridades arbitrárias, sendo uma opção ainda mais flexível para diversos cenários.
Heap é uma árvore... mas não exatamente
Quando dizemos que uma Heap é uma árvore, isso é verdade apenas do ponto de vista conceitual.
Na prática, quase todas as implementações utilizam um array para armazenar seus elementos, sem criar objetos representando nós, ponteiros para filhos ou referências para o pai.
Isso é possível porque uma Heap possui uma característica muito importante: ela é sempre uma árvore binária completa.
Uma árvore binária completa é aquela em que:
- todos os níveis são totalmente preenchidos, exceto possivelmente o último;
- o último nível é preenchido sempre da esquerda para a direita.
Por exemplo:
10
/ \
20 30
/ \ /
40 50 60
Essa organização elimina "buracos" na árvore, permitindo que todos os nós sejam armazenados sequencialmente em um vetor.
É justamente essa característica que torna a Heap extremamente eficiente em memória.
Enquanto uma árvore binária tradicional normalmente exige que cada nó armazene referências para pai, filho esquerdo e filho direito, uma Heap precisa apenas de um array.
Como consequência:
- utiliza menos memória;
- possui melhor localidade de cache;
- realiza menos alocações de objetos;
- costuma apresentar desempenho superior em operações intensivas.
Essa é uma das razões pelas quais praticamente todas as bibliotecas e linguagens implementam Heaps utilizando arrays.
Como a Heap fica armazenada em um Array
Imagine a seguinte Max Heap:
90
/ \
70 60
/ \ / \
40 30 20 10
Internamente, ela pode ser armazenada como:
[90, 70, 60, 40, 30, 20, 10]
Observe que não existe nenhuma informação explícita indicando quem é pai ou filho.
Essa relação é calculada apenas pelos índices do vetor.
Se um elemento estiver na posição i, então:
- pai:
(i - 1) / 2
- filho esquerdo:
2 * i + 1
- filho direito:
2 * i + 2
Por exemplo, considerando o índice 1:
Índice: 0 1 2 3 4 5 6
Valor : 90 70 60 40 30 20 10
O elemento da posição 1 possui valor 70.
Seus filhos serão:
esquerdo = 2 × 1 + 1 = 3
direito = 2 × 1 + 2 = 4
Logo:
70
├── 40
└── 30
Da mesma forma, o pai do elemento localizado no índice 4 é:
(4 - 1) / 2 = 1
Ou seja, seu pai é exatamente o elemento de índice 1.
Essas fórmulas tornam a navegação pela Heap extremamente eficiente, eliminando completamente a necessidade de ponteiros.
Inserção (Heapify Up)
Inserir um elemento em uma Heap é um processo bastante simples.
Primeiro, o novo elemento é colocado na próxima posição livre do array.
Isso mantém a propriedade de árvore binária completa.
Suponha a seguinte Max Heap:
90
/ \
70 60
/ \
40 30
Representada pelo vetor:
[90, 70, 60, 40, 30]
Agora vamos inserir o valor 80.
Inicialmente ele será colocado no final do vetor:
[90, 70, 60, 40, 30, 80]
A árvore passa a ser:
90
/ \
70 60
/ \ /
40 30 80
Nesse momento, a propriedade da Heap foi violada.
O valor 80 é maior que seu pai (60).
Então ocorre uma troca:
90
/ \
70 80
/ \ /
40 30 60
Agora o novo pai é 90.
Como 80 é menor que 90, o processo termina.
Esse movimento do elemento em direção à raiz recebe o nome de Heapify Up (também chamado de Sift Up ou Bubble Up).
Na pior hipótese, o elemento sobe da última folha até a raiz.
Como uma Heap possui altura log₂(n), a inserção possui complexidade:
O(log n)
Remoção (Heapify Down)
Remover um elemento da Heap normalmente significa remover sua raiz, pois é nela que está armazenado o elemento de maior (ou menor) prioridade.
Considere novamente esta Max Heap:
90
/ \
70 80
/ \ /
40 30 60
Ao remover o 90, não podemos simplesmente apagar a raiz, pois isso deixaria um espaço vazio na árvore.
A solução é mover o último elemento para a raiz.
Assim, a árvore fica:
60
/ \
70 80
/ \
40 30
Agora a propriedade da Heap foi quebrada.
O valor 60 é menor que seus dois filhos.
Então ele é comparado com ambos.
Como 80 é o maior filho, ocorre a troca:
80
/ \
70 60
/ \
40 30
Nesse ponto, a Heap voltou a obedecer sua propriedade.
Esse processo de "descer" o elemento até sua posição correta recebe o nome de Heapify Down (ou Sift Down).
Assim como ocorre na inserção, o elemento percorre no máximo a altura da árvore.
Logo, remover a raiz também custa:
O(log n)
Complexidade
Uma das maiores vantagens da Heap é oferecer operações muito eficientes para manipular elementos prioritários.
| Operação | Complexidade |
|---|---|
| Consultar a raiz | O(1) |
| Inserção | O(log n) |
| Remover a raiz | O(log n) |
| Atualizar prioridade | O(log n) |
| Construir uma Heap a partir de um array | O(n) |
À primeira vista, um resultado costuma causar estranheza: por que construir uma Heap inteira custa O(n), e não O(n log n)?
A resposta está no algoritmo utilizado.
Uma abordagem ingênua seria inserir os elementos um por um em uma Heap inicialmente vazia. Como cada inserção custa O(log n), o custo total seria realmente O(n log n).
Entretanto, existe um algoritmo mais eficiente, conhecido como Floyd's Heap Construction ou simplesmente Build Heap, que parte de um array já preenchido e reorganiza seus elementos de baixo para cima.
Em vez de realizar um Heapify Up para cada elemento inserido, ele executa um Heapify Down apenas nos nós internos, começando pelo último pai e avançando até a raiz.
Como a maioria dos nós está localizada nos níveis mais baixos da árvore — onde os deslocamentos são muito curtos — o trabalho total cresce linearmente em relação ao número de elementos.
Esse é um resultado clássico da análise de algoritmos e uma das propriedades mais elegantes da estrutura.
Na prática, isso significa que, quando você já possui todos os dados em um array, é significativamente mais eficiente construir a Heap de uma só vez do que inserir cada elemento individualmente. Essa diferença se torna cada vez mais perceptível à medida que a quantidade de elementos aumenta.
Heap vs Array Ordenado
À primeira vista, pode parecer que uma Heap e um array ordenado resolvem o mesmo problema: ambos permitem encontrar rapidamente o maior ou o menor elemento. No entanto, suas características são bastante diferentes.
Em um array ordenado, os elementos permanecem em ordem durante todo o tempo.
[10, 20, 30, 40, 50, 60, 70]
Isso torna muito simples acessar o menor ou o maior elemento, dependendo da ordem utilizada.
O problema surge quando precisamos inserir ou remover elementos.
Imagine inserir o número 35 no array acima.
[10, 20, 30, 35, 40, 50, 60, 70]
Todos os elementos após a posição de inserção precisam ser deslocados uma posição para a direita.
Da mesma forma, remover um elemento exige mover diversos valores para preencher o espaço deixado.
Por isso, embora a consulta seja extremamente rápida, atualizações costumam custar O(n).
Já em uma Heap, apenas a propriedade entre pais e filhos é mantida.
Ela pode parecer "desorganizada":
[70, 50, 60, 20, 40, 30, 10]
Mesmo assim, o maior elemento continua sendo facilmente encontrado na raiz.
Como apenas alguns elementos precisam trocar de posição durante inserções e remoções, essas operações possuem custo O(log n).
A comparação pode ser resumida na tabela abaixo:
| Operação | Array Ordenado | Heap |
|---|---|---|
| Consultar maior/menor | O(1) | O(1) |
| Inserção | O(n) | O(log n) |
| Remoção do maior/menor | O(n) | O(log n) |
| Manter todos os elementos ordenados | ✅ | ❌ |
Portanto, se você precisa percorrer todos os elementos em ordem crescente ou decrescente, um array ordenado é mais apropriado.
Por outro lado, se o objetivo é acessar repetidamente apenas o elemento de maior ou menor prioridade enquanto novos elementos entram e saem da coleção, uma Heap tende a ser muito mais eficiente.
Heap vs Árvore Binária de Busca
Outra comparação bastante comum é entre Heap e Árvore Binária de Busca (Binary Search Tree — BST).
Embora ambas sejam árvores binárias, elas possuem objetivos completamente diferentes.
Uma BST organiza seus elementos de forma que:
- todos os elementos da subárvore esquerda sejam menores que o nó;
- todos os elementos da subárvore direita sejam maiores.
Por exemplo:
50
/ \
30 70
/ \ / \
20 40 60 80
Essa organização permite realizar buscas eficientes.
Já uma Heap não possui essa propriedade.
Considere a seguinte Max Heap:
80
/ \
70 60
/ \ / \
20 40 30 50
Perceba que o valor 50 está na subárvore direita de 60, embora seja menor que ele.
Isso não representa qualquer problema, pois a Heap não foi criada para realizar buscas.
Seu único compromisso é garantir que o pai respeite a propriedade da Heap em relação aos seus filhos.
Como consequência, encontrar um valor específico dentro de uma Heap normalmente exige percorrer praticamente todos os seus elementos.
A comparação fica mais clara na tabela:
| Característica | Heap | BST |
|---|---|---|
| Encontrar maior/menor | Excelente | Boa |
| Buscar um valor específico | Ruim | Excelente |
| Inserção | O(log n) | O(log n)* |
| Remoção | O(log n) | O(log n)* |
| Mantém todos os elementos ordenados | ❌ | ✅ |
Em árvores balanceadas, como AVL e Red-Black Trees. Uma BST simples pode degradar para O(n).
Em resumo:
- escolha uma Heap quando a prioridade for sempre acessar rapidamente o maior ou o menor elemento;
- escolha uma BST quando a aplicação precisar realizar muitas buscas por valores específicos ou percorrer os elementos em ordem.
Priority Queue
Uma Priority Queue (fila de prioridade) é um tipo abstrato de dado cujo comportamento difere de uma fila tradicional.
Em uma fila comum (FIFO – First In, First Out), o primeiro elemento inserido é sempre o primeiro a ser removido.
A → B → C → D
A ordem de processamento depende exclusivamente do momento em que cada elemento entrou na fila.
Já em uma fila de prioridade, cada elemento possui uma prioridade associada.
Quem possui maior prioridade é processado primeiro, independentemente da ordem de inserção.
Por exemplo:
| Tarefa | Prioridade |
|---|---|
| Backup | 2 |
| Enviar e-mail | 1 |
| Processar pagamento | 10 |
| Gerar relatório | 3 |
A ordem de execução será:
- Processar pagamento
- Gerar relatório
- Backup
- Enviar e-mail
Observe que "Processar pagamento" foi a terceira tarefa inserida, mas será executada primeiro porque possui a maior prioridade.
É justamente aqui que a Heap entra em cena.
A implementação mais comum de uma Priority Queue utiliza uma Heap internamente.
Isso permite:
- inserir novos elementos em O(log n);
- remover o elemento prioritário em O(log n);
- consultar o próximo elemento em O(1).
Na prática, muitas vezes você utilizará uma Heap sem sequer perceber, pois diversas bibliotecas oferecem diretamente uma implementação de Priority Queue baseada nessa estrutura.
Heap no PHP
O PHP oferece implementações prontas de Heap através da extensão SPL (Standard PHP Library).
Na maioria dos casos, não há necessidade de implementar uma Heap manualmente em aplicações reais.
As principais classes são:
SplMinHeapSplMaxHeapSplPriorityQueue
Embora possuam comportamentos semelhantes, elas foram criadas para resolver problemas diferentes.
SplMinHeap
Mantém sempre o menor elemento no topo.
$heap = new SplMinHeap();
$heap->insert(30);
$heap->insert(10);
$heap->insert(50);
$heap->insert(20);
echo $heap->extract(); // 10
echo $heap->extract(); // 20
Os elementos são removidos em ordem crescente.
SplMaxHeap
É o comportamento inverso.
$heap = new SplMaxHeap();
$heap->insert(30);
$heap->insert(10);
$heap->insert(50);
$heap->insert(20);
echo $heap->extract(); // 50
echo $heap->extract(); // 30
Agora os elementos saem do maior para o menor.
SplPriorityQueue
Essa é a implementação mais flexível.
Em vez de armazenar apenas valores, ela permite associar uma prioridade independente para cada elemento.
$queue = new SplPriorityQueue();
$queue->insert('Enviar e-mail', 1);
$queue->insert('Gerar relatório', 3);
$queue->insert('Processar pagamento', 10);
echo $queue->extract(); // Processar pagamento
Observe que o valor retornado não é a prioridade, mas sim o elemento cuja prioridade é maior.
Essa classe é muito útil quando os dados possuem um peso próprio que determina sua ordem de processamento.
Exemplos em PHP
Depois de entender como funciona uma Heap, fica mais fácil reconhecer situações em que ela simplifica bastante a solução de um problema.
Encontrando os 10 maiores números
Imagine que você precise descobrir os dez maiores valores de um arquivo contendo milhões de números.
Uma abordagem ingênua seria carregar tudo na memória e ordenar o array:
sort($numbers);
$top10 = array_slice($numbers, -10);
Além do custo de ordenar todos os elementos (O(n log n)), essa solução exige manter todo o conjunto em memória.
Com uma Min Heap limitada a dez elementos, basta manter apenas os dez maiores valores encontrados até o momento.
Cada novo número é comparado com o menor elemento da Heap. Se for maior, substitui esse elemento.
Assim, o consumo de memória permanece constante, independentemente do tamanho do arquivo.
Ranking em tempo real
Imagine um jogo online que precisa exibir continuamente os 100 melhores jogadores.
Sempre que um jogador aumenta sua pontuação, basta atualizar a Heap responsável pelo ranking.
Não é necessário ordenar novamente toda a tabela de jogadores.
Escalonador de tarefas
Filas de processamento costumam utilizar prioridades.
Por exemplo:
$queue = new SplPriorityQueue();
$queue->insert('Enviar boleto', 2);
$queue->insert('Gerar nota fiscal', 5);
$queue->insert('Atualizar cache', 1);
while (!$queue->isEmpty()) {
echo $queue->extract() . PHP_EOL;
}
Saída:
Gerar nota fiscal
Enviar boleto
Atualizar cache
As tarefas mais importantes são sempre executadas primeiro.
Processamento de eventos
Sistemas de simulação, motores de jogos e plataformas de negociação frequentemente precisam executar eventos em ordem cronológica.
Cada evento recebe como prioridade seu instante de execução.
A Heap garante que o próximo evento esteja sempre disponível em tempo constante, enquanto inserções e remoções continuam eficientes mesmo quando milhões de eventos estão aguardando processamento.
Em todos esses cenários, a principal vantagem da Heap é evitar ordenar toda a coleção repetidamente. Em vez disso, ela mantém apenas a informação necessária para que o próximo elemento de maior (ou menor) prioridade esteja sempre disponível com baixo custo computacional.
Exemplo equivalente em Python
Ao contrário do PHP, que oferece classes prontas como SplMinHeap e SplMaxHeap, a biblioteca padrão do Python disponibiliza apenas o módulo heapq, que implementa uma Min Heap.
A estrutura é utilizada sobre uma lista comum.
import heapq
heap = []
heapq.heappush(heap, 30)
heapq.heappush(heap, 10)
heapq.heappush(heap, 50)
heapq.heappush(heap, 20)
print(heap)
# [10, 20, 50, 30]
print(heapq.heappop(heap))
# 10
print(heapq.heappop(heap))
# 20
Embora a lista pareça parcialmente ordenada, ela não está totalmente ordenada.
A única garantia é que o menor elemento esteja sempre na primeira posição (heap[0]).
Simulando uma Max Heap
Uma dúvida comum entre desenvolvedores Python é:
Como criar uma Max Heap se o
heapqimplementa apenas uma Min Heap?
A solução tradicional consiste em armazenar os valores negativos.
import heapq
heap = []
heapq.heappush(heap, -30)
heapq.heappush(heap, -10)
heapq.heappush(heap, -50)
heapq.heappush(heap, -20)
print(-heapq.heappop(heap))
# 50
print(-heapq.heappop(heap))
# 30
Como o menor número negativo representa o maior número positivo, a Min Heap passa a se comportar exatamente como uma Max Heap.
Essa técnica é amplamente utilizada em competições de programação, entrevistas técnicas e em soluções publicadas para problemas do LeetCode.
Problemas clássicos resolvidos com Heap
Uma Heap aparece com tanta frequência em entrevistas técnicas que aprender a reconhecê-la costuma fazer mais diferença do que saber implementá-la do zero.
Sempre que um problema envolve encontrar repetidamente o maior ou o menor elemento de uma coleção dinâmica, vale a pena considerar essa estrutura.
Alguns exemplos clássicos são:
Kth Largest Element
Encontrar o k-ésimo maior elemento de um conjunto.
Em vez de ordenar todo o array, basta manter uma Min Heap com tamanho k.
Complexidade:
- Tempo: O(n log k)
- Memória: O(k)
Top K Frequent Elements
Dado um conjunto de valores, encontrar os k elementos mais frequentes.
Depois de contar as ocorrências utilizando um mapa (HashMap ou dict), uma Heap mantém apenas os elementos mais relevantes.
Merge K Sorted Lists
Problema bastante conhecido do LeetCode.
Em vez de comparar continuamente todas as listas, a Heap mantém apenas o menor elemento de cada uma.
Cada remoção é seguida pela inserção do próximo elemento da lista correspondente.
Essa estratégia reduz significativamente a complexidade da solução.
Median Finder
Outro exercício clássico.
A solução mais eficiente utiliza duas Heaps:
- uma Max Heap para armazenar a metade inferior dos valores;
- uma Min Heap para armazenar a metade superior.
Dessa forma, a mediana pode ser obtida em tempo constante após cada inserção.
Algoritmo de Dijkstra
Talvez seja o exemplo mais famoso.
Durante a execução, é necessário descobrir continuamente qual vértice possui a menor distância conhecida.
Uma Min Heap torna essa operação extremamente eficiente.
Algoritmo de Prim
Utilizado para encontrar árvores geradoras mínimas.
Assim como no algoritmo de Dijkstra, uma Heap é empregada para selecionar rapidamente a próxima aresta de menor custo.
A*
O algoritmo A* utiliza uma fila de prioridade para selecionar continuamente o próximo estado mais promissor durante a busca.
É uma técnica bastante empregada em jogos, robótica e sistemas de navegação.
Escalonadores de tarefas
Servidores de aplicações, sistemas operacionais e plataformas de processamento assíncrono frequentemente utilizam Heaps para determinar qual tarefa deve ser executada primeiro.
Na prática, sempre que um problema menciona palavras como:
- prioridade;
- menor;
- maior;
- próximo;
- top K;
- ranking;
- agenda;
- scheduler;
- fila de processamento;
há uma boa chance de que uma Heap seja uma excelente solução.
Heap Sort
O Heap Sort é um algoritmo de ordenação baseado diretamente na estrutura Heap.
Seu funcionamento ocorre em duas etapas:
- construir uma Max Heap a partir do conjunto de dados;
- remover repetidamente o maior elemento, posicionando-o no final do array.
Por exemplo:
[8, 3, 6, 1, 9, 5]
Após construir a Heap:
[9, 8, 6, 1, 3, 5]
Em seguida, o maior elemento é movido para o final do vetor, a Heap é reconstruída para os elementos restantes e o processo continua até que todos estejam ordenados.
As principais características do Heap Sort são:
| Característica | Valor |
|---|---|
| Melhor caso | O(n log n) |
| Caso médio | O(n log n) |
| Pior caso | O(n log n) |
| Memória extra | O(1) |
| Estável | ❌ Não |
Uma vantagem importante é que seu desempenho permanece O(n log n) independentemente da entrada, diferentemente do Quick Sort, que pode degradar para O(n²) em determinados cenários.
Apesar disso, o Heap Sort costuma apresentar desempenho inferior ao Quick Sort na prática devido ao maior número de acessos não sequenciais à memória, o que reduz a eficiência do cache do processador.
Por ser um tema bastante rico, dedicaremos um artigo exclusivo ao Heap Sort no futuro.
Erros comuns
Quem está aprendendo Heap costuma cometer alguns erros recorrentes.
Achar que a Heap está totalmente ordenada
Talvez seja o equívoco mais comum.
Uma Heap não é um array ordenado.
[100, 70, 90, 20, 60, 40, 80]
Esse vetor representa uma Heap válida, embora claramente não esteja em ordem crescente nem decrescente.
Utilizar sort() antes de resolver o problema
Muitos exercícios podem ser resolvidos ordenando todo o array.
Isso funciona, mas frequentemente produz uma solução mais lenta do que a necessária.
Se você precisa apenas do maior elemento, do menor elemento ou dos k maiores, provavelmente não há motivo para ordenar toda a coleção.
Ignorar a complexidade
Inserir um elemento em uma Heap custa O(log n).
Ordenar toda a coleção repetidamente custa O(n log n).
Essa diferença pode parecer pequena para poucos elementos, mas torna-se enorme quando o volume de dados cresce.
Esquecer o Heapify
Após remover a raiz ou inserir um novo elemento, é indispensável restaurar a propriedade da Heap.
Caso contrário, a estrutura deixa de ser uma Heap.
Escolher a Heap errada
É surpreendentemente comum implementar uma Max Heap quando o algoritmo exige uma Min Heap (ou vice-versa).
Antes de começar a programar, faça sempre a seguinte pergunta:
Quero acessar continuamente o menor elemento ou o maior?
A resposta normalmente determina qual tipo de Heap utilizar.
Quando NÃO usar Heap
Embora seja extremamente útil, a Heap não é a solução ideal para todos os problemas.
Evite utilizá-la quando:
Você precisa pesquisar elementos específicos
Encontrar um valor qualquer dentro de uma Heap exige percorrer praticamente toda a estrutura.
Nesse cenário, uma árvore balanceada ou uma tabela hash costuma ser muito mais eficiente.
Todos os elementos precisam permanecer ordenados
Se sua aplicação exige percorrer continuamente os dados em ordem crescente ou decrescente, estruturas como árvores balanceadas ou até um array ordenado podem ser mais adequadas.
A Heap garante apenas que o elemento prioritário esteja corretamente posicionado.
O conjunto de dados quase não muda
Se os dados são carregados uma única vez e permanecem inalterados, simplesmente ordená-los pode ser mais simples do que manter uma Heap.
O volume de dados é pequeno
Para dezenas de elementos, a diferença de desempenho costuma ser irrelevante.
Nesses casos, uma solução mais simples pode oferecer melhor legibilidade sem impacto perceptível na performance.
Como acontece com praticamente toda estrutura de dados, a melhor escolha depende do problema que está sendo resolvido.
Conclusão
À primeira vista, a Heap pode parecer uma estrutura complexa, principalmente porque costuma ser apresentada como uma árvore binária.
No entanto, sua ideia central é surpreendentemente simples: garantir que o elemento de maior ou menor prioridade esteja sempre disponível na raiz.
Essa única propriedade é suficiente para oferecer inserções e remoções em O(log n) e consultas ao elemento prioritário em O(1), tornando a Heap uma das estruturas mais eficientes para trabalhar com prioridades.
Ao longo deste artigo, vimos que uma Heap:
- não mantém todos os elementos ordenados;
- normalmente é armazenada em um array, e não em uma árvore de nós;
- serve de base para filas de prioridade (Priority Queues);
- está presente em algoritmos clássicos como Dijkstra, Prim, A* e Heap Sort;
- possui implementações prontas tanto no PHP (
SplMinHeap,SplMaxHeapeSplPriorityQueue) quanto no Python (heapq).
Mais importante do que decorar sua implementação é aprender a reconhecer os cenários em que ela faz sentido. Em entrevistas técnicas, competições de programação e sistemas reais, identificar rapidamente que um problema envolve prioridades costuma ser o passo decisivo para chegar a uma solução eficiente.
Por fim, mesmo que sua linguagem ofereça uma implementação pronta, vale a pena construir uma Heap manualmente pelo menos uma vez. Esse exercício ajuda a compreender como funcionam operações como Heapify Up e Heapify Down, por que sua complexidade é O(log n) e, principalmente, desenvolve a intuição necessária para escolher a estrutura certa quando um novo problema surgir.
Depois que você entende a lógica por trás de uma Heap, passa a enxergar uma série de problemas sob uma nova perspectiva. Muitas soluções que pareciam exigir ordenações completas ou algoritmos complexos podem ser reduzidas a algumas operações simples sobre uma fila de prioridade — e essa mudança de raciocínio é um dos maiores ganhos ao estudar estruturas de dados.