Unlock Your Python Backend Career: Build 30 Projects in 30 Days. Join now for just $54

Collections in Rust: The Essentials

by Ugochukwu Chizaram Omumusinachi

.

Updated Tue Sep 09 2025

.
Collections in Rust: The Essentials

When you're building real applications in Rust, you'll quickly discover that primitive types only get you so far. You need data structures that can grow, shrink, and organize multiple values efficiently. 

That's where Rust's collections come in.

Collections are the workhorses of modern programming. They're heap-allocated, carefully articulated collections of data structures that can hold multiple values and resize dynamically during runtime. Rust's standard library provides several powerful collections, each optimized for different use cases. Let's dive into the ones you'll probably use in production code.

You see, rust collections are broken into four separate groups 

  • Sequences: Vec, VecDeque, LinkedList

  • Maps: HashMap, BTreeMap

  • Sets: HashSet, BTreeSet

  • Misc: BinaryHeap

In this article, we will go over the most commonly used ones and basic knowledge on their usage. You would learn the basics of collections. 

The Big Three: Vec, HashMap, and HashSet

Vector: Your Dynamic Array

The Vec<T> It's probably the first collection you'll reach for. Think of it as a resizable array that lives on the heap. It's ordered, allows duplicates, and provides fast indexed access.


// Creating vectors
let mut numbers = Vec::new();
let mut scores = vec![85, 92, 78, 96]; // macro for convenience

// Adding elements
numbers.push(42);
numbers.push(17);
numbers.extend([1, 2, 3]); // add multiple at once

// Accessing elements
let first = scores[0]; // panics if out of bounds
let maybe_second = scores.get(1); // returns Option<&T>

// Iterating
for score in &scores {
    println!("Score: {}", score);
}

// Capacity management
println!("Length: {}, Capacity: {}", scores.len(), scores.capacity());
scores.reserve(100); // pre-allocate space

The beauty of Vec lies in its versatility. Need a stack? Use push() and pop(). Need a queue? Use push() and remove(0) (though VecDeque is better for this). Need to sort data? Call sort() or sort_by(). You could see it as the Swiss knife of collections

HashMap: Key-Value Powerhouse

When you need to associate keys with values, HashMap<K, V> is your friend. It provides average O(1) insertion, deletion, and lookup times, making it perfect for caches, indices, and lookup tables.


use std::collections::HashMap;

// Creating and populating
let mut student_grades = HashMap::new();
student_grades.insert("Alice", 95);
student_grades.insert("Bob", 87);
student_grades.insert("Charlie", 92);

// Alternative creation
let scores = HashMap::from([
    ("Math", 85),
    ("Science", 90),
    ("History", 78),
]);

// Accessing values
match student_grades.get("Alice") {
    Some(grade) => println!("Alice scored: {}", grade),
    None => println!("Student not found"),
}

// The entry API is incredibly useful
let grade = student_grades.entry("David").or_insert(0);
*grade += 10; // David now has 10

// Updating existing values
student_grades.entry("Alice").and_modify(|g| *g += 5);

The entry API deserves special attention. It's one of Rust's hashmaps most used features, allowing you to handle the "insert if missing, update if present" pattern without multiple lookups, basically you can manipulate an entry by calling a mutable instance of the entry — allowing you to do alot.

HashSet: Unique Values Only


HashSet<T> is essentially a HashMap where you only care about the keys. It guarantees uniqueness and provides fast membership testing.

use std::collections::HashSet;

let mut unique_ids = HashSet::new();
unique_ids.insert(42);
unique_ids.insert(17);
unique_ids.insert(42); // duplicate - won't be added

// Set operations
let set1: HashSet<i32> = [1, 2, 3, 4].into();
let set2: HashSet<i32> = [3, 4, 5, 6].into();

let intersection: HashSet<_> = set1.intersection(&set2).cloned().collect();
let union: HashSet<_> = set1.union(&set2).cloned().collect();
let difference: HashSet<_> = set1.difference(&set2).cloned().collect();

// Membership testing
if unique_ids.contains(&42) {
    println!("Found it!");
}

The Supporting Cast

VecDeque: Double-Ended Queue

When you need efficient insertion and removal at both ends, VecDeque<T> (pronounced "vec-deck") is your answer. It's implemented as a ring buffer.


use std::collections::VecDeque;

let mut queue = VecDeque::new();

// Add to both ends
queue.push_back(1);    // [1]
queue.push_front(0);   // [0, 1]
queue.push_back(2);    // [0, 1, 2]

// Remove from both ends
let front = queue.pop_front(); // Some(0), queue is now [1, 2]
let back = queue.pop_back();   // Some(2), queue is now [1]

BTreeMap and BTreeSet: Ordered Alternatives

Sometimes you need your data sorted. BTreeMap<K, V> and BTreeSet<T> maintain their elements in sorted order, providing O(log n) operations.


use std::collections::BTreeMap;

let mut leaderboard = BTreeMap::new();
leaderboard.insert("Alice", 1500);
leaderboard.insert("Bob", 1200);
leaderboard.insert("Charlie", 1800);

// Iteration is automatically sorted by key
for (name, score) in &leaderboard {
    println!("{}: {}", name, score); // Alphabetical order
}

// Range queries
let high_scorers: BTreeMap<_, _> = leaderboard
    .range("Bob".."Charlie")
    .map(|(k, v)| (*k, *v))
    .collect();

BinaryHeap: Priority Queue

Need a priority queue? BinaryHeap<T> implements a max-heap, always keeping the largest element at the front.


use std::collections::BinaryHeap;

let mut heap = BinaryHeap::new();
heap.push(5);
heap.push(1);
heap.push(9);
heap.push(3);

while let Some(max) = heap.pop() {
    println!("{}", max); // Prints: 9, 5, 3, 1
}

Performance Characteristics: Choose Wisely

Knowing what to choose is very dicey since it largely depends on your use case , however here are some general guides to help you pick.

Vec: Use when you need indexed access, don't mind occasional expensive resizing, and primarily append to the end. Perfect for most use cases.

VecDeque: Choose when you need efficient operations at both ends. Great for queues and sliding windows.

HashMap: Your go-to for key-value associations when you don't need ordering. Excellent for caches and lookup tables.

BTreeMap: Use when you need sorted key-value pairs or range queries. Slightly slower than HashMap but maintains order.

HashSet: Perfect for membership testing and eliminating duplicates.

BTreeSet: When you need a sorted set or want to perform set operations on ordered data.

BinaryHeap: Essential for priority queues and algorithms that need "give me the next largest/smallest" semantics.

Memory Management and Ownership

Collections in Rust follow the same ownership rules as everything else, but they introduce some interesting patterns:


let mut data = vec![String::from("hello"), String::from("world")];

// This moves the string out of the vector
let first = data.remove(0); // "hello" is now owned by `first`

// This borrows without moving
let second = &data[0]; // borrowing "world"

// Collections can own or borrow
let borrowed_data: Vec<&str> = vec!["hello", "world"];
let owned_data: Vec<String> = vec!["hello".to_string(), "world".to_string()];

When designing your data structures, consider whether your collection should own its data or just hold references. Owned data is simpler but uses more memory. Borrowed data is efficient but comes with lifetime constraints.

Common Patterns and Idioms

The Collect Pattern

One of Rust's most powerful patterns is using iterators with collect():


// Transform and collect
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();

// Filter and collect
let evens: Vec<i32> = numbers.into_iter().filter(|&x| x % 2 == 0).collect();

// Collect into different types
let unique_numbers: HashSet<i32> = vec![1, 2, 2, 3, 3, 4].into_iter().collect();
let letter_counts: HashMap<char, usize> = "hello"
    .chars()
    .fold(HashMap::new(), |mut acc, c| {
        *acc.entry(c).or_insert(0) += 1;
        acc
    });

Building Collections from Other Collections


// Converting between types
let vec_data = vec![1, 2, 3];
let set_data: HashSet<_> = vec_data.into_iter().collect();
let vec_again: Vec<_> = set_data.into_iter().collect();

// Partial moves
let original = vec![1, 2, 3, 4, 5];
let (first_half, second_half) = original.split_at(3);
let first_vec = first_half.to_vec(); // cloning is explicit

Custom Hashing and Ordering

Sometimes the default behavior isn't what you want:


use std::collections::HashMap;
use std::hash::{Hash, Hasher};

#[derive(Debug)]
struct CaseInsensitiveString(String);

impl Hash for CaseInsensitiveString {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.0.to_lowercase().hash(state);
    }
}

impl PartialEq for CaseInsensitiveString {
    fn eq(&self, other: &Self) -> bool {
        self.0.to_lowercase() == other.0.to_lowercase()
    }
}

impl Eq for CaseInsensitiveString {}

// Now you can use it as a HashMap key with case-insensitive matching
let mut case_insensitive_map = HashMap::new();
case_insensitive_map.insert(CaseInsensitiveString("Hello".to_string()), 42);

Performance Tips and Gotchas

Pre-allocate when possible: If you know roughly how much data you'll store, use Vec::with_capacity() or HashMap::with_capacity() to avoid reallocations, this is just how vectors in programming work.


let mut big_vec = Vec::with_capacity(1000); // avoid reallocation
let mut cache = HashMap::with_capacity(50);

Choose the right iteration method:


for item in collection - moves/consumes the collection
for item in &collection - borrows each item
for item in &mut collection - mutably borrows each item

HashMap keys must implement Hash + Eq: Most built-in types work, but be careful to ensure it is done how you expect especially for custom types. 

Vec indexing can panic: Use get() for safe access, especially with user input.

When to Use What: Decision Tree

Need ordered data with indexed access?Vec<T>

Need efficient insertion/removal at both ends?VecDeque<T>

Need key-value lookups without caring about order?HashMap<K, V>

Need key-value lookups with sorted keys?BTreeMap<K, V>

Need to track unique values without caring about order?HashSet<T>

Need unique values in sorted order?BTreeSet<T>

Need a priority queue?BinaryHeap<T>

The Bottom Line

Rust's collections are designed with performance and memory safety in mind. They give you the tools to build efficient, correct programs without the fear of memory leaks or buffer overflows that plague other systems languages.

The core point here is that rust collections are just normal structures like you could write , they simply come with the standard library to ease you writing them.

Remember: premature optimization is still the root of all evil, even in Rust. Start with the simplest collection that meets your needs (Vec for most sequences, HashMap for most key-value pairs), and only switch to more specialized collections when profiling shows they're necessary. The Rust standard library's collections are all highly optimized, so your choice of collection often matters more than micro-optimizations within a collection type.

If you have any questions, feel free to reach out to me on my LinkedIn. For more Rust content, see https://rustdaily.com/

Goodbye, see you next week !!

Course image
Become a Rust Backend Engineeer today

All-in-one Rust course for learning backend engineering with Rust. This comprehensive course is designed for Rust developers seeking proficiency in Rust.

Start Learning Now

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board

Backend Tips, Every week

Backend Tips, Every week