Ir al contenido

VecDeque y BinaryHeap en Rust: colas y colas de prioridad

Piensa en la fila de una cafetería. La gente nueva se pone al final, y el barista atiende a quien está delante. Ahora piensa en la sala de espera de un hospital. Ahí pasa primero el paciente más urgente, da igual cuándo llegó. Las dos son “filas de espera”, pero siguen reglas distintas.

Rust tiene una colección para cada una. Un VecDeque es una fila rápida en la que puedes añadir y quitar por los dos extremos. Un BinaryHeap es una cola de prioridad: siempre te da el elemento más grande (o más urgente). Ya conoces los vectores, y la página de HashSet te mostró VecDeque por encima. Esta página profundiza en él y además te presenta BinaryHeap.

Puedes usar un Vec como cola. Añades con push y sacas del principio con remove(0). Funciona. El problema es lo que pasa por debajo.

Un Vec guarda sus elementos juntos, empezando en la posición 0. Cuando quitas el primero, todos los demás tienen que moverse un paso a la izquierda para tapar el hueco. Con 3 elementos, nadie lo nota. Con 100 000 elementos, cada remove(0) mueve 99 999 valores. Eso es lo que significa “O(n)”: el trabajo crece con el tamaño de la lista.

Un VecDeque (se pronuncia “vec dek”, de double-ended queue, cola de doble extremo) lo resuelve con un ring buffer (búfer circular). Imagina una mesa redonda con asientos numerados y un cartelito que dice “la fila empieza aquí”. Cuando se va la primera persona, nadie se levanta. Solo mueves el cartel al siguiente asiento. Cuando llega alguien nuevo y el último asiento está ocupado, se sienta en el asiento 0 si está libre, dando la vuelta a la mesa. Añadir o quitar por cualquiera de los dos extremos es solo “mover el cartel” u “ocupar un asiento”, así que tarda lo mismo, muy poquito, sea cual sea el largo de la fila. Eso es “O(1)”.

use std::collections::VecDeque;
fn main() {
// Un Vec usado como cola: funciona, pero remove(0) mueve a todos a la izquierda.
let mut vec_line = vec!["Ana", "Ben", "Cai"];
let served = vec_line.remove(0);
println!("Vec atendió a {served}, siguen esperando: {:?}", vec_line);
// Un VecDeque: pop_front solo mueve su marca de "inicio".
let mut deque_line = VecDeque::from(vec!["Ana", "Ben", "Cai"]);
let served = deque_line.pop_front();
println!("VecDeque atendió a {:?}, siguen esperando: {:?}", served, deque_line);
}

Salida:

Vec atendió a Ana, siguen esperando: ["Ben", "Cai"]
VecDeque atendió a Some("Ana"), siguen esperando: ["Ben", "Cai"]
▶ Pruébalo tú mismo

El resultado se ve igual. La diferencia es la velocidad, y solo se nota cuando la fila se hace larga. Fíjate en otra cosa: pop_front devuelve un Option. Si la fila está vacía, obtienes None en lugar de que el programa falle.

VecDeque está en std::collections, así que necesitas una línea use. Después, los métodos se leen casi solos: push (meter) o pop (sacar), por delante (front) o por detrá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!("delante: {:?}, detrás: {:?}", deque.front(), deque.back());
println!("len: {}, vacío: {}", 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!("sacados {:?} y {:?}, quedan: {:?}", first, last, deque);
}

Salida:

[1, 2, 3]
delante: Some(1), detrás: Some(3)
len: 3, vacío: false
deque[1] = 2
get(9) = None
sacados Some(1) y Some(3), quedan: [2]
▶ Pruébalo tú mismo

front y back te dejan mirar sin quitar nada. Indexar con deque[1] funciona igual que en un Vec, y provoca un panic si el índice es demasiado grande, así que usa get cuando no estés seguro.

También puedes crear un VecDeque a partir de un Vec, recorrerlo con un bucle y volver a convertirlo en un Vec o en un slice cuando otra función lo necesite:

use std::collections::VecDeque;
fn main() {
let mut playlist = VecDeque::from(vec!["Intro", "Estrofa", "Estribillo"]);
playlist.push_front("Conteo");
playlist.push_back("Cierre");
for (i, song) in playlist.iter().enumerate() {
println!("{}. {}", i + 1, song);
}
// ¿Necesitas un slice normal? make_contiguous lo pone todo seguido en memoria.
let songs = playlist.make_contiguous();
songs.sort();
println!("ordenadas: {:?}", songs);
// O convierte todo de nuevo en un Vec.
let as_vec: Vec<&str> = playlist.into();
println!("como Vec: {:?}", as_vec);
}

Salida:

1. Conteo
2. Intro
3. Estrofa
4. Estribillo
5. Cierre
ordenadas: ["Cierre", "Conteo", "Estribillo", "Estrofa", "Intro"]
como Vec: ["Cierre", "Conteo", "Estribillo", "Estrofa", "Intro"]
▶ Pruébalo tú mismo

¿Por qué hace falta make_contiguous? Acuérdate de la mesa redonda: después de dar algunas vueltas, los elementos pueden quedar en dos trozos separados de memoria. make_contiguous los ordena en un solo trozo y te da un slice normal, para que puedas usar herramientas de slices como sort. No lo necesitarás a menudo, pero conviene saber que existe.

Aquí tienes tres tareas del día a día en las que un VecDeque encaja perfecto.

Una cola de tareas en la que el primero en entrar es el primero en salir. Los trabajos de impresión esperan en fila. Los nuevos van al final y la impresora toma el de delante. Aun así, un trabajo urgente puede colarse con push_front:

use std::collections::VecDeque;
fn main() {
let mut print_queue = VecDeque::new();
print_queue.push_back("informe.pdf");
print_queue.push_back("foto.jpg");
print_queue.push_back("factura.pdf");
// ¡Urgente! Este se pone el primero de la fila.
print_queue.push_front("tarjeta-de-embarque.pdf");
while let Some(job) = print_queue.pop_front() {
println!("Imprimiendo {job} (en espera: {})", print_queue.len());
}
println!("Listo. Cola vacía: {}", print_queue.is_empty());
}

Salida:

Imprimiendo tarjeta-de-embarque.pdf (en espera: 3)
Imprimiendo informe.pdf (en espera: 2)
Imprimiendo foto.jpg (en espera: 1)
Imprimiendo factura.pdf (en espera: 0)
Listo. Cola vacía: true
▶ Pruébalo tú mismo

while let Some(job) = ...pop_front() es el bucle clásico de una cola. Sigue hasta que pop_front devuelve None, que significa que la fila está vacía. (Si esta forma es nueva para ti, mira if let).

Un historial de “los últimos N elementos”. Un buscador que recuerda tus 3 búsquedas más recientes. Cuando está lleno, la más antigua se cae por delante:

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(); // olvida la búsqueda más antigua
}
recent.push_back(search.to_string());
}
fn main() {
let mut recent = VecDeque::with_capacity(MAX_RECENT);
for search in ["libro de rust", "vecdeque", "binary heap", "pizza cerca de mí"] {
remember(&mut recent, search);
println!("{:?}", recent);
}
// Muestra primero la más nueva, como haría un buscador.
let newest_first: Vec<&String> = recent.iter().rev().collect();
println!("más nuevas primero: {:?}", newest_first);
}

Salida:

["libro de rust"]
["libro de rust", "vecdeque"]
["libro de rust", "vecdeque", "binary heap"]
["vecdeque", "binary heap", "pizza cerca de mí"]
más nuevas primero: ["pizza cerca de mí", "binary heap", "vecdeque"]
▶ Pruébalo tú mismo

Una media con ventana deslizante. Un termómetro envía una lectura cada minuto y quieres la media de las 3 últimas. Cada lectura nueva entra por detrás y la más antigua sale por delante. Si llevas una sum (suma) acumulada, nunca tienes que volver a sumar toda la ventana:

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(); // sale la lectura más antigua
}
if window.len() == window_size {
let average = sum / window_size as f64;
println!("{:?} -> media {:.1}", window, average);
}
}
}

Salida:

[20.0, 22.0, 24.0] -> media 22.0
[22.0, 24.0, 23.0] -> media 23.0
[24.0, 23.0, 19.0] -> media 22.0
[23.0, 19.0, 21.0] -> media 21.0
▶ Pruébalo tú mismo

Aquí el unwrap() es seguro: solo sacamos cuando la ventana tiene más de 3 elementos, así que no puede estar vacía.

Ahora, la sala de espera del hospital. A veces no te importa quién llegó primero. Lo que quieres es “dame lo más importante”, una y otra vez, mientras siguen llegando cosas nuevas.

Podrías meter todo en un Vec y ordenarlo cada vez. Pero ordenar todo solo para encontrar el primero es mucho trabajo desperdiciado. Un BinaryHeap (montículo binario) mantiene sus elementos organizados solo a medias, lo justo para que el más grande esté siempre arriba. Añadir un elemento o sacar el de arriba es barato (O(log n), que crece muy despacio: unos 17 pasos para 100 000 elementos).

El BinaryHeap de Rust es un max-heap (montículo de máximos): pop siempre te da el valor más grande. Aquí tienes las pujas de una subasta:

use std::collections::BinaryHeap;
fn main() {
let mut bids = BinaryHeap::new();
bids.push(120);
bids.push(450);
bids.push(80);
bids.push(300);
println!("puja más alta: {:?}", bids.peek());
println!("número de pujas: {}", bids.len());
while let Some(bid) = bids.pop() {
println!("siguiente: {bid}");
}
let heap = BinaryHeap::from(vec![5, 1, 4, 2, 3]);
println!("ordenado: {:?}", heap.into_sorted_vec());
}

Salida:

puja más alta: Some(450)
número de pujas: 4
siguiente: 450
siguiente: 300
siguiente: 120
siguiente: 80
ordenado: [1, 2, 3, 4, 5]
▶ Pruébalo tú mismo

peek mira el de arriba sin quitarlo. pop lo quita. into_sorted_vec consume el heap y te da un Vec ordenado de menor a mayor. Igual que en VecDeque, peek y pop devuelven un Option, así que un heap vacío nunca hace fallar el programa.

¿Y si quieres el más pequeño primero? La parada de reparto más cercana, el vuelo más barato, la fecha límite más próxima. No necesitas otra colección. Envuelve cada valor en std::cmp::Reverse, que simplemente invierte la comparación: lo que era más grande ahora es más pequeño.

use std::cmp::Reverse;
use std::collections::BinaryHeap;
fn main() {
// Paradas de reparto, por distancia en km. Siempre vas a la más cercana.
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 más cercana: {closest} km");
}
while let Some(Reverse(km)) = stops.pop() {
println!("conduce {km} km");
}
}

Salida:

parada más cercana: 3 km
conduce 3 km
conduce 5 km
conduce 8 km
conduce 12 km
▶ Pruébalo tú mismo

Reverse(km) es un struct envoltorio muy pequeño. El patrón Some(Reverse(km)) quita las dos capas de una vez, así que recuperas el número tal cual.

Las tareas reales no son solo números. Tienen un nombre, una prioridad y quizá una hora de llegada. Para meter tu propio struct en un BinaryHeap, Rust necesita saber cómo comparar dos de ellos, lo que significa que el struct debe implementar Ord (consulta Traits comunes).

La forma más rápida es derive. Una cosa que debes saber: derive compara los campos de arriba abajo. Compara el primer campo y solo mira el segundo si los primeros son iguales. Así que pon primero el campo por el que quieres ordenar:

use std::collections::BinaryHeap;
// El orden de los campos importa: derive compara primero `priority` y luego `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: "pedir cita con el dentista".to_string() });
todo.push(Task { priority: 5, name: "arreglar la caída del servidor".to_string() });
todo.push(Task { priority: 1, name: "regar las plantas".to_string() });
todo.push(Task { priority: 2, name: "responder el correo".to_string() });
while let Some(task) = todo.pop() {
println!("[{}] {}", task.priority, task.name);
}
}

Salida:

[5] arreglar la caída del servidor
[2] responder el correo
[2] pedir cita con el dentista
[1] regar las plantas
▶ Pruébalo tú mismo

Si cambiaras el orden de los dos campos para que name fuera primero, el heap ordenaría las tareas alfabéticamente y casi ignoraría la prioridad. Un error fácil de cometer, y un bug silencioso.

Mira las dos tareas con prioridad 2. “pedir cita con el dentista” llegó antes, pero “responder el correo” salió primero. El empate lo decidió name, y “responder” gana a “pedir” porque va después en el alfabeto. Es predecible, pero seguramente no es lo que quieres. Normalmente, en un empate, debería ir primero la tarea que llegó antes. Para una regla personalizada así, escribe Ord tú mismo:

use std::cmp::Ordering;
use std::collections::BinaryHeap;
#[derive(Debug, PartialEq, Eq)]
struct Job {
priority: u8,
id: u32, // orden de llegada: un id más bajo llegó antes
name: String,
}
impl Ord for Job {
fn cmp(&self, other: &Self) -> Ordering {
self.priority
.cmp(&other.priority) // gana la prioridad más alta
.then_with(|| other.id.cmp(&self.id)) // en un empate, gana el id más antiguo
}
}
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: "pedir cita con el dentista".to_string() });
jobs.push(Job { priority: 5, id: 2, name: "arreglar la caída del servidor".to_string() });
jobs.push(Job { priority: 1, id: 3, name: "regar las plantas".to_string() });
jobs.push(Job { priority: 2, id: 4, name: "responder el correo".to_string() });
while let Some(job) = jobs.pop() {
println!("[{}] #{} {}", job.priority, job.id, job.name);
}
}

Salida:

[5] #2 arreglar la caída del servidor
[2] #1 pedir cita con el dentista
[2] #4 responder el correo
[1] #3 regar las plantas
▶ Pruébalo tú mismo

El truco está en then_with: si las prioridades son iguales, compara los ids al revés (other.id contra self.id). Así el id más pequeño cuenta como “más grande” y sale primero del max-heap. Tranquilo si te cuesta un poco entenderlo. Invertir la comparación es justo lo que Reverse hace por ti, solo que aquí está escrito a mano. El bloque de PartialOrd es siempre el mismo código repetitivo: solo llama a tu cmp.

Un algoritmo real: las 3 mejores puntuaciones

Sección titulada «Un algoritmo real: las 3 mejores puntuaciones»

Imagina que tienes una lista enorme de puntuaciones de un juego y solo quieres las 3 mejores. Ordenar toda la lista funciona, pero hay un truco muy útil que solo guarda 3 elementos a la vez.

Usa un min-heap de tamaño 3. Mete cada puntuación. Cuando el heap tenga 4 elementos, saca uno. Como es un min-heap, el que tiras es el más pequeño. Al final, solo quedan los 3 mejores.

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(); // un min-heap: la puntuación más baja está arriba
for score in scores {
top.push(Reverse(score));
if top.len() > 3 {
top.pop(); // cuatro es uno de más: quita la más baja
}
}
let best: Vec<u32> = top
.into_sorted_vec()
.into_iter()
.map(|Reverse(score)| score)
.collect();
println!("las 3 mejores: {:?}", best);
}

Salida:

las 3 mejores: [99, 95, 91]
▶ Pruébalo tú mismo

Un buen extra: into_sorted_vec ordena los valores Reverse de “menor” a “mayor”, lo que para las puntuaciones reales significa de mayor a menor. Así que la mejor puntuación sale primero, sin ordenar nada más. Este patrón funciona con una lista de un millón de puntuaciones y nunca guarda más de 4 en memoria.

Aquí tienes una chuleta. “Rápido” significa que el tiempo se mantiene más o menos igual por grande que sea la colección. “Lento” significa que crece con el tamaño.

Colección Ideal para Meter Sacar de delante / de arriba Buscar un elemento cualquiera
Vec<T> Una lista que lees por posición; una pila Rápido al final Lento (remove(0) lo mueve todo) Lento (mira cada elemento)
VecDeque<T> Colas: añadir y quitar por los dos extremos Rápido en los dos extremos Rápido en los dos extremos Lento (mira cada elemento)
BinaryHeap<T> “Dame el más grande / el más urgente” Bastante rápido (O(log n)) Bastante rápido: siempre el más grande Lento, y sin orden
BTreeSet<T> Valores únicos, siempre ordenados Bastante rápido (O(log n)) Bastante rápido: el más pequeño y el más grande Bastante rápido (O(log n))

Si dudas, empieza con un Vec. Pasa a un VecDeque cuando saques cosas por delante. Pasa a un BinaryHeap cuando te preguntes una y otra vez “¿qué es lo más importante ahora?”. Usa un BTreeSet cuando necesites valores únicos y ordenados y quieras buscar elementos, no solo sacar el de arriba.

Esperar que .iter() en un BinaryHeap salga ordenado. Un heap solo está ordenado a medias. El elemento más grande va primero, pero el resto está en un orden revuelto que le sirve al heap, no a ti:

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!("sacados:");
while let Some(n) = heap.pop() {
print!(" {n}");
}
println!();
}

Salida:

iter: [9, 8, 2, 5, 3, 1]
ordenado: [1, 2, 3, 5, 8, 9]
sacados: 9 8 5 3 2 1
▶ Pruébalo tú mismo

Lo mismo pasa si imprimes un heap con {:?}: verás el desorden interno. Si necesitas los elementos en orden, sácalos uno a uno con pop o llama a into_sorted_vec.

Meter en un BinaryHeap un struct sin Ord. El heap tiene que comparar los elementos para saber cuál va arriba. Si tu struct no se puede comparar, Rust te detiene:

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

El compilador dice que Ticket no implementa el trait Ord, y sugiere añadir #[derive(Ord)]. La solución es poner #[derive(PartialEq, Eq, PartialOrd, Ord)] encima del struct (Ord también necesita los otros tres), con urgency como primer campo. O escribir Ord a mano, como en el ejemplo de Job de más arriba.

Tres amigos, Ana, Ben y Cai, juegan por turnos a un juego de mesa. Mételos en un VecDeque e imprime quién juega en los turnos del 1 al 5. Después de cada turno, el jugador vuelve al final de la fila. Pista: saca al jugador con pop_front, imprímelo y vuelve a meterlo con push_back.

Ver solución
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!("Turno {turn}: {player}");
players.push_back(player); // de vuelta al final de la fila
}
}

Salida:

Turno 1: Ana
Turno 2: Ben
Turno 3: Cai
Turno 4: Ana
Turno 5: Ben
▶ Pruébalo tú mismo

En un VecDeque, tanto el principio como el final son rápidos, así que rotar la fila no cuesta casi nada, haya los jugadores que haya.

Los pacientes llegan con una gravedad del 1 al 5. El más grave pasa primero. Si dos pacientes tienen la misma gravedad, pasa primero el que llegó antes. Usa un BinaryHeap de tuplas e imprime el orden en que se atiende a los pacientes. Pacientes: Mia (gravedad 2, llegada 1), Leo (gravedad 5, llegada 2), Sam (gravedad 2, llegada 3) y Zoe (gravedad 4, llegada 4). Pista: las tuplas se comparan campo a campo, igual que un struct con derive. Prueba con (severity, Reverse(arrival), name).

Ver solución
use std::cmp::Reverse;
use std::collections::BinaryHeap;
fn main() {
// (gravedad, Reverse(llegada), nombre)
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} (gravedad {severity}, llegada #{arrival})");
}
}

Salida:

Leo (gravedad 5, llegada #2)
Zoe (gravedad 4, llegada #4)
Mia (gravedad 2, llegada #1)
Sam (gravedad 2, llegada #3)
▶ Pruébalo tú mismo

La gravedad se compara primero, así que Leo pasa el primero. Mia y Sam empatan en gravedad, así que decide Reverse(arrival), y gana quien llegó antes (Mia). Como cada número de llegada es distinto, nunca hace falta el nombre para desempatar.

  • Un Vec es una mala cola: remove(0) mueve todos los demás elementos. Un VecDeque es un ring buffer (búfer circular) con push/pop rápidos en los dos extremos.
  • Usa VecDeque para colas en las que el primero en entrar es el primero en salir, historiales de “los últimos N elementos” y ventanas deslizantes.
  • Un BinaryHeap es un max-heap: pop y peek siempre te dan el elemento más grande, sin ordenarlo todo.
  • Envuelve los valores en std::cmp::Reverse para tener un min-heap (el más pequeño primero).
  • Los structs de un heap necesitan Ord. Con derive, los campos se comparan de arriba abajo, así que pon la prioridad primero. Escribe Ord a mano para desempates personalizados.
  • .iter() en un heap no sale ordenado. Saca los elementos uno a uno con pop o usa into_sorted_vec.

Comprueba lo aprendido

1. ¿Por qué VecDeque::pop_front es más rápido que Vec::remove(0) en una lista larga?

2. Metes 4, 9 y 2 en un BinaryHeap. ¿Qué devuelve el primer pop()?

3. ¿Cómo haces que un BinaryHeap te dé primero el valor más pequeño?

4. Un struct deriva Ord con los campos name y después priority. ¿Qué decide el orden en un BinaryHeap?

Puntuación: 0 / 4