Skip to main content
Rust beginner Lesson 17 of 30

Collections in Rust

Work with Vec<T>, HashMap<K,V>, HashSet, BTreeMap, and iterator adapters in Rust.

Vec<T> — Dynamic Array

Vec<T> is Rust’s workhorse collection — a heap-allocated, contiguous, growable array. It is the right default choice whenever you need to store a variable number of values of the same type. Because elements are stored contiguously in memory, iteration and random access are cache-friendly and fast. Pre-allocating with Vec::with_capacity avoids repeated reallocations when the final size is known in advance.

fn main() {
    // Create
    let mut v: Vec<i32> = Vec::new();
    let v2 = vec![1, 2, 3, 4, 5];
    let _v3: Vec<i32> = Vec::with_capacity(100); // pre-allocate to avoid reallocations

    // Add elements
    v.push(10);
    v.push(20);
    v.push(30);

    // Access — indexing panics on out of bounds; .get() returns Option
    println!("{}", v[0]);              // 10 (panics if out of bounds)
    println!("{:?}", v.get(1));        // Some(20) — safe access
    println!("{:?}", v.get(99));       // None — no panic

    // Modify
    v[0] = 100;

    // Remove
    let last = v.pop();                // Some(30) — removes from the end
    v.remove(0);                       // removes index 0, shifts remaining elements

    // Length and capacity
    println!("len: {}", v2.len());     // 5
    println!("is_empty: {}", v.is_empty());

    // Iterate by reference (v2 is not consumed)
    for n in &v2 {
        print!("{} ", n);
    }
    println!();

    // Slice operations
    println!("{:?}", &v2[1..4]);       // [2, 3, 4]
    println!("{:?}", v2.first());      // Some(1)
    println!("{:?}", v2.last());       // Some(5)
}

Common Vec Operations

fn main() {
    let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6];

    // Sort then deduplicate consecutive duplicates
    v.sort();
    println!("{:?}", v); // [1, 1, 2, 3, 4, 5, 6, 9]
    v.dedup();           // dedup only removes consecutive duplicates — sort first
    println!("{:?}", v); // [1, 2, 3, 4, 5, 6, 9]

    // retain keeps only elements satisfying the predicate
    v.retain(|&x| x % 2 == 0);
    println!("{:?}", v); // [2, 4, 6]

    // Extend appends elements from any iterable
    let mut a = vec![1, 2, 3];
    a.extend([4, 5, 6]);
    println!("{:?}", a); // [1, 2, 3, 4, 5, 6]

    // chain + collect concatenates without mutating either source
    let b = vec![7, 8, 9];
    let c: Vec<i32> = a.iter().chain(b.iter()).copied().collect();
    println!("{:?}", c); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

    // flatten removes one level of nesting from a Vec<Vec<T>>
    let nested = vec![vec![1, 2], vec![3, 4], vec![5, 6]];
    let flat: Vec<i32> = nested.into_iter().flatten().collect();
    println!("{:?}", flat); // [1, 2, 3, 4, 5, 6]
}

HashMap<K, V> — Hash Map

HashMap maps keys to values with O(1) average-case lookup, insert, and remove. It is the right choice when you need to associate data with arbitrary keys and access it by key later. The standard library’s HashMap uses a high-quality hash function by default that is resistant to hash-flooding attacks — if raw speed matters more than security you can swap in a faster hasher.

use std::collections::HashMap;

fn main() {
    let mut scores: HashMap<String, i32> = HashMap::new();

    // Insert key-value pairs
    scores.insert(String::from("Alice"), 95);
    scores.insert(String::from("Bob"), 87);
    scores.insert(String::from("Charlie"), 92);

    // Access — .get() returns Option, indexing panics on missing key
    println!("{:?}", scores.get("Alice"));      // Some(95)
    println!("{:?}", scores.get("Dave"));       // None
    println!("{}", scores["Alice"]);            // 95 (panics if missing)

    // Membership test
    println!("{}", scores.contains_key("Bob")); // true

    // Remove
    scores.remove("Bob");

    // Iterate — order is not guaranteed
    for (name, score) in &scores {
        println!("{}: {}", name, score);
    }

    // Overwrite an existing entry
    scores.insert(String::from("Alice"), 100);
    println!("{}", scores["Alice"]);            // 100
}

The Entry API

The entry API solves a common pattern — insert a value if the key is absent, or update it if present — without a separate lookup. It avoids the double-hash and the clone that a contains_key + insert pair would require, making it both cleaner and faster for accumulation patterns like word counting.

use std::collections::HashMap;

fn word_count(text: &str) -> HashMap<&str, usize> {
    let mut map = HashMap::new();

    for word in text.split_whitespace() {
        // or_insert returns a mutable reference to the value, inserting 0 if absent
        *map.entry(word).or_insert(0) += 1;
    }

    map
}

fn main() {
    let counts = word_count("the quick brown fox jumps over the lazy fox");
    let mut pairs: Vec<_> = counts.iter().collect();
    pairs.sort_by_key(|&(_, v)| std::cmp::Reverse(*v));

    for (word, count) in pairs.iter().take(3) {
        println!("{}: {}", word, count);
    }
}
use std::collections::HashMap;

fn main() {
    let mut map: HashMap<&str, Vec<i32>> = HashMap::new();

    // or_insert_with creates the Vec lazily only when the key is absent
    map.entry("evens").or_insert_with(Vec::new).push(2);
    map.entry("evens").or_insert_with(Vec::new).push(4);
    map.entry("odds").or_insert_with(Vec::new).push(1);

    println!("{:?}", map); // {"evens": [2, 4], "odds": [1]}
}

HashSet<T> — Unique Elements

HashSet is a collection of unique values backed by a hash table. It is ideal when you care about membership rather than order — checking whether an element exists, deduplicating a list, or computing set operations like union and intersection. The underlying hash mechanism is the same as HashMap, so lookup is O(1) average.

use std::collections::HashSet;

fn main() {
    let mut set: HashSet<i32> = HashSet::new();

    set.insert(1);
    set.insert(2);
    set.insert(3);
    set.insert(2); // duplicate — silently ignored

    println!("{}", set.contains(&2)); // true
    println!("{}", set.len());        // 3

    let a: HashSet<i32> = [1, 2, 3, 4].iter().copied().collect();
    let b: HashSet<i32> = [3, 4, 5, 6].iter().copied().collect();

    // Set algebra — all return iterators over references
    let union: HashSet<_>        = a.union(&b).collect();
    let intersection: HashSet<_> = a.intersection(&b).collect();
    let difference: HashSet<_>   = a.difference(&b).collect();

    println!("union: {:?}", union);
    println!("intersection: {:?}", intersection); // {3, 4}
    println!("a - b: {:?}", difference);          // {1, 2}

    println!("a subset of union: {}", a.is_subset(&union.into_iter().copied().collect()));
}

BTreeMap<K, V> — Sorted Map

BTreeMap is like HashMap but keys are always stored in sorted order, which means iteration visits keys in ascending order and you can efficiently query ranges. The trade-off is O(log n) operations instead of O(1). Use it when you need sorted output, need to find the minimum or maximum key, or need to query a range of keys.

use std::collections::BTreeMap;

fn main() {
    let mut scores: BTreeMap<&str, i32> = BTreeMap::new();
    scores.insert("Charlie", 92);
    scores.insert("Alice", 95);
    scores.insert("Bob", 87);

    // Iteration always visits keys in sorted (alphabetical) order
    for (name, score) in &scores {
        println!("{}: {}", name, score);
    }
    // Alice: 95
    // Bob: 87
    // Charlie: 92

    // Range queries — only possible with BTreeMap, not HashMap
    for (name, score) in scores.range("Alice"..="Bob") {
        println!("{}: {}", name, score);
    }
}

VecDeque<T> — Double-Ended Queue

VecDeque is a ring buffer that supports efficient push and pop from both ends. It is the right choice when you need a queue (FIFO) or deque — pushing to the back and popping from the front is O(1), unlike Vec where removing from the front is O(n) because it shifts all elements.

use std::collections::VecDeque;

fn main() {
    let mut deque: VecDeque<i32> = VecDeque::new();

    // O(1) push to either end
    deque.push_back(1);
    deque.push_back(2);
    deque.push_front(0);

    println!("{:?}", deque); // [0, 1, 2]

    // O(1) pop from either end
    println!("{:?}", deque.pop_front()); // Some(0)
    println!("{:?}", deque.pop_back());  // Some(2)
}

Collecting into Collections

The .collect() method is how you materialize an iterator into a concrete collection. Because collect is generic, Rust can build any collection type as long as you tell it which one — either through a type annotation or the turbofish syntax. This makes iterator pipelines compose naturally with any collection you need.

use std::collections::{HashMap, HashSet};

fn main() {
    // Vec — most common collect target
    let squares: Vec<i32> = (1..=5).map(|x| x * x).collect();
    println!("{:?}", squares); // [1, 4, 9, 16, 25]

    // HashSet — automatically deduplicates
    let unique: HashSet<i32> = vec![1, 2, 2, 3, 3, 3].into_iter().collect();
    println!("{}", unique.len()); // 3

    // HashMap — collect from an iterator of (key, value) tuples
    let map: HashMap<&str, i32> = vec![("one", 1), ("two", 2), ("three", 3)]
        .into_iter()
        .collect();
    println!("{}", map["two"]); // 2

    // partition — split one iterator into two Vecs based on a predicate
    let (evens, odds): (Vec<_>, Vec<_>) = (0..10).partition(|&x| x % 2 == 0);
    println!("{:?}", evens); // [0, 2, 4, 6, 8]
    println!("{:?}", odds);  // [1, 3, 5, 7, 9]
}

Frequently Asked Questions

When should I use BTreeMap instead of HashMap?
Use BTreeMap when you need keys to be in sorted order, or when you need range queries. HashMap has O(1) average operations; BTreeMap has O(log n) but maintains sorted order.
How do I avoid cloning when inserting into a HashMap?
Use the entry API: map.entry(key).or_insert(value) inserts only when the key is absent, avoiding a clone when the key already exists.
What is the difference between Vec::new() and vec![]?
Vec::new() creates an empty vector. vec![] is a macro that creates a vector with initial elements: vec![1, 2, 3].