1. rust
  2. /advanced
  3. /collections

Collections

Rust's standard library provides several collection types for storing multiple values. Each collection has different performance characteristics and use cases. Understanding these collections is essential for writing efficient Rust programs.

Vec<T> - Dynamic Arrays

Vec<T> is a growable array that stores elements of the same type in contiguous memory.

Creating and Initializing Vectors

fn main() {
    // Creating empty vectors
    let mut vec1: Vec<i32> = Vec::new();
    let mut vec2 = Vec::<i32>::new();
    let mut vec3 = vec![]; // Type will be inferred
    
    // Creating with initial values
    let vec4 = vec![1, 2, 3, 4, 5];
    let vec5 = vec![0; 5]; // [0, 0, 0, 0, 0]
    
    // Creating with capacity
    let mut vec6 = Vec::with_capacity(10);
    
    println!("vec4: {:?}", vec4);
    println!("vec5: {:?}", vec5);
    println!("vec6 capacity: {}", vec6.capacity());
}

Adding and Removing Elements

fn main() {
    let mut numbers = Vec::new();
    
    // Adding elements
    numbers.push(1);
    numbers.push(2);
    numbers.push(3);
    
    // Extending with another vector
    numbers.extend(vec![4, 5, 6]);
    
    // Inserting at specific position
    numbers.insert(0, 0); // Insert 0 at beginning
    
    println!("After additions: {:?}", numbers);
    
    // Removing elements
    let last = numbers.pop(); // Removes and returns last element
    println!("Popped: {:?}", last);
    
    let removed = numbers.remove(0); // Remove element at index 0
    println!("Removed: {}", removed);
    
    // Remove all elements matching a condition
    numbers.retain(|&x| x % 2 == 0);
    println!("After retaining even numbers: {:?}", numbers);
    
    // Clear all elements
    numbers.clear();
    println!("After clear: {:?}", numbers);
}

Accessing Elements

fn main() {
    let numbers = vec![10, 20, 30, 40, 50];
    
    // Indexing (can panic)
    println!("First element: {}", numbers[0]);
    
    // Safe access with get()
    match numbers.get(10) {
        Some(value) => println!("Element at index 10: {}", value),
        None => println!("No element at index 10"),
    }
    
    // First and last elements
    if let Some(first) = numbers.first() {
        println!("First: {}", first);
    }
    
    if let Some(last) = numbers.last() {
        println!("Last: {}", last);
    }
    
    // Slicing
    let slice = &numbers[1..4]; // Elements at indices 1, 2, 3
    println!("Slice: {:?}", slice);
}

Iterating Over Vectors

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // Iterate by reference
    for num in &numbers {
        println!("Number: {}", num);
    }
    
    // Iterate by value (consumes the vector)
    for num in numbers.clone() {
        println!("Owned number: {}", num);
    }
    
    // Iterate with index
    for (index, value) in numbers.iter().enumerate() {
        println!("Index {}: {}", index, value);
    }
    
    // Iterate mutably
    let mut mutable_numbers = vec![1, 2, 3, 4, 5];
    for num in &mut mutable_numbers {
        *num *= 2;
    }
    println!("Doubled: {:?}", mutable_numbers);
}

HashMap<K, V> - Key-Value Storage

HashMap<K, V> stores key-value pairs and provides O(1) average-case lookup.

Creating and Using HashMaps

use std::collections::HashMap;

fn main() {
    // Creating empty HashMap
    let mut scores: HashMap<String, i32> = HashMap::new();
    
    // Inserting values
    scores.insert("Blue".to_string(), 10);
    scores.insert("Red".to_string(), 50);
    
    // Creating from vectors
    let teams = vec!["Blue".to_string(), "Red".to_string()];
    let initial_scores = vec![10, 50];
    let scores_from_vecs: HashMap<_, _> = teams.into_iter()
        .zip(initial_scores.into_iter())
        .collect();
    
    println!("Scores: {:?}", scores_from_vecs);
    
    // Accessing values
    match scores.get("Blue") {
        Some(score) => println!("Blue team score: {}", score),
        None => println!("Blue team not found"),
    }
    
    // Iterating over key-value pairs
    for (team, score) in &scores {
        println!("{}: {}", team, score);
    }
}

Updating Values

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    
    // Insert or update
    scores.insert("Blue", 10);
    scores.insert("Blue", 25); // Overwrites previous value
    
    // Insert only if key doesn't exist
    scores.entry("Yellow").or_insert(50);
    scores.entry("Blue").or_insert(100); // Won't change Blue's value
    
    // Update based on old value
    let counter = scores.entry("Red").or_insert(0);
    *counter += 10;
    
    // Update with a closure
    scores.entry("Green").and_modify(|score| *score += 10).or_insert(5);
    
    println!("Final scores: {:?}", scores);
}

Advanced HashMap Operations

use std::collections::HashMap;

fn word_count(text: &str) -> HashMap<String, usize> {
    let mut map = HashMap::new();
    
    for word in text.split_whitespace() {
        let count = map.entry(word.to_string()).or_insert(0);
        *count += 1;
    }
    
    map
}

fn group_by_length(words: Vec<&str>) -> HashMap<usize, Vec<&str>> {
    let mut groups: HashMap<usize, Vec<&str>> = HashMap::new();
    
    for word in words {
        groups.entry(word.len()).or_insert_with(Vec::new).push(word);
    }
    
    groups
}

fn main() {
    let text = "hello world hello rust world";
    let counts = word_count(text);
    println!("Word counts: {:?}", counts);
    
    let words = vec!["rust", "is", "awesome", "and", "fast"];
    let grouped = group_by_length(words);
    println!("Grouped by length: {:?}", grouped);
}

HashSet<T> - Unique Values

HashSet<T> stores unique values and provides fast membership testing.

Basic HashSet Operations

use std::collections::HashSet;

fn main() {
    let mut set = HashSet::new();
    
    // Adding elements
    set.insert("apple");
    set.insert("banana");
    set.insert("apple"); // Duplicate, won't be added
    
    println!("Set: {:?}", set);
    println!("Contains 'apple': {}", set.contains("apple"));
    println!("Set length: {}", set.len());
    
    // Removing elements
    if set.remove("banana") {
        println!("Removed banana");
    }
    
    // Creating from iterator
    let numbers: HashSet<i32> = (1..=5).collect();
    println!("Numbers: {:?}", numbers);
}

Set Operations

use std::collections::HashSet;

fn main() {
    let set1: HashSet<i32> = [1, 2, 3, 4].iter().cloned().collect();
    let set2: HashSet<i32> = [3, 4, 5, 6].iter().cloned().collect();
    
    // Union: elements in either set
    let union: HashSet<_> = set1.union(&set2).cloned().collect();
    println!("Union: {:?}", union);
    
    // Intersection: elements in both sets
    let intersection: HashSet<_> = set1.intersection(&set2).cloned().collect();
    println!("Intersection: {:?}", intersection);
    
    // Difference: elements in set1 but not in set2
    let difference: HashSet<_> = set1.difference(&set2).cloned().collect();
    println!("Difference: {:?}", difference);
    
    // Symmetric difference: elements in either set but not in both
    let sym_diff: HashSet<_> = set1.symmetric_difference(&set2).cloned().collect();
    println!("Symmetric difference: {:?}", sym_diff);
    
    // Subset checking
    let subset: HashSet<i32> = [1, 2].iter().cloned().collect();
    println!("Is {:?} a subset of {:?}? {}", subset, set1, subset.is_subset(&set1));
}

VecDeque<T> - Double-Ended Queue

VecDeque<T> allows efficient insertion and removal at both ends.

use std::collections::VecDeque;

fn main() {
    let mut deque = VecDeque::new();
    
    // Adding to both ends
    deque.push_back(1);
    deque.push_back(2);
    deque.push_front(0);
    deque.push_front(-1);
    
    println!("Deque: {:?}", deque); // [-1, 0, 1, 2]
    
    // Removing from both ends
    while let Some(value) = deque.pop_front() {
        println!("Popped from front: {}", value);
    }
    
    // Using as a ring buffer
    let mut ring_buffer = VecDeque::with_capacity(3);
    for i in 0..5 {
        if ring_buffer.len() == 3 {
            ring_buffer.pop_front();
        }
        ring_buffer.push_back(i);
        println!("Ring buffer: {:?}", ring_buffer);
    }
}

BTreeMap<K, V> and BTreeSet<T> - Sorted Collections

B-tree collections maintain elements in sorted order.

BTreeMap

use std::collections::BTreeMap;

fn main() {
    let mut map = BTreeMap::new();
    
    map.insert(3, "three");
    map.insert(1, "one");
    map.insert(4, "four");
    map.insert(2, "two");
    
    // Iteration is in sorted order by key
    for (key, value) in &map {
        println!("{}: {}", key, value);
    }
    
    // Range queries
    for (key, value) in map.range(2..=3) {
        println!("In range [2,3]: {}: {}", key, value);
    }
    
    // First and last entries
    if let Some((key, value)) = map.first_key_value() {
        println!("First: {}: {}", key, value);
    }
    
    if let Some((key, value)) = map.last_key_value() {
        println!("Last: {}: {}", key, value);
    }
}

BTreeSet

use std::collections::BTreeSet;

fn main() {
    let mut set = BTreeSet::new();
    
    set.insert(3);
    set.insert(1);
    set.insert(4);
    set.insert(2);
    
    // Iteration is in sorted order
    for value in &set {
        println!("Value: {}", value);
    }
    
    // Range operations
    let subset: BTreeSet<_> = set.range(2..=3).cloned().collect();
    println!("Subset [2,3]: {:?}", subset);
    
    // Split operations
    let split_set = set.split_off(&3);
    println!("Original (< 3): {:?}", set);
    println!("Split off (>= 3): {:?}", split_set);
}

BinaryHeap<T> - Priority Queue

BinaryHeap<T> is a max-heap that always keeps the largest element at the top.

use std::collections::BinaryHeap;

fn main() {
    let mut heap = BinaryHeap::new();
    
    // Adding elements
    heap.push(4);
    heap.push(1);
    heap.push(9);
    heap.push(3);
    heap.push(16);
    
    println!("Heap: {:?}", heap);
    
    // Peek at the largest element
    if let Some(&largest) = heap.peek() {
        println!("Largest element: {}", largest);
    }
    
    // Remove elements in descending order
    while let Some(value) = heap.pop() {
        println!("Popped: {}", value);
    }
    
    // Creating from vector
    let numbers = vec![2, 1, 5, 3, 4];
    let heap_from_vec = BinaryHeap::from(numbers);
    println!("Heap from vector: {:?}", heap_from_vec);
}

Custom Priority Queue

use std::collections::BinaryHeap;
use std::cmp::Reverse;

#[derive(Eq, PartialEq)]
struct Task {
    priority: u32,
    description: String,
}

impl Ord for Task {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.priority.cmp(&other.priority)
    }
}

impl PartialOrd for Task {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

fn main() {
    let mut task_queue = BinaryHeap::new();
    
    task_queue.push(Task {
        priority: 1,
        description: "Low priority task".to_string(),
    });
    
    task_queue.push(Task {
        priority: 5,
        description: "High priority task".to_string(),
    });
    
    task_queue.push(Task {
        priority: 3,
        description: "Medium priority task".to_string(),
    });
    
    // Process tasks in priority order (highest first)
    while let Some(task) = task_queue.pop() {
        println!("Processing: {} (priority: {})", task.description, task.priority);
    }
    
    // Min-heap using Reverse
    let mut min_heap: BinaryHeap<Reverse<i32>> = BinaryHeap::new();
    min_heap.push(Reverse(3));
    min_heap.push(Reverse(1));
    min_heap.push(Reverse(4));
    
    while let Some(Reverse(value)) = min_heap.pop() {
        println!("Min value: {}", value);
    }
}

Performance Characteristics

CollectionAccessInsertRemoveSearchOrdered
Vec<T>O(1)O(1)*O(n)O(n)No
VecDeque<T>O(1)O(1)O(1)O(n)No
HashMap<K,V>O(1)*O(1)*O(1)*O(1)*No
HashSet<T>O(1)*O(1)*O(1)*O(1)*No
BTreeMap<K,V>O(log n)O(log n)O(log n)O(log n)Yes
BTreeSet<T>O(log n)O(log n)O(log n)O(log n)Yes
BinaryHeap<T>O(1)O(log n)O(log n)O(n)Partial

*Amortized time complexity

Choosing the Right Collection

When to Use Vec<T>

// Good for: Sequential access, known size, cache-friendly operations
fn process_numbers(numbers: Vec<i32>) -> Vec<i32> {
    numbers.into_iter()
        .map(|x| x * 2)
        .filter(|&x| x > 10)
        .collect()
}

// Good for: Stack-like operations
fn reverse_polish_calculator(tokens: Vec<&str>) -> Option<i32> {
    let mut stack = Vec::new();
    
    for token in tokens {
        match token {
            "+" => {
                let b = stack.pop()?;
                let a = stack.pop()?;
                stack.push(a + b);
            }
            "-" => {
                let b = stack.pop()?;
                let a = stack.pop()?;
                stack.push(a - b);
            }
            num => {
                if let Ok(n) = num.parse::<i32>() {
                    stack.push(n);
                } else {
                    return None;
                }
            }
        }
    }
    
    stack.pop()
}

When to Use HashMap<K, V>

use std::collections::HashMap;

// Good for: Fast lookups, caching, frequency counting
fn build_index(words: Vec<String>) -> HashMap<String, Vec<usize>> {
    let mut index = HashMap::new();
    
    for (pos, word) in words.into_iter().enumerate() {
        index.entry(word).or_insert_with(Vec::new).push(pos);
    }
    
    index
}

// Good for: Configuration storage
fn load_config() -> HashMap<String, String> {
    let mut config = HashMap::new();
    config.insert("host".to_string(), "localhost".to_string());
    config.insert("port".to_string(), "8080".to_string());
    config.insert("debug".to_string(), "true".to_string());
    config
}

When to Use BTreeMap<K, V>

use std::collections::BTreeMap;

// Good for: Ordered iteration, range queries
fn analyze_grades(grades: Vec<(String, u32)>) -> BTreeMap<u32, Vec<String>> {
    let mut grade_groups = BTreeMap::new();
    
    for (student, grade) in grades {
        grade_groups.entry(grade).or_insert_with(Vec::new).push(student);
    }
    
    grade_groups
}

// Good for: Time-series data
fn record_temperature(readings: &mut BTreeMap<u64, f64>, timestamp: u64, temp: f64) {
    readings.insert(timestamp, temp);
}

fn get_temperature_range(readings: &BTreeMap<u64, f64>, start: u64, end: u64) -> Vec<f64> {
    readings.range(start..=end).map(|(_, &temp)| temp).collect()
}

Common Patterns

Builder Pattern with Collections

struct Report {
    title: String,
    sections: Vec<String>,
    metadata: std::collections::HashMap<String, String>,
}

impl Report {
    fn new(title: &str) -> Self {
        Report {
            title: title.to_string(),
            sections: Vec::new(),
            metadata: std::collections::HashMap::new(),
        }
    }
    
    fn add_section(mut self, section: &str) -> Self {
        self.sections.push(section.to_string());
        self
    }
    
    fn add_metadata(mut self, key: &str, value: &str) -> Self {
        self.metadata.insert(key.to_string(), value.to_string());
        self
    }
    
    fn build(self) -> Self {
        self
    }
}

fn main() {
    let report = Report::new("Monthly Report")
        .add_section("Introduction")
        .add_section("Data Analysis")
        .add_section("Conclusion")
        .add_metadata("author", "Alice")
        .add_metadata("date", "2024-01-15")
        .build();
    
    println!("Report: {}", report.title);
    println!("Sections: {:?}", report.sections);
    println!("Metadata: {:?}", report.metadata);
}

Working with Multiple Collections

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

struct Library {
    books: HashMap<String, Book>,
    authors: HashMap<String, HashSet<String>>, // author -> book_ids
    genres: HashMap<String, HashSet<String>>,  // genre -> book_ids
}

struct Book {
    id: String,
    title: String,
    author: String,
    genre: String,
}

impl Library {
    fn new() -> Self {
        Library {
            books: HashMap::new(),
            authors: HashMap::new(),
            genres: HashMap::new(),
        }
    }
    
    fn add_book(&mut self, book: Book) {
        let book_id = book.id.clone();
        let author = book.author.clone();
        let genre = book.genre.clone();
        
        // Add to books collection
        self.books.insert(book_id.clone(), book);
        
        // Update author index
        self.authors.entry(author).or_insert_with(HashSet::new).insert(book_id.clone());
        
        // Update genre index
        self.genres.entry(genre).or_insert_with(HashSet::new).insert(book_id);
    }
    
    fn books_by_author(&self, author: &str) -> Vec<&Book> {
        self.authors.get(author)
            .map(|book_ids| {
                book_ids.iter()
                    .filter_map(|id| self.books.get(id))
                    .collect()
            })
            .unwrap_or_default()
    }
    
    fn books_by_genre(&self, genre: &str) -> Vec<&Book> {
        self.genres.get(genre)
            .map(|book_ids| {
                book_ids.iter()
                    .filter_map(|id| self.books.get(id))
                    .collect()
            })
            .unwrap_or_default()
    }
}

Best Practices

1. Choose Based on Use Case

// Use Vec for sequences and when order matters
let scores = vec![85, 92, 78, 96, 88];

// Use HashMap for key-value associations
use std::collections::HashMap;
let mut user_scores = HashMap::new();
user_scores.insert("alice", 85);

// Use HashSet for unique items
use std::collections::HashSet;
let unique_visitors: HashSet<String> = HashSet::new();

// Use BTreeMap when you need sorted order
use std::collections::BTreeMap;
let sorted_scores: BTreeMap<String, i32> = BTreeMap::new();

2. Preallocate When Size is Known

// Good: Allocate with known capacity
let mut vec = Vec::with_capacity(1000);
let mut map = std::collections::HashMap::with_capacity(100);

// Avoid: Multiple reallocations
let mut vec = Vec::new(); // Will reallocate as it grows

3. Use Entry API for HashMap Operations

use std::collections::HashMap;

// Good: Use entry API
fn count_words(text: &str) -> HashMap<String, usize> {
    let mut counts = HashMap::new();
    for word in text.split_whitespace() {
        *counts.entry(word.to_string()).or_insert(0) += 1;
    }
    counts
}

// Avoid: Multiple lookups
fn count_words_inefficient(text: &str) -> HashMap<String, usize> {
    let mut counts = HashMap::new();
    for word in text.split_whitespace() {
        let word = word.to_string();
        if counts.contains_key(&word) {
            let count = counts.get(&word).unwrap();
            counts.insert(word, count + 1);
        } else {
            counts.insert(word, 1);
        }
    }
    counts
}

4. Consider Iterator Chaining

use std::collections::HashMap;

// Good: Iterator chaining
fn process_data(data: Vec<(String, i32)>) -> HashMap<String, i32> {
    data.into_iter()
        .filter(|(_, value)| *value > 0)
        .map(|(key, value)| (key.to_uppercase(), value * 2))
        .collect()
}

// More explicit but verbose
fn process_data_verbose(data: Vec<(String, i32)>) -> HashMap<String, i32> {
    let mut result = HashMap::new();
    for (key, value) in data {
        if value > 0 {
            result.insert(key.to_uppercase(), value * 2);
        }
    }
    result
}

Collections are fundamental to most Rust programs. Choose the right collection based on your access patterns, performance requirements, and whether you need ordering. Understanding the characteristics of each collection will help you write more efficient and idiomatic Rust code.