Why this matters
Vec and the maps cover most needs, but two other shapes come up constantly: "is this value in my collection?" (a set) and "first in, first out" (a queue). Using the right one turns a slow, awkward program into a fast, obvious one. This lesson covers HashSet, BTreeSet and VecDeque, mentions BinaryHeap, and ends with a guide to choosing among all the standard collections.
Sets: membership without values
A set stores distinct values and answers "is it there?" quickly. A HashSet<T> is literally a HashMap<T, ()>: keys with no values.
use std::collections::HashSet;
fn main() {
let mut seen = HashSet::new();
for word in "to be or not to be".split(' ') {
let new = seen.insert(word);
println!("{word:4} new: {new}");
}
println!("{} distinct, contains \"or\": {}", seen.len(), seen.contains("or"));
}
to new: true
be new: true
or new: true
not new: true
to new: false
be new: false
4 distinct, contains "or": true
insert returns true if the value was not already present, which makes "first time seeing this?" a single call. Removing duplicates, tracking visited states in a search, and checking membership against a list of allowed values are all set jobs. A Vec with contains gives the same answers but scans the whole list on each call: O(n) versus O(1). For thousands of checks against thousands of values, that is the difference between instant and minutes.
BTreeSet and set operations
BTreeSet<T> keeps values sorted, iterates in order, and supports ranges. Both set types support the operations from mathematics:
use std::collections::BTreeSet;
fn main() {
let a: BTreeSet<i32> = [1, 2, 3, 4].into_iter().collect();
let b: BTreeSet<i32> = [3, 4, 5].into_iter().collect();
println!("union {:?}", a.union(&b).collect::<Vec<_>>());
println!("intersection {:?}", a.intersection(&b).collect::<Vec<_>>());
println!("difference {:?}", a.difference(&b).collect::<Vec<_>>());
println!("symmetric {:?}", a.symmetric_difference(&b).collect::<Vec<_>>());
println!("subset: {}", [3, 4].into_iter().collect::<BTreeSet<_>>().is_subset(&a));
println!("smallest {:?}, range {:?}", a.first(), a.range(2..4).collect::<Vec<_>>());
}
union [1, 2, 3, 4, 5]
intersection [3, 4]
difference [1, 2]
symmetric [1, 2, 5]
subset: true
smallest Some(1), range [2, 3]
The operations return iterators of references; collect gathers them. With BTreeSet, results come out sorted; with HashSet, in arbitrary order (sort before printing). Values in a HashSet need Hash + Eq, in a BTreeSet they need Ord.
VecDeque: a double-ended queue
VecDeque<T> ("vec-deck") is a growable ring buffer: a Vec whose contents may wrap around the end of the allocation, so both ends can grow and shrink in O(1):
capacity 8, holding [c, d, e, f] with head at slot 6:
slots: 0 1 2 3 4 5 6 7
[ e | f | | | | | c | d ]
^ tail ^ head
push_front(b) -> slot 5; push_back(g) -> slot 2; pop_front() -> c
use std::collections::VecDeque;
fn main() {
let mut q: VecDeque<&str> = VecDeque::new();
q.push_back("job1");
q.push_back("job2");
q.push_front("urgent");
println!("{:?} front={:?} back={:?}", q, q.front(), q.back());
while let Some(job) = q.pop_front() {
println!("processing {job}");
}
}
["urgent", "job1", "job2"] front=Some("urgent") back=Some("job2")
processing urgent
processing job1
processing job2
Use it for queues (breadth-first search, task scheduling, buffering the last N items). It supports indexing and iteration like a Vec, though its elements are not guaranteed to be one contiguous slice.
BinaryHeap<T> is the other specialized collection: a priority queue where pop always returns the largest element (wrap values in std::cmp::Reverse for smallest first). It suits "always process the most urgent item next" and algorithms like Dijkstra's shortest paths.
Choosing a collection
| need | collection | key costs |
|---|---|---|
| a list in order, index by position | Vec<T> | push/pop end O(1), index O(1), insert/remove front O(n), contains O(n) |
| queue or deque (both ends) | VecDeque<T> | push/pop both ends O(1), index O(1) |
| lookup by key | HashMap<K, V> | insert/get/remove O(1) average; no order |
| lookup by key, sorted, ranges | BTreeMap<K, V> | O(log n); sorted iteration |
| membership, uniqueness | HashSet<T> | O(1) average; no order |
| sorted unique values, ranges | BTreeSet<T> | O(log n); sorted |
| always take the largest | BinaryHeap<T> | push/pop O(log n), peek O(1) |
Some practical guidance:
- Start with
Vec. It is compact, cache-friendly and fast for small sizes, often beating "better" structures below a few dozen elements. Switch when profiling or the access pattern says so. - Need lookups by something other than position? A map or set.
- Need deterministic output? BTree versions, or sort at the end.
- Repeatedly removing from the front?
VecDeque. LinkedListexists, but it is rarely the right choice: Vec and VecDeque are almost always faster in practice.
The "O(...)" notation describes how cost grows with size n: O(1) is constant, O(log n) grows very slowly (20 steps for a million items), O(n) grows in proportion. An O(n) operation inside a loop over n items is O(n^2): fine for a hundred items, painful for a million.
Common mistakes
Vec::containsinside a loop over many items: quadratic. Build aHashSetonce.- Printing a
HashSetwhere the output must be stable: sort or useBTreeSet. Vec::remove(0)as a queue: useVecDeque::pop_front.- Sets of floats: E0277 (
f64is notHash/Eq/Ord).
Recap
HashSetandBTreeSetstore distinct values;insertreturns whether the value was new.- Set operations:
union,intersection,difference,symmetric_difference,is_subset. VecDequeis a ring buffer with O(1) operations at both ends;BinaryHeappops the largest first.- Choose by access pattern: position (Vec), both ends (VecDeque), key (maps), membership (sets), priority (heap).
- Start with Vec; prefer BTree types when order or determinism matters.
Going further: implement breadth-first search over a small grid using a VecDeque for the frontier and a HashSet<(i32, i32)> for visited cells. Read the std::collections module docs, which include a performance table.