↓ Skip to main content
  1. Rust/

Using and Choosing std::collections in Rust: BTreeMap, HashSet, BinaryHeap, and VecDeque

1006 words·5 mins· loading · loading · · ·Rust-middle
About Rust - This article is part of a series.

Introduction
#

Most of the time in Rust, Vec<T> and HashMap<K, V> are all you need. But when you hit message queues, specialized queries, or task schedulers, the default containers start falling short.

The standard std::collections module covers most application needs without third-party crates. Still, it is worth looking beyond just O(1) or O(log N) complexity ratings and paying attention to how data actually sits in memory. Modern CPUs love contiguous memory: good data placement and L1/L2 cache hits often outweigh theoretical algorithmic bounds in practice.

Let’s walk through four key structures from the standard library: VecDeque, BTreeMap, BTreeSet, and BinaryHeap.


1. Collections: VecDeque vs Vec vs LinkedList
#

Dynamic Array (Vec<T>)
#

Vec<T> stores elements as a contiguous array on the heap. Because of this, iterating over a vector runs very fast, and indexing is rated at O(1).

The vector’s weak spot is inserting or removing elements at the front or middle: you have to shift the entire tail for O(N).

Double-Ended Queue (VecDeque<T>)
#

If you need a queue (FIFO) or a ring buffer of recent events, reach for VecDeque<T>.

Under the hood, it is a dynamic ring buffer:

  • Push and pop at the back (push_back / pop_back) — O(1).
  • Push and pop at the front (push_front / pop_front) — O(1) without shifting the entire array.
Tip

In almost every scenario where you might think about a linked list, VecDeque wins in both speed and memory efficiency.

Linked List (LinkedList<T>)
#

Doubly linked LinkedList<T> is almost never seen in real-world Rust code. Each node lives in its own heap allocation. Traversing the list forces the CPU to jump across memory and constantly hit cache misses.


2. Maps: BTreeMap vs HashMap
#

For working with key-value pairs, the standard library offers two tools.

Hash Tables (HashMap<K, V> and HashSet<T>)
#

  • Under the hood: SwissTable algorithm.
  • Complexity: lookup, insertion, and deletion run in average O(1).
  • Characteristics: element order is arbitrary. Keys must implement Hash + Eq.

B-Trees (BTreeMap<K, V> and BTreeSet<T>)
#

  • Under the hood: a B-Tree where each node holds a small array of elements. This packs keys tightly into CPU cache lines.
  • Complexity: lookup, insertion, and deletion guarantee O(log N) time.
  • Characteristics: keys are always sorted. Keys must implement Ord.
use std::collections::BTreeMap;

let mut map = BTreeMap::new();
map.insert(3, "Third");
map.insert(1, "First");

// Iterates in ascending key order: 1, then 3
for (key, val) in &map {
    println!("{key}: {val}");
}
Note

Why choose BTreeMap if HashMap offers O(1)?

  1. You need keys sorted by default.
  2. You need range queries via .range(min..max).
  3. You need predictable runtime guarantees without worst-case O(N) hash collisions.

3. Priority Queue: BinaryHeap
#

BinaryHeap<T> is a binary Max-Heap implemented on top of a vector. The collection is built for task schedulers and event queues where you need to process the highest-priority item first.

  • Adding an item via .push(item) takes O(log N).
  • Extracting the maximum item via .pop() takes O(log N).
  • Peeking at the top item via .peek() takes O(1).
Note

To turn BinaryHeap into a Min-Heap (popping the smallest item first), wrap values in std::cmp::Reverse(x).


4. Complexity Cheatsheet
#

CollectionAccessFront InsertBack InsertElement Ordering
Vec<T>O(1) by indexO(N)O(1) amortizedUnsorted
VecDeque<T>O(1) by indexO(1) amortizedO(1) amortizedUnsorted
HashMap<K, V>O(1) avg——Random
BTreeMap<K, V>O(log N)——Sorted
BinaryHeap<T>O(1) for Max—O(log N)Priority

Below are interactive slides for each collection:

Working with VecDeque, BTreeMap, and BinaryHeap
Step 1/3
 1use std::collections::VecDeque;
 2
 3fn main() {
 4    // Double-ended queue (Ring Buffer)
 5    let mut queue = VecDeque::with_capacity(5);
 6
 7    // Appending to the back:
 8    queue.push_back("Task 1");
 9    queue.push_back("Task 2");
10
11    // Urgent push to the front:
12    queue.push_front("Priority task!");
13
14    println!("Queue elements: {:?}", queue);
15
16    // Pop from opposite ends in O(1):
17    if let Some(first) = queue.pop_front() {
18        println!("Popped from front: {first}");
19    }
20
21    if let Some(last) = queue.pop_back() {
22        println!("Popped from back: {last}");
23    }
24}
25

Double-Ended Queue: VecDeque

  • VecDeque<T> is implemented as a contiguous ring buffer.
  • Allows O(1) insertions and deletions at both ends (push_front/pop_front, push_back/pop_back).
  • Ideal for FIFO task queues, message buffers, and sliding windows.

Quick Quiz
#

A short test to check your understanding of Rust collections:

Article read
Please rate how helpful and clear this article was to you
▶ Article series
About Rust - This article is part of a series.

Related