Filas de prioridade e heaps

Forma completa, prioridade entre pai e filhos, construção linear e retirada do extremo.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

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 é ⌊log⁡2n⌋\lfloor\log_2 n\rfloor 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.

Construir e retirar de um heap
#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';
}
Resultado

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 é O(log⁡(n+1))O(\log(n+1)); consultar o máximo custa O(1)O(1).

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 O(log⁡n)O(\log n) porque percorre um caminho. Encontrar antes uma chave arbitrária pode custar O(n)O(n). 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 O(nlog⁡n)O(n\log n) 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.

  1. Entrada
    4911247
    O último pai é o índice 2, com valor 1 e filho 7.
  2. Descer do índice 2
    4971241
    Troca 1 com 7.
  3. Descer do índice 1
    4127941
    Escolhe o filho 12, maior que 4, e troca com 9.
  4. Descer do índice 0
    1297441
    4 troca com 12 e depois com 9. A max-heap está construída.
Cada descida deixa uma subárvore em ordem. No último passo, a subárvore é a árvore inteira.
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 n/2hn/2^h. O trabalho total é limitado por

O(n∑h≥1h2h)=O(n).O\left(n\sum_{h\ge1}\frac{h}{2^h}\right)=O(n).

As somas parciais são ∑h=1Hh/2h=2−(H+2)/2H<2\sum_{h=1}^{H}h/2^h=2-(H+2)/2^H<2. Por isso a série converge para 2. O limite superior nlog⁡nn\log n obtido ao cobrar a altura máxima a todos os nós é demasiado largo.

Ler o vetor já exige Ω(n)\Omega(n), pelo que construir é Θ(n)\Theta(n).

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á O(nlog⁡n)O(n\log n) no pior caso. A implementação iterativa usa O(1)O(1) 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 O(nlog⁡(k+1))O(n\log(k+1)), espaço O(k)O(k). 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 O(n)O(n) 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_heap

sozinho não apaga a posição final.

Voltar à explicação
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 Θ.

Voltar à explicação
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.

Voltar à explicação
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
O próximo par é formado pelos dois mínimos ainda disponíveis.
Mais uma pista
Soma l1+l2 ao custo e reinsere a soma.
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.

Voltar à explicação
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.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.