Filas de prioridade e heaps
Forma completa, prioridade entre pai e filhos, construção linear e retirada do extremo.
Uma fila de prioridade permite inserir e consultar ou retirar o elemento mais prioritário. Não oferece, por si só, pesquisa eficiente de uma chave arbitrária nem um percurso ordenado. Uma heap binária é uma representação deste TAD.
Assume vetores e índices a partir de zero da página de C++. A novidade são os dois invariantes que permitem guardar a árvore sem apontadores e reparar a ordem com subidas e descidas.
Dois invariantes independentes
Uma max-heap tem a forma de uma árvore binária completa e cada pai tem valor maior ou igual ao dos filhos. A forma permite guardar os nós por níveis num vetor sem buracos; a ordem garante que o máximo está na raiz. Não obriga o filho esquerdo a ser menor que o direito, nem impõe a ordem de uma BST.
Com índices a partir de zero, os filhos de i são 2*i+1 e 2*i+2, quando essas posições existem. O pai de i>0 é (i-1)/2. Numa convenção a partir de um, as fórmulas mudam para 2*i, 2*i+1 e i/2. Não mistures as duas.
O vetor [12,9,7,4,4,1] representa uma max-heap: 12 tem filhos 9 e 7; 9 tem filhos 4 e 4; 7 tem filho 1. A altura é para n>0. O vetor não está ordenado, mas qualquer descendente é no máximo o seu antepassado.
Inserir e retirar
Para inserires, acrescenta o valor no fim, conservando a forma completa. Só o caminho do novo nó até à raiz pode violar a ordem. Enquanto o valor exceder o pai, troca-os e sobe. Em [9,4,7], inserir 12 produz [9,4,7,12], depois [9,12,7,4] e finalmente [12,9,7,4].
Para retirares o máximo, guarda a raiz, põe o último elemento na raiz e reduz o tamanho. As subárvores já são heaps; só a nova raiz pode violar a prioridade. Troca-a com o maior filho enquanto esse filho for maior. Escolher sempre o esquerdo pode deixar o direito acima do pai.
O invariante da descida é que a única possível violação está no nó atual e que as subárvores abaixo dele são heaps. A troca com o maior filho repõe a ordem no nó antigo; a possível violação desce um nível. Empates não exigem troca.
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>
class Heap {
std::vector<int> a;
void descer(std::size_t i) {
while (i < a.size() / 2) {
std::size_t j = 2 * i + 1;
if (j + 1 < a.size() && a[j] < a[j + 1]) ++j;
if (a[j] <= a[i]) break;
std::swap(a[i], a[j]); i = j;
}
}
public:
explicit Heap(std::vector<int> v) : a(std::move(v)) {
for (std::size_t i = a.size() / 2; i > 0; --i) descer(i - 1);
}
bool retirar(int& x) {
if (a.empty()) return false;
x = a.front(); a.front() = a.back(); a.pop_back();
if (!a.empty()) descer(0);
return true;
}
};
int main() {
Heap h({4, 9, 1, 12, 4, 7}); int x; bool primeiro = true;
while (h.retirar(x)) { std::cout << (primeiro ? "" : " ") << x; primeiro = false; }
std::cout << "\nvazio: " << !h.retirar(x) << '\n';
}Executa o código para ver a saída e os gráficos.
retirar devolve falso no vazio, porque uma heap vazia não tem raiz para consultar. Os dois valores 4 conservam-se como elementos distintos. O custo de inserir e retirar é ; consultar o máximo custa .
Mínimo e alteração de prioridade
Numa max-heap, existe um mínimo numa folha: se um nó mínimo tiver filhos, estes não podem ser maiores que ele nem menores que o mínimo, pelo que algum descendente folha tem o mesmo valor. Isso não obriga todas as ocorrências mínimas a serem folhas, pois todos os valores podem ser iguais.
Uma folha pode estar um nível acima das folhas mais profundas. Em [9,8,1,7,6], o mínimo 1 é folha na profundidade 1; as folhas 7 e 6 estão na profundidade 2. Portanto procurar o mínimo apenas no nível mais profundo falha. Num vetor de max-heap não vazio, basta examinar as posições de folhas, [n/2,n), mas o pior caso continua linear.
Se já conheces o índice do valor a alterar, aumentar a prioridade numa max-heap só pode violar a ligação com o pai, pelo que sobe. Diminuí-la só pode violar as ligações com os filhos, pelo que desce pelo maior filho. Em [15,10,12,4,8,7], mudar o índice 4 de 8 para 20 sobe por 10 e 15, dando [20,15,12,4,10,7]. Partindo de novo do original, mudar a raiz de 15 para 6 desce por 12 e 7, dando [12,10,7,4,8,6].
A reparação custa porque percorre um caminho. Encontrar antes uma chave arbitrária pode custar . Se mantiveres um mapa de chave para índice, atualiza-o a cada troca. A vantagem do vetor para a heap vem da forma completa: calcula pai e filhos sem guardar apontadores por nó e mantém os valores contíguos em memória.
Construção em tempo linear
Inserir os elementos um a um custa no pior caso. Para construir a partir de um vetor, começa no último pai, índice n/2-1 para n>=2, e desce em cada pai até à raiz. As folhas já são heaps de um nó; quando chega a vez de um pai, os seus filhos já foram tratados.
- EntradaO último pai é o índice 2, com valor 1 e filho 7.
4911247 - Descer do índice 2Troca 1 com 7.
4971241 - Descer do índice 1Escolhe o filho 12, maior que 4, e troca com 9.
4127941 - Descer do índice 04 troca com 12 e depois com 9. A max-heap está construída.
1297441
Porque a soma converge
Há muitos nós perto das folhas, onde descer custa pouco. O número de nós com altura pelo menos h é da ordem de . O trabalho total é limitado por
As somas parciais são . Por isso a série converge para 2. O limite superior obtido ao cobrar a altura máxima a todos os nós é demasiado largo.
Ler o vetor já exige , pelo que construir é .
Heapsort
Constrói uma max-heap, troca a raiz com a última posição ativa, reduz a região da heap e volta a descer a raiz. O prefixo [0,k) continua a ser heap e o sufixo [k,n) já está ordenado. Com [12,9,7,4,4,1], trocar 12 com 1 e descer da raiz repõe [9,4,7,4,1] no prefixo e fixa 12 no fim. O sufixo contém os maiores valores nas posições finais, por ordem crescente. Ao terminar, o vetor inteiro está crescente.
Construção linear seguida de n-1 retiradas dá no pior caso. A implementação iterativa usa de espaço auxiliar sobre o vetor e não é estável. Na biblioteca, make_heap, push_heap e pop_heap operam sobre uma região; pop_heap move o extremo para o fim, mas não reduz o container. Tens de retirar o último elemento depois, se esse for o objetivo.
Prioridades, empates e top-k
std::priority_queue<int> é uma max-heap. Para min-heap declara std::priority_queue<int,std::vector<int>,std::greater<int>>. Para pares ordenados por preço, usa std::priority_queue<std::pair<int,std::string>> na max-heap e o comparador greater na min-heap. top exige heap não vazia; pop não devolve o valor. Com registos, define primeiro a prioridade e depois o desempate, por exemplo menor instante e, no empate, menor número de sequência. Sem desempate, não assumes FIFO entre prioridades iguais.
Para conservares os k maiores valores de um fluxo, mantém uma min-heap com no máximo k elementos. Se estiver cheia e o novo valor não exceder a raiz, descarta-o; caso contrário substitui a raiz. O invariante é que a heap contém os maiores k elementos processados, com multiplicidades, quando já existem pelo menos k. Tempo , espaço . Para k=0, não consultes a raiz: a resposta é vazia.
Para juntar cordas, a min-heap oferece as duas menores em cada passo. Em [4,3,2,6], junta 2 e 3, reinsere 5 e acrescenta 5 ao custo. Depois junta 4 e 5, pagando 9, e finalmente 6 e 9, pagando 15. O custo total é 5+9+15=29; a soma final 15 é o comprimento da corda, não o custo acumulado. Esta aplicação é trabalhada na ficha 12 de 2025/26.
Uma heap dá o extremo depressa, mas procurar ou alterar uma chave arbitrária pode custar sem uma estrutura auxiliar que conheça a sua posição. Alterar um valor no vetor exige voltar a subir ou descer conforme a direção da alteração.
Exercícios
As justificações e os programas em C++ são de autoavaliação. Compara o raciocínio com os critérios da solução.
Descer pelo filho certo
Retira o máximo da max-heap [15,10,12,4,8,7]. Dá o vetor final depois da descida.
Pista
O último 7 substitui a raiz.
Mais uma pista
Entre 10 e 12, o maior filho é 12.
Solução
A região ativa começa [7,10,12,4,8]. Troca 7 com 12, obtendo [12,10,7,4,8]. O nó com 7 já não tem filhos, por isso a heap final é essa. O valor devolvido é 15. A forma completa e a prioridade ficam satisfeitas.
Confere a tua resposta:
- Devolvi 15 e coloquei o último 7 na raiz.
- Escolhi o filho 12 em vez de 10.
- Obtive [12,10,7,4,8] e conferi forma completa e prioridade.
Erros frequentes
Trocar sempre com o filho esquerdo deixaria 12 acima do novo pai;
pop_heapsozinho não apaga a posição final.
Explicar a construção linear
Por que motivo construir uma heap de baixo para cima não custa Θ(n log n), apesar de cada descida ter limite O(log n)?
Pista
Poucos nós têm a altura máxima.
Mais uma pista
Cerca de metade são folhas e não descem; alturas maiores ficam geometricamente mais raras.
Solução
O trabalho soma O(n×Σ(h/2^h))=O(n), porque a série converge. Ler e organizar n elementos exige Ω(n), dando Θ(n). Ou seja, o limite O(n log n) é válido mas não apertado para esta construção. Inserir um a um é um processo diferente e pode ter esse custo.
Confere a tua resposta:
- Contei muitos nós baixos e poucos nós com descidas longas.
- Usei a série convergente para obter O(n), sem cobrar log n a todos.
- Combinei com o limite inferior linear para concluir Θ(n).
Erros frequentes
Não multipliques o número de nós pela descida máxima sem considerar a distribuição de alturas; um limite superior não prova Θ.
Escolher min-heap para top-k
Mantém os três maiores valores do fluxo [5,1,9,5,12,2]. Indica o multiconjunto final e o tipo de heap que facilita descartar candidatos.
Pista
O extremo que queres consultar é o menor dos valores conservados.
Mais uma pista
Depois de conservar 5,1,9, o próximo 5 substitui 1.
Solução
Usa uma min-heap de capacidade 3, porque o candidato a descartar é o menor dos conservados. Estados por multiconjunto: {5}, {1,5}, {1,5,9}, {5,5,9}, {5,9,12}; 2 é descartado. A resposta é {5,9,12}. Há no máximo O(log k) trabalho por valor e O(k) memória. Empates são conservados com multiplicidade conforme a política.
Confere a tua resposta:
- Escolhi min-heap com no máximo três elementos.
- Substituí 1 pelo segundo 5 e um dos 5 por 12; descartei 2.
- Obtive o multiconjunto 5,9,12 e mantive multiplicidades durante o traço.
Erros frequentes
Uma max-heap mostra o maior, que não é o candidato a retirar; k=0 tem de ser tratado antes de consultar a raiz.
Juntar sempre as duas menores
Com cordas [4,3,2,6], cada junção custa a soma dos dois comprimentos
e produz uma corda desse comprimento, que volta a ficar disponível.
Junta sempre as duas menores com uma min-heap. Qual é o custo acumulado?
Mostra os valores disponíveis após cada junção.
Pista
Mais uma pista
Solução
Os multiconjuntos disponíveis são {2,3,4,6}, {4,5,6}, {6,9} e
{15}. As junções custam 5, 9 e 15, dando 5+9+15=29.
A ordem física da heap não tem de ser crescente: basta o mínimo na
raiz e a prioridade entre pais e filhos. std::greater<int> permite
usar std::priority_queue como min-heap.
Erros frequentes
Juntar as maiores primeiro aumenta o custo; max-heap sem adaptação escolhe o lado errado.
Procurar o mínimo e reparar uma prioridade conhecida
Refuta «o mínimo de uma max-heap está sempre no nível mais profundo» com
cinco valores distintos. Depois parte da max-heap [15,10,12,4,8,7], com
índices a partir de zero. Muda o índice 4 para 20 e repõe os invariantes.
Partindo novamente do original, muda a raiz para 6 e repõe-os. Dá os dois
vetores finais e os custos com índice conhecido ou desconhecido.
Solução
[9,8,1,7,6] é max-heap, mas o mínimo 1 é folha de profundidade 1; 7 e 6
estão à profundidade 2. No primeiro ajuste, 20 sobe sobre 10 e 15, dando
[20,15,12,4,10,7]. No segundo, 6 desce primeiro por 12 e depois por 7,
dando [12,10,7,4,8,6]. A forma completa não muda; cada pai fica maior ou
igual aos filhos. Com índice conhecido, reparar custa O(log n); localizar
antes uma chave arbitrária pode custar O(n). Uma estrutura que guarde
índices precisa de os atualizar depois de cada troca. Com empates, podem
existir mínimos em nós internos, por exemplo se todos forem iguais.
Confere a tua resposta:
- Dei uma max-heap de cinco valores cujo mínimo não está no nível mais profundo.
- Reparei o aumento pela subida e a diminuição pela descida, partindo do original em cada caso.
- Distingui índice conhecido de pesquisa arbitrária e preservei o multiconjunto após a alteração.