Collections & Iterators

The types you'll use every day: Vec, String, HashMap, and the iterators that make them powerful.

String vs &str

&str is a read-only view of text. You can't change it. String is text you own — you can push to it, clear it, and return it from functions. Think of &str as "I'm looking at some text" and String as "this text is mine, I can modify it."
 1 │ // &str — borrowed, read-only
 2 │ let s: &str = "hello";
 3 │
 4 │ // String — owned, growable
 5 │ let mut s = String::from("hello");
 6 │ s.push(' ');
 7 │ s.push_str("world");
 8 │ s += "!";
 9 │
10 │ // Convert back and forth
11 │ let s: &str = &owned_string;
12 │ let owned: String = "hello".to_string();
13 │ let owned2: String = String::from("hello");
14 │
15 │ // format! builds Strings without copying
16 │ let name = "Bob";
17 │ let msg = format!("Hello, {}!", name);
Legend: 2 &str = borrowed string slice (read-only)   5 String = owned string that can grow   6-8 ways to grow a String   17 format! creates a new String

Important: indexing doesn't work

You can't do text[0] on a string in Rust. Text uses UTF-8 encoding, and one "character" can be multiple bytes (like emoji). Use .chars() to walk through actual characters, or .bytes() for raw bytes.
let s = "hello";
// s[0] — ERROR

for c in s.chars() { }  // each character
for b in s.bytes() { }  // each byte

// Common methods
s.len();                // byte count (not char count)
s.is_empty();
s.contains("ell");
s.replace("ell", "ipp");
s.trim();
s.to_uppercase();
s.split(' ').collect::>();

Vec<T>

Vec is a growable list. Items are stored in order. You push to the end, pop from the end. Use this when you have a list of things where order matters. It's the most common collection.
 1 │ let mut v: Vec<i32> = Vec::new();
 2 │ v.push(1);
 3 │ v.push(2);
 4 │ v.push(3);
 5 │
 6 │ // Or with the vec! macro (most common)
 7 │ let v = vec![1, 2, 3];
 8 │
 9 │ // Access
10 │ let first = &v[0];      // panic if empty
11 │ let safe = v.get(0);    // Option<&T>
12 │
13 │ // Remove
14 │ let last = v.pop();     // Option<T>
15 │
16 │ // Iterate (read)
17 │ for item in &v { }
18 │
19 │ // Iterate (modify)
20 │ for item in &mut v {
21 │     *item += 1;
22 │ }
23 │
24 │ // Iterate (consume)
25 │ for item in v { }  // v is moved here
26 │
27 │ // Useful methods
28 │ v.len();
29 │ v.is_empty();
30 │ v.contains(&1);
31 │ v.sort();
32 │ v.reverse();
33 │ v.clear();
34 │
35 │ // Capacity (performance)
36 │ let mut v = Vec::with_capacity(100);
37 │ v.capacity();  // 100
38 │ v.shrink_to_fit();
Legend: 1-4 create a Vec and push items   7 vec! macro is the idiomatic way   10 index with [] panics on empty   11 .get() returns Option (safe)   14 .pop() removes and returns last item   17-25 three ways to iterate: read, modify, consume   36 pre-allocate for performance

Common pattern: collect from iterator

let doubled: Vec = vec![1, 2, 3].iter().map(|x| x * 2).collect();

HashMap<K, V>

A HashMap (dictionary) stores key-value pairs. Use this when you need to look things up by name, count things, or create a mapping. It's like a real dictionary: you look up a word (key) and get the definition (value).
 1 │ use std::collections::HashMap;
 2 │
 3 │ let mut scores = HashMap::new();
 4 │ scores.insert("Alice".to_string(), 42);
 5 │ scores.insert("Bob".to_string(), 27);
 6 │
 7 │ // Get a value
 8 │ let a = scores.get("Alice");  // Option<&i32>
 9 │ let a = scores["Alice"];       // panic if missing
10 │
11 │ // Insert if not present
12 │ scores.entry("Carol".to_string()).or_insert(0);
13 │
14 │ // Update
15 │ scores.insert("Alice".to_string(), 50);  // overwrites
16 │
17 │ // Iterate
18 │ for (name, score) in &scores {
19 │     println!("{name}: {score}");
20 │ }
21 │
22 │ // Remove
23 │ scores.remove("Bob");
24 │
25 │ // Common pattern: count things
26 │ let text = "apple banana apple";
27 │ let mut counts = HashMap::new();
28 │ for word in text.split_whitespace() {
29 │     *counts.entry(word).or_insert(0) += 1;
30 │ }
Legend: 3-5 create a HashMap and insert keys   8 .get() returns Option (safe)   9 [] panics if key missing   12 entry() + or_insert() inserts only if absent   29 counting idiom: increment or default to 0

HashSet<T>

A set is a collection of unique items. Same as HashMap but you only store keys. Use this when you need to check "is this thing in the list?" quickly.
use std::collections::HashSet;

let mut seen = HashSet::new();
seen.insert("apple");
seen.insert("banana");
seen.insert("apple");  // no effect, already present

seen.contains("apple");  // true
seen.len();              // 2

for item in &seen { }

Iterators

Iterators are Rust's way of processing sequences. Instead of writing for-loops with indexes, you chain methods. The key idea: iterators are lazy — nothing happens until you call collect(), for_each(), or something that consumes them. This means you can build complex pipelines and Rust optimizes them away at compile time.

The Three Forms

 1 │ let v = vec![1, 2, 3];
 2 │
 3 │ v.iter();       // &T (borrow, read-only)
 4 │ v.iter_mut();   // &mut T (borrow, can modify)
 5 │ v.into_iter();  // T (consumes v, takes ownership)
Legend: 3 .iter() — borrow each item (read-only)   4 .iter_mut() — borrow each item (can modify)   5 .into_iter() — take ownership (v moves)

Common Iterator Methods

let nums = vec![1, 2, 3, 4, 5];

// map — transform each item
let doubled: Vec<_> = nums.iter().map(|x| x * 2).collect();

// filter — keep items that match
let evens: Vec<_> = nums.iter().filter(|x| *x % 2 == 0).collect();

// filter_map — filter and transform in one pass
let parsed: Vec = vec!["1", "two", "3"]
    .iter()
    .filter_map(|s| s.parse().ok())
    .collect();

// flat_map — each item becomes multiple items
let words: Vec<_> = vec!["hi there", "bye now"]
    .iter()
    .flat_map(|s| s.split_whitespace())
    .collect();

// take — first N items
let first_two: Vec<_> = nums.iter().take(2).collect();

// skip — skip first N
let after_two: Vec<_> = nums.iter().skip(2).collect();

// chain — combine two iterators
let combo: Vec<_> = vec![1, 2].iter().chain(vec![3, 4].iter()).collect();

// enumerate — get index + value
for (i, val) in nums.iter().enumerate() { }

// zip — combine two iterators
let pairs: Vec<_> = vec![1, 2, 3].iter()
    .zip(vec!["a", "b", "c"].iter())
    .collect();  // [(1, "a"), (2, "b"), (3, "c")]

// fold / reduce — accumulate into one value
let sum = nums.iter().fold(0, |acc, x| acc + x);

// all / any — check conditions
let all_positive = nums.iter().all(|x| x > &0);
let has_even = nums.iter().any(|x| x % 2 == 0);

// find — first match
let first_even = nums.iter().find(|x| *x % 2 == 0);

// position — index of first match
let idx = nums.iter().position(|x| x == &3);  // Some(2)

// count
let n = nums.iter().count();

// min / max / sum
let smallest = nums.iter().min();
let total: i32 = nums.iter().sum();

// collect into different collections
let set: HashSet<_> = vec![1, 1, 2, 2, 3].into_iter().collect();  // {1, 2, 3}
let map: HashMap<_, _> = vec![("a", 1), ("b", 2)].into_iter().collect();

Chaining (real example)

This is where Rust iterators shine. You chain methods together, and Rust compiles it to the same code as a hand-written loop. No overhead, full expressiveness.
1 │ // Get the sum of the first 10 even squares
2 │ let result: i32 = (1..)
3 │     .map(|n| n * n)
4 │     .filter(|n| n % 2 == 0)
5 │     .take(10)
6 │     .sum();
Legend: 2 (1..) infinite range from 1 onward   3 .map() square each number   4 .filter() keep only even numbers   5 .take() stop after 10 items   6 .sum() add them all up

Custom Iterator (for your own types)

struct Counter {
    count: u32,
    max: u32,
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option {
        if self.count >= self.max {
            None
        } else {
            self.count += 1;
            Some(self.count)
        }
    }
}

let count: Vec<_> = Counter { count: 0, max: 5 }.collect();
// [1, 2, 3, 4, 5]

Try It Yourself

Task 1: Word Counter

Write a function that takes a string and returns a HashMap<&str, u32> counting how many times each word appears.

fn count_words(text: &str) -> HashMap<&str, u32> {
    // your code here
    // hint: text.split_whitespace()
    // hint: entry().or_insert(0)
}
Show solution
fn count_words(text: &str) -> HashMap<&str, u32> {
    let mut counts = HashMap::new();
    for word in text.split_whitespace() {
        *counts.entry(word).or_insert(0) += 1;
    }
    counts
}

Task 2: Filter Even and Square

Given a Vec<i32>, return a new Vec with only the even numbers, each squared. Use iterator methods (map, filter, collect).

fn even_squares(nums: Vec) -> Vec {
    // one line with iterators
}
Show solution
fn even_squares(nums: Vec) -> Vec {
    nums.into_iter().filter(|x| x % 2 == 0).map(|x| x * x).collect()
}

Task 3: String Joiner

Write a function that joins a Vec of strings with a separator. Don't use the built-in join() method.

fn join_strings(strings: Vec<&str>, sep: &str) -> String {
    // your code here
    // hint: iter().enumerate(), push_str()
}
Show solution
fn join_strings(strings: Vec<&str>, sep: &str) -> String {
    let mut result = String::new();
    for (i, s) in strings.iter().enumerate() {
        if i > 0 { result.push_str(sep); }
        result.push_str(s);
    }
    result
}

Task 4: Group by First Letter

Given a Vec of words, return a HashMap<char, Vec<&str>> grouping them by their first letter.

fn group_by_first(words: Vec<&str>) -> HashMap> {
    // your code here
}
Show solution
fn group_by_first(words: Vec<&str>) -> HashMap> {
    let mut groups = HashMap::new();
    for word in words {
        if let Some(c) = word.chars().next() {
            groups.entry(c).or_insert(Vec::new()).push(word);
        }
    }
    groups
}

Summary