Skip to content
Select theme

How to use a Vec as a queue

Short answer: reach for VecDeque, not Vec. push_back adds to the back, pop_front takes from the front, and neither moves the other elements. Vec::remove(0) does the same job and shifts the whole tail left every time, so draining a queue that way costs O(n) per item.

From a fresh project (cargo new queue-demo — the name is mine, and nothing below prints it), replace the contents of src/main.rs with each block and run it with cargo run. Everything here is Rust 1.99.0, edition 2024.

len: 3
front: Some("first")
back: Some("third")
pop_front: Some("first")
pop_front: Some("second")
left: ["third"]
pop_front: Some("third")
pop_front on empty: None

front and back look without taking, and all three of these return an Option — Some while the queue holds something, None when it runs dry — so a drain loop can run until the queue is empty without tracking a length. It is a double-ended queue, so push_front and pop_back work at the other end too. A Vec already has push and pop at the back; a queue needs the front.

error[E0308]: mismatched types
--> src/main.rs:3:12
|
3 | if let Some(x) = v.remove(0) {
| ^^^^^^^ ----------- this expression has type `i32`
| |
| expected `i32`, found `Option<_>`
|
= note: expected type `i32`
found enum `Option<_>`
For more information about this error, try `rustc --explain E0308`.

The mistake is natural: pop_front hands back an Option, so remove looks like it should too. Vec::remove returns the element itself, and on an empty Vec it panics with removal index (is 0) should be < len (is 0). The cost is the real problem. The standard library’s own note on remove gives it “a worst-case performance of O(n)” — it shifts every element after the index to the left — and points at VecDeque::pop_front for removing from the front of a Vec.

Run that with cargo run --release: median of five rounds, draining a queue built from 0..n one item at a time. At 100,000 items remove(0) takes about 740 ms and pop_front about 130 µs. Ten times the items makes remove(0) roughly a hundred times slower — each pop shifts what is left, so the total grows with the square of the count — while pop_front costs the same per item whatever the size. A plain cargo run prints different numbers — larger for pop_front, about the same for remove(0).

When the order genuinely does not matter, swap_remove(0) is O(1): it moves the last element into the hole, so ["a", "b", "c", "d"] loses "a" and becomes ["d", "b", "c"]. That is no longer a queue, but it is the right tool for “delete this one item”.

len: 8, capacity: 8
as_slices: [4, 5, 6, 7] and [8, 9, 10, 11]
iter: [4, 5, 6, 7, 8, 9, 10, 11]
index 0: 4
get 100: None

The two slices are the ring showing through: after the pops and pushes the buffer has wrapped, so part of the queue sits at the end of the allocation and the rest at the start. iter() and Debug still walk front to back, and the capacity never moved: the queue kept its buffer instead of reallocating one. When one contiguous slice is needed, make_contiguous() rotates the deque and returns &[T]; the standard library’s words are that the elements are “not necessarily contiguous in memory”, and it names sorting as the reason to reach for it. VecDeque also indexes by position only: queue[0] works, while &queue[1..3] does not compile, because the index takes a usize and not a range.

Next in this chapter: how to get the largest element from a collection.