Pular para o conteúdo

VecDeque e BinaryHeap em Rust: filas e filas de prioridade

Pense na fila de uma padaria. Quem chega entra no fim, e o atendente chama quem está na frente. Agora pense na sala de espera de um pronto-socorro. Lá, o paciente mais grave é atendido primeiro, não importa a hora em que chegou. As duas são “filas”, mas seguem regras diferentes.

O Rust tem uma coleção para cada uma. Um VecDeque é uma fila rápida em que você pode colocar e tirar itens nas duas pontas. Um BinaryHeap é uma fila de prioridade: ele sempre entrega o maior (ou mais urgente) item primeiro. Você já conhece os vetores, e a página de HashSet já mostrou o VecDeque por cima. Esta página vai mais fundo e ainda apresenta o BinaryHeap.

Você pode usar um Vec como fila. Coloca com push e tira da frente com remove(0). Funciona. O problema é o que acontece por trás.

Um Vec guarda os itens todos juntinhos, a partir da posição 0. Quando você remove o primeiro item, todos os outros precisam andar uma casa para a esquerda para fechar o buraco. Com 3 itens, ninguém percebe. Com 100.000 itens, cada remove(0) move 99.999 valores. É isso que “O(n)” quer dizer: o trabalho cresce junto com o tamanho da lista.

O VecDeque (pronuncia-se “vec dek”, abreviação de double-ended queue, ou fila de duas pontas) resolve isso com um ring buffer (buffer circular). Imagine uma mesa redonda com cadeiras numeradas e uma plaquinha dizendo “a fila começa aqui”. Quando a primeira pessoa sai, ninguém se levanta. Você só passa a plaquinha para a cadeira seguinte. Quando alguém novo chega e a última cadeira está ocupada, ele senta na cadeira 0, se ela estiver livre, dando a volta na mesa. Colocar ou tirar em qualquer ponta é só “mudar a plaquinha” ou “ocupar uma cadeira”, então leva o mesmo tempinho, não importa o tamanho da fila. Isso é “O(1)”.

use std::collections::VecDeque;
fn main() {
// Um Vec usado como fila: funciona, mas remove(0) empurra todo mundo para a esquerda.
let mut vec_line = vec!["Ana", "Ben", "Cai"];
let served = vec_line.remove(0);
println!("Vec atendeu {served}, ainda esperando: {:?}", vec_line);
// Um VecDeque: pop_front só move a marcação de "início".
let mut deque_line = VecDeque::from(vec!["Ana", "Ben", "Cai"]);
let served = deque_line.pop_front();
println!("VecDeque atendeu {:?}, ainda esperando: {:?}", served, deque_line);
}

Saída:

Vec atendeu Ana, ainda esperando: ["Ben", "Cai"]
VecDeque atendeu Some("Ana"), ainda esperando: ["Ben", "Cai"]
▶ Experimente você mesmo

O resultado parece igual. A diferença está na velocidade, e ela só aparece quando a fila fica longa. Repare em mais uma coisa: pop_front retorna um Option. Se a fila estiver vazia, você recebe None em vez de o programa quebrar.

O VecDeque fica em std::collections, então você precisa de uma linha use. Depois disso, os métodos são bem diretos: push (colocar) ou pop (tirar), na frente (front) ou atrás (back).

use std::collections::VecDeque;
fn main() {
let mut deque: VecDeque<i32> = VecDeque::new();
deque.push_back(2);
deque.push_back(3);
deque.push_front(1);
println!("{:?}", deque);
println!("frente: {:?}, fim: {:?}", deque.front(), deque.back());
println!("tamanho: {}, vazio: {}", deque.len(), deque.is_empty());
println!("deque[1] = {}", deque[1]);
println!("get(9) = {:?}", deque.get(9));
let first = deque.pop_front();
let last = deque.pop_back();
println!("tirei {:?} e {:?}, sobrou: {:?}", first, last, deque);
}

Saída:

[1, 2, 3]
frente: Some(1), fim: Some(3)
tamanho: 3, vazio: false
deque[1] = 2
get(9) = None
tirei Some(1) e Some(3), sobrou: [2]
▶ Experimente você mesmo

front e back deixam você olhar sem remover nada. Acessar por índice com deque[1] funciona igualzinho a um Vec, e dá panic se o índice for grande demais. Por isso, use get quando não tiver certeza.

Você também pode criar um VecDeque a partir de um Vec, percorrê-lo com um loop e transformá-lo de volta em Vec ou em slice quando outra função precisar:

use std::collections::VecDeque;
fn main() {
let mut playlist = VecDeque::from(vec!["Intro", "Verso", "Refrão"]);
playlist.push_front("Contagem");
playlist.push_back("Final");
for (i, song) in playlist.iter().enumerate() {
println!("{}. {}", i + 1, song);
}
// Precisa de uma slice normal? make_contiguous coloca tudo em sequência na memória.
let songs = playlist.make_contiguous();
songs.sort();
println!("ordenado: {:?}", songs);
// Ou transforme tudo de volta em um Vec.
let as_vec: Vec<&str> = playlist.into();
println!("como Vec: {:?}", as_vec);
}

Saída:

1. Contagem
2. Intro
3. Verso
4. Refrão
5. Final
ordenado: ["Contagem", "Final", "Intro", "Refrão", "Verso"]
como Vec: ["Contagem", "Final", "Intro", "Refrão", "Verso"]
▶ Experimente você mesmo

Por que o make_contiguous é necessário? Lembre da mesa redonda: depois de algumas voltas, os itens podem estar em dois pedaços separados da memória. O make_contiguous junta tudo em um pedaço só e entrega uma slice normal, para você poder usar ferramentas de slice como o sort. Você não vai precisar disso com frequência, mas é bom saber que existe.

Aqui estão três tarefas do dia a dia em que um VecDeque encaixa perfeitamente.

Uma fila de tarefas “primeiro a entrar, primeiro a sair”. Os trabalhos de impressão esperam na fila. Os novos vão para o fim, e a impressora pega da frente. Um trabalho urgente ainda pode furar a fila com push_front:

use std::collections::VecDeque;
fn main() {
let mut print_queue = VecDeque::new();
print_queue.push_back("relatorio.pdf");
print_queue.push_back("foto.jpg");
print_queue.push_back("fatura.pdf");
// Urgente! Este vai direto para a frente da fila.
print_queue.push_front("cartao-de-embarque.pdf");
while let Some(job) = print_queue.pop_front() {
println!("Imprimindo {job} ({} ainda esperando)", print_queue.len());
}
println!("Tudo pronto. Fila vazia: {}", print_queue.is_empty());
}

Saída:

Imprimindo cartao-de-embarque.pdf (3 ainda esperando)
Imprimindo relatorio.pdf (2 ainda esperando)
Imprimindo foto.jpg (1 ainda esperando)
Imprimindo fatura.pdf (0 ainda esperando)
Tudo pronto. Fila vazia: true
▶ Experimente você mesmo

while let Some(job) = ...pop_front() é o loop clássico de fila. Ele continua até pop_front retornar None, o que significa que a fila esvaziou. (Veja if let se esse formato for novidade para você.)

Um histórico dos “últimos N itens”. Uma caixa de busca que lembra suas 3 pesquisas mais recentes. Quando ela enche, a mais antiga cai pela frente:

use std::collections::VecDeque;
const MAX_RECENT: usize = 3;
fn remember(recent: &mut VecDeque<String>, search: &str) {
if recent.len() == MAX_RECENT {
recent.pop_front(); // esquece a pesquisa mais antiga
}
recent.push_back(search.to_string());
}
fn main() {
let mut recent = VecDeque::with_capacity(MAX_RECENT);
for search in ["livro de rust", "vecdeque", "binary heap", "pizza perto de mim"] {
remember(&mut recent, search);
println!("{:?}", recent);
}
// Mostra a mais nova primeiro, como uma caixa de busca faria.
let newest_first: Vec<&String> = recent.iter().rev().collect();
println!("mais novas primeiro: {:?}", newest_first);
}

Saída:

["livro de rust"]
["livro de rust", "vecdeque"]
["livro de rust", "vecdeque", "binary heap"]
["vecdeque", "binary heap", "pizza perto de mim"]
mais novas primeiro: ["pizza perto de mim", "binary heap", "vecdeque"]
▶ Experimente você mesmo

Uma média em janela deslizante. Um termômetro manda uma leitura por minuto, e você quer a média das 3 últimas. Cada leitura nova entra no fim, e a mais antiga sai pela frente. Manter uma soma corrente (sum) evita que você tenha que somar a janela inteira de novo a cada passo:

use std::collections::VecDeque;
fn main() {
let readings = [20.0, 22.0, 24.0, 23.0, 19.0, 21.0];
let window_size = 3;
let mut window: VecDeque<f64> = VecDeque::new();
let mut sum = 0.0;
for reading in readings {
window.push_back(reading);
sum += reading;
if window.len() > window_size {
sum -= window.pop_front().unwrap(); // a leitura mais antiga sai
}
if window.len() == window_size {
let average = sum / window_size as f64;
println!("{:?} -> média {:.1}", window, average);
}
}
}

Saída:

[20.0, 22.0, 24.0] -> média 22.0
[22.0, 24.0, 23.0] -> média 23.0
[24.0, 23.0, 19.0] -> média 22.0
[23.0, 19.0, 21.0] -> média 21.0
▶ Experimente você mesmo

O unwrap() é seguro aqui: só tiramos um item quando a janela tem mais de 3, então ela não pode estar vazia.

Agora, a sala de espera do pronto-socorro. Às vezes você não quer saber quem chegou primeiro. Você quer “me dê a coisa mais importante agora”, de novo e de novo, enquanto coisas novas continuam chegando.

Você poderia jogar tudo em um Vec e ordenar a cada vez. Mas ordenar tudo só para achar o item do topo é muito trabalho jogado fora. Um BinaryHeap mantém os itens organizados só mais ou menos, o suficiente para que o maior esteja sempre no topo. Colocar um item ou tirar o do topo é barato (O(log n), que cresce bem devagar: uns 17 passos para 100.000 itens).

O BinaryHeap do Rust é um max-heap: pop sempre devolve o maior valor. Aqui estão os lances de um leilão:

use std::collections::BinaryHeap;
fn main() {
let mut bids = BinaryHeap::new();
bids.push(120);
bids.push(450);
bids.push(80);
bids.push(300);
println!("maior lance: {:?}", bids.peek());
println!("número de lances: {}", bids.len());
while let Some(bid) = bids.pop() {
println!("próximo: {bid}");
}
let heap = BinaryHeap::from(vec![5, 1, 4, 2, 3]);
println!("ordenado: {:?}", heap.into_sorted_vec());
}

Saída:

maior lance: Some(450)
número de lances: 4
próximo: 450
próximo: 300
próximo: 120
próximo: 80
ordenado: [1, 2, 3, 4, 5]
▶ Experimente você mesmo

peek olha o topo sem remover. pop remove. into_sorted_vec consome o heap e devolve um Vec ordenado do menor para o maior. Assim como no VecDeque, tanto peek quanto pop retornam um Option, então um heap vazio nunca derruba o programa.

E se você quiser o menor primeiro? A parada de entrega mais próxima, o voo mais barato, o prazo mais curto. Você não precisa de outra coleção. Embrulhe cada valor em std::cmp::Reverse, que simplesmente inverte a comparação: o que era o maior agora é o menor.

use std::cmp::Reverse;
use std::collections::BinaryHeap;
fn main() {
// Paradas de entrega, por distância em km. Sempre vá para a mais próxima.
let mut stops = BinaryHeap::new();
for km in [12, 3, 8, 5] {
stops.push(Reverse(km));
}
if let Some(Reverse(closest)) = stops.peek() {
println!("parada mais próxima: {closest} km");
}
while let Some(Reverse(km)) = stops.pop() {
println!("dirija {km} km");
}
}

Saída:

parada mais próxima: 3 km
dirija 3 km
dirija 5 km
dirija 8 km
dirija 12 km
▶ Experimente você mesmo

Reverse(km) é uma struct bem pequena que só embrulha o valor. O pattern Some(Reverse(km)) desembrulha as duas camadas de uma vez, e você recebe o número puro de volta.

Tarefas de verdade não são só números. Elas têm um nome, uma prioridade, talvez uma hora de chegada. Para colocar sua própria struct em um BinaryHeap, o Rust precisa saber comparar duas delas, ou seja, a struct precisa implementar Ord (veja Traits comuns).

O jeito mais rápido é o derive. Uma coisa importante: o derive compara os campos de cima para baixo. Ele compara o primeiro campo e só olha o segundo se os primeiros forem iguais. Então coloque primeiro o campo pelo qual você quer ordenar:

use std::collections::BinaryHeap;
// A ordem dos campos importa: o derive compara `priority` primeiro, depois `name`.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Task {
priority: u8,
name: String,
}
fn main() {
let mut todo = BinaryHeap::new();
todo.push(Task { priority: 2, name: "marcar dentista".to_string() });
todo.push(Task { priority: 5, name: "consertar o servidor fora do ar".to_string() });
todo.push(Task { priority: 1, name: "regar as plantas".to_string() });
todo.push(Task { priority: 2, name: "responder e-mail".to_string() });
while let Some(task) = todo.pop() {
println!("[{}] {}", task.priority, task.name);
}
}

Saída:

[5] consertar o servidor fora do ar
[2] responder e-mail
[2] marcar dentista
[1] regar as plantas
▶ Experimente você mesmo

Se você trocasse os dois campos de lugar e name viesse primeiro, o heap ordenaria as tarefas em ordem alfabética e quase ignoraria a prioridade. Erro fácil de cometer, bug silencioso.

Olhe as duas tarefas com prioridade 2. “marcar dentista” chegou antes, mas “responder e-mail” saiu primeiro. O empate foi decidido pelo name, e “responder” ganha de “marcar” porque vem depois no alfabeto. É previsível, mas provavelmente não é o que você quer. Normalmente, no empate, a tarefa que chegou primeiro deve sair primeiro. Para uma regra personalizada assim, escreva o Ord você mesmo:

use std::cmp::Ordering;
use std::collections::BinaryHeap;
#[derive(Debug, PartialEq, Eq)]
struct Job {
priority: u8,
id: u32, // ordem de chegada: um id menor chegou antes
name: String,
}
impl Ord for Job {
fn cmp(&self, other: &Self) -> Ordering {
self.priority
.cmp(&other.priority) // a prioridade maior ganha
.then_with(|| other.id.cmp(&self.id)) // no empate, ganha o id mais antigo
}
}
impl PartialOrd for Job {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn main() {
let mut jobs = BinaryHeap::new();
jobs.push(Job { priority: 2, id: 1, name: "marcar dentista".to_string() });
jobs.push(Job { priority: 5, id: 2, name: "consertar o servidor fora do ar".to_string() });
jobs.push(Job { priority: 1, id: 3, name: "regar as plantas".to_string() });
jobs.push(Job { priority: 2, id: 4, name: "responder e-mail".to_string() });
while let Some(job) = jobs.pop() {
println!("[{}] #{} {}", job.priority, job.id, job.name);
}
}

Saída:

[5] #2 consertar o servidor fora do ar
[2] #1 marcar dentista
[2] #4 responder e-mail
[1] #3 regar as plantas
▶ Experimente você mesmo

O truque está no then_with: se as prioridades forem iguais, os ids são comparados ao contrário (other.id contra self.id). Isso faz o id menor contar como “maior”, então ele sai primeiro do max-heap. Não se preocupe se isso levar um minutinho para fazer sentido. Inverter a comparação é exatamente o que o Reverse faz por você, só que escrito à mão. O bloco do PartialOrd é sempre o mesmo código padrão: ele só chama o seu cmp.

Um algoritmo de verdade: as 3 maiores pontuações

Seção intitulada “Um algoritmo de verdade: as 3 maiores pontuações”

Digamos que você tenha uma lista enorme de pontuações de um jogo e só queira as 3 maiores. Ordenar a lista inteira funciona, mas existe um truque esperto que nunca guarda mais do que alguns itens.

Use um min-heap de tamanho 3. Coloque cada pontuação. Quando o heap ficar com 4 itens, tire um. Como é um min-heap, o que você joga fora é o menor. No final, só sobram as 3 melhores.

use std::cmp::Reverse;
use std::collections::BinaryHeap;
fn main() {
let scores = [72, 95, 64, 88, 91, 55, 99, 80];
let mut top = BinaryHeap::new(); // um min-heap: a menor pontuação fica no topo
for score in scores {
top.push(Reverse(score));
if top.len() > 3 {
top.pop(); // quatro é um a mais: descarta a menor
}
}
let best: Vec<u32> = top
.into_sorted_vec()
.into_iter()
.map(|Reverse(score)| score)
.collect();
println!("top 3: {:?}", best);
}

Saída:

top 3: [99, 95, 91]
▶ Experimente você mesmo

Um bônus legal: into_sorted_vec ordena os valores Reverse do “menor” para o “maior”, o que, para as pontuações reais, significa do maior para o menor. Então a melhor pontuação sai primeiro, sem nenhuma ordenação extra. Esse padrão funciona com uma lista de um milhão de pontuações guardando só 4 na memória.

Aqui vai uma colinha. “Rápido” quer dizer que o tempo fica mais ou menos o mesmo, não importa o tamanho da coleção. “Lento” quer dizer que ele cresce junto com o tamanho.

Coleção Ótima para Colocar Tirar da frente / do topo Achar um item qualquer
Vec<T> Uma lista lida por posição; uma pilha Rápido no fim Lento (remove(0) empurra tudo) Lento (olha item por item)
VecDeque<T> Filas: colocar e tirar nas duas pontas Rápido nas duas pontas Rápido nas duas pontas Lento (olha item por item)
BinaryHeap<T> “Me dê o maior / mais urgente agora” Quase rápido (O(log n)) Quase rápido: sempre o maior Lento, e sem ordem definida
BTreeSet<T> Valores únicos, sempre ordenados Quase rápido (O(log n)) Quase rápido: o menor e o maior Quase rápido (O(log n))

Na dúvida, comece com um Vec. Parta para o VecDeque quando você tira coisas da frente. Parta para o BinaryHeap quando você vive perguntando “o que é mais importante agora?”. Use um BTreeSet quando precisar de valores únicos e ordenados e quiser procurar itens, não só pegar o do topo.

Achar que .iter() em um BinaryHeap vem ordenado. Um heap é organizado só mais ou menos. O maior item vem primeiro, mas o resto fica numa ordem bagunçada que é útil para o heap, não para você:

use std::collections::BinaryHeap;
fn main() {
let heap = BinaryHeap::from(vec![1, 5, 2, 8, 3, 9]);
println!("iter: {:?}", heap.iter().collect::<Vec<_>>());
println!("ordenado: {:?}", heap.clone().into_sorted_vec());
let mut heap = heap;
print!("retirados:");
while let Some(n) = heap.pop() {
print!(" {n}");
}
println!();
}

Saída:

iter: [9, 8, 2, 5, 3, 1]
ordenado: [1, 2, 3, 5, 8, 9]
retirados: 9 8 5 3 2 1
▶ Experimente você mesmo

O mesmo vale para imprimir um heap com {:?}: você vai ver a bagunça interna. Se precisar dos itens em ordem, tire um por um com pop ou chame into_sorted_vec.

Colocar uma struct sem Ord em um BinaryHeap. O heap precisa comparar os itens para saber qual vai para o topo. Se a sua struct não pode ser comparada, o Rust barra você:

use std::collections::BinaryHeap;
struct Ticket {
urgency: u8,
title: String,
}
fn main() {
let mut tickets = BinaryHeap::new();
tickets.push(Ticket { urgency: 3, title: "login quebrado".to_string() });
}
error[E0277]: the trait bound `Ticket: Ord` is not satisfied
--> src/main.rs:10:13
|
10 | tickets.push(Ticket { urgency: 3, title: "login quebrado".to_string() });
| ^^^^ the trait `Ord` is not implemented for `Ticket`
|
help: consider annotating `Ticket` with `#[derive(Ord)]`

A mensagem diz que Ticket não implementa a trait Ord, e sugere adicionar #[derive(Ord)]. A correção é colocar #[derive(PartialEq, Eq, PartialOrd, Ord)] acima da struct (o Ord também precisa das outras três), com urgency como primeiro campo. Ou escreva o Ord à mão, como no exemplo do Job acima.

Três amigos, Ana, Ben e Cai, se revezam em um jogo de tabuleiro. Coloque os três em um VecDeque e imprima quem joga nas rodadas de 1 a 5. Depois de cada rodada, o jogador vai para o fim da fila. Dica: tire o jogador com pop_front, imprima e depois coloque o mesmo jogador de volta com push_back.

Ver solução
use std::collections::VecDeque;
fn main() {
let mut players = VecDeque::from(vec!["Ana", "Ben", "Cai"]);
for turn in 1..=5 {
let player = players.pop_front().unwrap();
println!("Rodada {turn}: {player}");
players.push_back(player); // de volta para o fim da fila
}
}

Saída:

Rodada 1: Ana
Rodada 2: Ben
Rodada 3: Cai
Rodada 4: Ana
Rodada 5: Ben
▶ Experimente você mesmo

A frente e o fim são rápidos em um VecDeque, então girar a fila quase não custa nada, não importa quantos jogadores existam.

Os pacientes chegam com uma gravidade de 1 a 5. O mais grave é atendido primeiro. Se dois pacientes tiverem a mesma gravidade, quem chegou antes é atendido antes. Use um BinaryHeap de tuplas e imprima a ordem de atendimento. Pacientes: Mia (gravidade 2, chegou em 1º), Leo (5, 2º), Sam (2, 3º), Zoe (4, 4º). Dica: tuplas são comparadas campo por campo, igualzinho a uma struct com derive. Experimente (severity, Reverse(arrival), name).

Ver solução
use std::cmp::Reverse;
use std::collections::BinaryHeap;
fn main() {
// (gravidade, Reverse(chegada), nome)
let mut waiting = BinaryHeap::new();
waiting.push((2, Reverse(1), "Mia"));
waiting.push((5, Reverse(2), "Leo"));
waiting.push((2, Reverse(3), "Sam"));
waiting.push((4, Reverse(4), "Zoe"));
while let Some((severity, Reverse(arrival), name)) = waiting.pop() {
println!("{name} (gravidade {severity}, chegou em #{arrival})");
}
}

Saída:

Leo (gravidade 5, chegou em #2)
Zoe (gravidade 4, chegou em #4)
Mia (gravidade 2, chegou em #1)
Sam (gravidade 2, chegou em #3)
▶ Experimente você mesmo

A gravidade é comparada primeiro, então o Leo vai primeiro. Mia e Sam empatam na gravidade, então o Reverse(arrival) decide, e quem chegou antes (Mia) ganha. Como cada número de chegada é diferente, o nome nunca é usado para desempatar.

  • Um Vec é uma fila ruim: remove(0) empurra todos os outros itens. Um VecDeque é um ring buffer (buffer circular) com push/pop rápidos nas duas pontas.
  • Use VecDeque para filas “primeiro a entrar, primeiro a sair”, históricos dos “últimos N itens” e janelas deslizantes.
  • Um BinaryHeap é um max-heap: pop e peek sempre devolvem o maior item, sem ordenar tudo.
  • Embrulhe os valores em std::cmp::Reverse para ter um min-heap (o menor primeiro).
  • Structs em um heap precisam de Ord. Com derive, os campos são comparados de cima para baixo, então coloque a prioridade primeiro. Escreva o Ord à mão para desempates personalizados.
  • .iter() em um heap não vem ordenado. Tire os itens um por um ou use into_sorted_vec.

Teste o que aprendeu

1. Por que VecDeque::pop_front é mais rápido que Vec::remove(0) em uma lista longa?

2. Você coloca 4, 9 e 2 em um BinaryHeap. O que o primeiro pop() retorna?

3. Como fazer um BinaryHeap devolver o menor valor primeiro?

4. Uma struct deriva Ord com os campos name e depois priority. O que decide a ordem em um BinaryHeap?

Pontuação: 0 / 4