Heap Binário

Implemente um Heap Binário em Rust com min-heap e max-heap, representação por array, heapify, insert e extract. Conheça BinaryHeap da stdlib e aplique em simulações de eventos.

Introdução

O Heap Binário é uma árvore binária completa com uma propriedade especial de ordenação: em um max-heap, o valor de cada nó é maior ou igual aos valores de seus filhos; em um min-heap, é menor ou igual. O heap é a estrutura de dados ideal para implementar filas de prioridade, onde sempre precisamos acessar o elemento de maior (ou menor) prioridade de forma eficiente.

A característica mais elegante do heap binário é que ele pode ser representado inteiramente por um array (ou Vec em Rust), sem necessidade de ponteiros. Essa representação compacta oferece excelente localidade de cache e simplicidade de implementação. A biblioteca padrão de Rust fornece BinaryHeap<T>, que é um max-heap pronto para uso em produção.


Conceito e Teoria

Propriedades do Heap

    Max-Heap:                     Min-Heap:
    (pai >= filhos)               (pai <= filhos)

         [90]                        [5]
        /    \                      /   \
      [80]   [70]                [10]   [15]
     /   \   /                  /   \   /
   [50] [60][30]             [20] [30][25]

    Maior no topo ↑              Menor no topo ↑

Propriedade estrutural: O heap é uma árvore binária completa — todos os níveis estão preenchidos, exceto possivelmente o último, que é preenchido da esquerda para a direita.

Representação por Array

A grande sacada do heap binário é que a árvore completa pode ser armazenada em um array sem desperdício de espaço:

    Árvore:           Array (índice 0):
         [90]         [90, 80, 70, 50, 60, 30]
        /    \          0   1   2   3   4   5
      [80]   [70]
     /   \   /
   [50] [60][30]

    Fórmulas de navegação (índice começando em 0):
    ┌─────────────────────────────────────────────┐
    │  Pai de i:           (i - 1) / 2            │
    │  Filho esquerdo de i: 2 * i + 1             │
    │  Filho direito de i:  2 * i + 2             │
    └─────────────────────────────────────────────┘

    Exemplo para o nó [80] (índice 1):
    - Pai: (1-1)/2 = 0 → [90]  ✓
    - Filho esq: 2*1+1 = 3 → [50]  ✓
    - Filho dir: 2*1+2 = 4 → [60]  ✓

Operação: Inserir (sift-up / bubble-up)

Insere no final do array e “sobe” o elemento até a posição correta:

    Inserir 95 no max-heap [90, 80, 70, 50, 60, 30]:

    Passo 1: Adicionar no final
    [90, 80, 70, 50, 60, 30, 95]
                                ^

    Passo 2: 95 > pai(70)? Sim → troca
    [90, 80, 95, 50, 60, 30, 70]
              ^               ^

    Passo 3: 95 > pai(90)? Sim → troca
    [95, 80, 90, 50, 60, 30, 70]
      ^       ^

    Passo 4: Chegou na raiz → parou!

    Resultado:
         [95]
        /    \
      [80]   [90]
     /   \   /  \
   [50] [60][30][70]

Operação: Extrair Máximo (sift-down / bubble-down)

Remove a raiz, coloca o último elemento no topo e “desce” até a posição correta:

    Extrair máximo do max-heap [95, 80, 90, 50, 60, 30, 70]:

    Passo 1: Salvar raiz (95), mover último para raiz
    [70, 80, 90, 50, 60, 30]
      ^

    Passo 2: 70 < maior filho (90)? Sim → troca com 90
    [90, 80, 70, 50, 60, 30]
      ^       ^

    Passo 3: 70 < maior filho (30)? Não → parou!

    Resultado: retorna 95
         [90]
        /    \
      [80]   [70]
     /   \   /
   [50] [60][30]

Heapify: Construir Heap a partir de Array

Em vez de inserir elementos um a um (O(n log n)), podemos construir o heap em O(n) aplicando sift-down de baixo para cima:

    Array desordenado: [30, 50, 90, 10, 80, 70, 60]

    Heapify (max-heap) — processa de trás para frente:
    Índice 2: [30, 50, 90, ...] → 90 OK (filhos são 70, 60)
    Índice 1: [30, 80, 90, 10, 50, ...] → swap 50↔80
    Índice 0: [90, 80, 70, 10, 50, 30, 60] → swap 30↔90, sift down

    Resultado: [90, 80, 70, 10, 50, 30, 60]

Complexidade

OperaçãoComplexidade
InserirO(log n)
Extrair máx/mínO(log n)
Peek (ver topo)O(1)
Heapify (construir)O(n)
Busca arbitráriaO(n)
EspaçoO(n)

Nota: O heap NÃO é eficiente para busca arbitrária (O(n)). Ele é otimizado para acessar e remover o elemento extremo (máximo ou mínimo).


Implementação em Rust

use std::fmt::Display;

/// Heap Binário genérico com suporte a min-heap e max-heap
#[derive(Debug)]
struct HeapBinario<T: Ord + Display> {
    dados: Vec<T>,
    eh_max_heap: bool, // true = max-heap, false = min-heap
}

impl<T: Ord + Display> HeapBinario<T> {
    /// Cria um max-heap vazio
    fn novo_max() -> Self {
        HeapBinario {
            dados: Vec::new(),
            eh_max_heap: true,
        }
    }

    /// Cria um min-heap vazio
    fn novo_min() -> Self {
        HeapBinario {
            dados: Vec::new(),
            eh_max_heap: false,
        }
    }

    /// Retorna o número de elementos
    fn tamanho(&self) -> usize {
        self.dados.len()
    }

    /// Verifica se o heap está vazio
    fn esta_vazio(&self) -> bool {
        self.dados.is_empty()
    }

    /// Retorna referência ao elemento no topo sem remover
    fn peek(&self) -> Option<&T> {
        self.dados.first()
    }

    /// Compara dois elementos respeitando o tipo de heap
    fn deve_subir(&self, filho: &T, pai: &T) -> bool {
        if self.eh_max_heap {
            filho > pai // Max-heap: maior sobe
        } else {
            filho < pai // Min-heap: menor sobe
        }
    }

    /// Índice do pai
    fn pai(i: usize) -> usize {
        (i - 1) / 2
    }

    /// Índice do filho esquerdo
    fn filho_esquerdo(i: usize) -> usize {
        2 * i + 1
    }

    /// Índice do filho direito
    fn filho_direito(i: usize) -> usize {
        2 * i + 2
    }

    /// Insere um elemento no heap
    fn inserir(&mut self, valor: T) {
        self.dados.push(valor);
        self.subir(self.dados.len() - 1);
    }

    /// Sift-up: sobe o elemento na posição i até restaurar a propriedade do heap
    fn subir(&mut self, mut i: usize) {
        while i > 0 {
            let pai = Self::pai(i);
            if self.deve_subir(&self.dados[i], &self.dados[pai]) {
                self.dados.swap(i, pai);
                i = pai;
            } else {
                break;
            }
        }
    }

    /// Extrai o elemento do topo (máximo em max-heap, mínimo em min-heap)
    fn extrair(&mut self) -> Option<T> {
        if self.dados.is_empty() {
            return None;
        }

        let ultimo = self.dados.len() - 1;
        self.dados.swap(0, ultimo);
        let extraido = self.dados.pop();

        if !self.dados.is_empty() {
            self.descer(0);
        }

        extraido
    }

    /// Sift-down: desce o elemento na posição i até restaurar a propriedade do heap
    fn descer(&mut self, mut i: usize) {
        let n = self.dados.len();
        loop {
            let esq = Self::filho_esquerdo(i);
            let dir = Self::filho_direito(i);
            let mut alvo = i;

            // Encontra o filho que deveria estar mais acima
            if esq < n && self.deve_subir(&self.dados[esq], &self.dados[alvo]) {
                alvo = esq;
            }
            if dir < n && self.deve_subir(&self.dados[dir], &self.dados[alvo]) {
                alvo = dir;
            }

            if alvo != i {
                self.dados.swap(i, alvo);
                i = alvo;
            } else {
                break;
            }
        }
    }

    /// Constrói um heap a partir de um vetor existente em O(n)
    fn heapify(dados: Vec<T>, eh_max_heap: bool) -> Self {
        let mut heap = HeapBinario {
            dados,
            eh_max_heap,
        };

        // Aplica sift-down de baixo para cima (ignora folhas)
        let n = heap.dados.len();
        if n > 1 {
            for i in (0..n / 2).rev() {
                heap.descer(i);
            }
        }

        heap
    }

    /// Exibe o heap como árvore (formato visual)
    fn exibir(&self) {
        if self.dados.is_empty() {
            println!("  (vazio)");
            return;
        }
        let tipo = if self.eh_max_heap { "Max-Heap" } else { "Min-Heap" };
        println!("  {} - Array: {:?}",
            tipo,
            self.dados.iter().map(|x| format!("{}", x)).collect::<Vec<_>>()
        );
    }
}

/// Implementação do Heap Sort usando o heap binário
fn heap_sort<T: Ord + Display>(mut dados: Vec<T>) -> Vec<T> {
    let mut heap = HeapBinario::heapify(dados, true); // Max-heap
    let mut ordenado = Vec::with_capacity(heap.tamanho());

    while let Some(max) = heap.extrair() {
        ordenado.push(max);
    }

    ordenado.reverse(); // Inverte para ordem crescente
    ordenado
}

fn main() {
    println!("=== Max-Heap ===");
    let mut max_heap = HeapBinario::novo_max();

    for v in [40, 10, 70, 30, 90, 20, 80, 50, 60] {
        max_heap.inserir(v);
    }
    max_heap.exibir();
    println!("  Topo (máximo): {:?}", max_heap.peek());

    println!("\n  Extraindo em ordem:");
    while let Some(v) = max_heap.extrair() {
        print!("  {} ", v);
    }
    println!();

    println!("\n=== Min-Heap ===");
    let mut min_heap = HeapBinario::novo_min();

    for v in [40, 10, 70, 30, 90, 20, 80, 50, 60] {
        min_heap.inserir(v);
    }
    min_heap.exibir();
    println!("  Topo (mínimo): {:?}", min_heap.peek());

    println!("\n=== Heapify (construção em O(n)) ===");
    let dados = vec![30, 50, 90, 10, 80, 70, 60];
    println!("  Array original: {:?}", dados);
    let heap = HeapBinario::heapify(dados, true);
    heap.exibir();

    println!("\n=== Heap Sort ===");
    let desordenado = vec![64, 25, 12, 22, 11, 90, 45, 33];
    println!("  Entrada: {:?}", desordenado);
    let ordenado = heap_sort(desordenado);
    println!("  Saída:   {:?}", ordenado);
}

Métodos Principais

MétodoDescrição
novo_max()Cria um max-heap vazio
novo_min()Cria um min-heap vazio
inserir(valor)Insere e restaura a propriedade com sift-up
extrair()Remove e retorna o elemento do topo
peek()Retorna referência ao topo sem remover
heapify(vec, tipo)Constrói heap a partir de vetor em O(n)
tamanho()Retorna o número de elementos
esta_vazio()Verifica se o heap está vazio
subir(i)Sift-up: restaura propriedade subindo o elemento
descer(i)Sift-down: restaura propriedade descendo o elemento

BinaryHeap na Biblioteca Padrão

use std::collections::BinaryHeap;
use std::cmp::Reverse;

fn main() {
    // Max-heap (padrão)
    let mut max_heap = BinaryHeap::new();
    max_heap.push(30);
    max_heap.push(10);
    max_heap.push(50);
    max_heap.push(20);

    println!("Max-heap - topo: {:?}", max_heap.peek()); // Some(50)

    // Min-heap usando Reverse
    let mut min_heap = BinaryHeap::new();
    min_heap.push(Reverse(30));
    min_heap.push(Reverse(10));
    min_heap.push(Reverse(50));
    min_heap.push(Reverse(20));

    println!("Min-heap - topo: {:?}", min_heap.peek()); // Some(Reverse(10))

    // Extrair em ordem de prioridade
    while let Some(Reverse(v)) = min_heap.pop() {
        print!("{} ", v); // 10 20 30 50
    }
    println!();
}

Exemplo Prático: Simulação de Fila de Tarefas com Prioridade

use std::collections::BinaryHeap;
use std::cmp::Ordering;

/// Tarefa com prioridade e prazo
#[derive(Debug, Clone, Eq, PartialEq)]
struct Tarefa {
    nome: String,
    prioridade: u8,    // 1 = baixa, 5 = crítica
    prazo_minutos: u32, // Prazo em minutos
}

impl Tarefa {
    fn nova(nome: &str, prioridade: u8, prazo: u32) -> Self {
        Tarefa {
            nome: nome.to_string(),
            prioridade,
            prazo_minutos: prazo,
        }
    }

    fn descricao_prioridade(&self) -> &str {
        match self.prioridade {
            1 => "Baixa",
            2 => "Normal",
            3 => "Alta",
            4 => "Urgente",
            5 => "Crítica",
            _ => "Desconhecida",
        }
    }
}

/// Ordenação: maior prioridade primeiro; em caso de empate, menor prazo primeiro
impl Ord for Tarefa {
    fn cmp(&self, other: &Self) -> Ordering {
        self.prioridade
            .cmp(&other.prioridade)
            .then_with(|| other.prazo_minutos.cmp(&self.prazo_minutos))
    }
}

impl PartialOrd for Tarefa {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

fn main() {
    let mut fila = BinaryHeap::new();

    // Adicionar tarefas com diferentes prioridades
    fila.push(Tarefa::nova("Corrigir bug em produção", 5, 30));
    fila.push(Tarefa::nova("Atualizar documentação", 1, 480));
    fila.push(Tarefa::nova("Implementar nova feature", 3, 240));
    fila.push(Tarefa::nova("Revisar pull request", 3, 60));
    fila.push(Tarefa::nova("Deploy para staging", 4, 120));
    fila.push(Tarefa::nova("Responder e-mails", 2, 360));
    fila.push(Tarefa::nova("Reunião com cliente", 4, 45));

    println!("=== Fila de Tarefas (ordem de execução) ===\n");
    println!("{:<35} {:>10} {:>15}", "Tarefa", "Prioridade", "Prazo (min)");
    println!("{}", "-".repeat(62));

    let mut tempo_total = 0u32;
    while let Some(tarefa) = fila.pop() {
        tempo_total += tarefa.prazo_minutos;
        println!(
            "{:<35} {:>10} {:>15}",
            tarefa.nome,
            tarefa.descricao_prioridade(),
            tarefa.prazo_minutos
        );
    }

    println!("{}", "-".repeat(62));
    println!(
        "Tempo total estimado: {} horas e {} minutos",
        tempo_total / 60,
        tempo_total % 60
    );
}

Comparação com Outras Estruturas

CritérioHeap BinárioLista OrdenadaBST BalanceadaArray Desordenado
InserirO(log n)O(n)O(log n)O(1)
Extrair extremoO(log n)O(1)O(log n)O(n)
Peek (ver extremo)O(1)O(1)O(log n)O(n)
Busca arbitráriaO(n)O(n)O(log n)O(n)
Construção (heapify)O(n)O(n log n)O(n log n)O(1)
Uso de memóriaCompactoPonteirosPonteirosCompacto
Cache-friendlySimNãoNãoSim

Quando usar Heap Binário?

  • Use quando precisa de acesso rápido ao maior ou menor elemento.
  • Use para filas de prioridade (agendadores, simulações, algoritmos de grafos como Dijkstra).
  • Use para o algoritmo Heap Sort (ordenação in-place O(n log n)).
  • Não use se precisa buscar elementos arbitrários — use BST ou HashMap.
  • Não use se precisa de dados totalmente ordenados — use BTreeSet ou Vec ordenado.

Exercícios

Exercício 1: Mediana Contínua

Implemente uma estrutura que mantém dois heaps (um max-heap e um min-heap) para calcular a mediana de um fluxo contínuo de números. O max-heap guarda a metade inferior e o min-heap guarda a metade superior. A mediana é obtida em O(1) e cada inserção custa O(log n).

Exercício 2: K Maiores Elementos

Dada uma lista de n números, encontre os k maiores elementos usando um min-heap de tamanho k. Isso é mais eficiente que ordenar toda a lista quando k « n. Implemente e compare o desempenho com a abordagem de ordenação completa.

Exercício 3: Merge de K Listas Ordenadas

Dadas k listas ordenadas, combine-as em uma única lista ordenada usando um min-heap. Cada elemento do heap deve conter o valor e o índice da lista de origem. A complexidade deve ser O(n * log k), onde n é o total de elementos.

Entrada:
  Lista 1: [1, 4, 7]
  Lista 2: [2, 5, 8]
  Lista 3: [3, 6, 9]

Saída: [1, 2, 3, 4, 5, 6, 7, 8, 9]

Conclusão

O heap binário é uma estrutura de dados surpreendentemente simples e poderosa. Sua representação por array elimina a necessidade de ponteiros e oferece excelente localidade de cache, enquanto as operações de sift-up e sift-down mantêm a propriedade de ordenação parcial com eficiência O(log n).

Em Rust, o BinaryHeap da biblioteca padrão é a escolha ideal para filas de prioridade em produção. Para min-heaps, o wrapper Reverse oferece uma solução idiomática e sem custo de performance. Compreender o heap binário é fundamental, pois ele aparece como componente em diversos algoritmos clássicos: Dijkstra para caminhos mínimos, Prim para árvore geradora mínima, Huffman para compressão, e o próprio Heap Sort para ordenação.