Collections · about 40 min

Vec in Depth

Why this matters

Vec<T> is the collection you will use most: a growable list of values of one type, stored contiguously on the heap. You have used it casually since module 2. Now that you know ownership and borrowing, it is worth understanding properly: how it is laid out, what push costs, why references into it are restricted, and the methods that make common list operations one-liners.

Creating and filling

fn main() {
    let mut a: Vec<i32> = Vec::new();
    a.push(10);
    a.push(20);
    let b = vec![1, 2, 3];
    let zeros = vec![0u8; 5];
    let words: Vec<&str> = "the quick fox".split(' ').collect();
    let squares: Vec<u32> = (1..=5).map(|x| x * x).collect();
    println!("{:?} {:?} {:?} {:?} {:?}", a, b, zeros, words, squares);
}
[10, 20] [1, 2, 3] [0, 0, 0, 0, 0] ["the", "quick", "fox"] [1, 4, 9, 16, 25]

Vec::new() starts empty (and allocates nothing until the first push), vec![...] builds from a list or a repeated value, and collect() gathers any iterator into a Vec.

The layout: pointer, length, capacity

A Vec<T> is the same shape as a String (which is in fact a Vec<u8> with a UTF-8 guarantee):

 stack                          heap
 +-------------+               +----+----+----+----+----+----+
 | ptr  ----------------------> | 10 | 20 | 30 | 40 |    |    |
 | len  4      |               +----+----+----+----+----+----+
 | cap  6      |                 in use (len)        spare
 +-------------+

len is how many elements exist; capacity is how many fit before the buffer must grow. When a push finds no room, the Vec allocates a bigger buffer (roughly double), moves the elements over and frees the old one:

fn main() {
    let mut v: Vec<u64> = Vec::new();
    let mut last_cap = v.capacity();
    for i in 0..20 {
        v.push(i);
        if v.capacity() != last_cap {
            println!("len {:2} -> capacity {:2}, buffer at {:p}", v.len(), v.capacity(), v.as_ptr());
            last_cap = v.capacity();
        }
    }
}
len  1 -> capacity  4, buffer at 0x56153fb3bae0
len  5 -> capacity  8, buffer at 0x56153fb3bae0
len  9 -> capacity 16, buffer at 0x56153fb3bae0
len 17 -> capacity 32, buffer at 0x56153fb3bae0

(Addresses differ between runs. Here the allocator happened to find room to extend the buffer in place; with other allocations around, it moves to a new address. Your code can never rely on either.) Doubling means that although an individual push may copy everything, the average cost per push stays constant ("amortized O(1)"). If you know the final size, Vec::with_capacity(n) allocates once up front.

This is also exactly why the borrow checker forbids holding a reference into a Vec across a push (module 5): the elements may move.

Reading elements

fn main() {
    let v = vec![10, 20, 30];
    let second = v[1];
    let maybe = v.get(5);
    println!("{second} {:?} {:?} {:?}", maybe, v.first(), v.last());
    println!("{} {}", v.contains(&20), v.len());
    if let Some(i) = v.iter().position(|&x| x == 30) {
        println!("30 is at index {i}");
    }
}
20 None Some(10) Some(30)
true 3
30 is at index 2

v[i] panics when out of range; v.get(i) returns Option<&T>. Use indexing when an out-of-range index would be a bug, get when it is an expected possibility (user-supplied index, optional neighbor).

Indexing a Vec of non-Copy values gives you a place, not a value you can move out:

fn main() {
    let names = vec![String::from("Ada"), String::from("Bob")];
    let first = names[0];
    println!("{first}");
}
error[E0507]: cannot move out of index of `Vec<String>`
 --> main.rs:3:17
  |
3 |     let first = names[0];
  |                 ^^^^^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait
  |
help: consider borrowing here
  |
3 |     let first = &names[0];
  |                 +
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let first = names[0].clone();
  |                         ++++++++

Borrow it (&names[0]), clone it, or remove it from the Vec (remove, pop, swap_remove), which transfers ownership out.

Changing a Vec

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    v.push(6);
    let last = v.pop();
    v.insert(0, 0);
    let removed = v.remove(2);
    v.swap(0, 1);
    v.truncate(4);
    println!("{:?} popped {:?} removed {removed}", v, last);
    v.extend([7, 8]);
    v.retain(|&x| x % 2 == 0);
    println!("{:?}", v);
    v.clear();
    println!("{:?} empty={}", v, v.is_empty());
}
[1, 0, 3, 4] popped Some(6) removed 2
[0, 4, 8]
[] empty=true

Costs matter: push and pop work at the end and are cheap. insert(0, x) and remove(0) shift every later element, so they are O(n); in a loop over a big Vec that becomes slow. If order does not matter, swap_remove(i) moves the last element into the hole in O(1). If you need cheap pushes and pops at both ends, use VecDeque (lesson 3).

Iterating three ways

fn main() {
    let mut v = vec![String::from("a"), String::from("b")];
    for s in &v {
        print!("{s} ");
    }
    println!();
    for s in &mut v {
        s.push('!');
    }
    println!("{:?}", v);
    for s in v {
        print!("{s} ");
    }
    println!();
}
a b 
["a!", "b!"]
a! b! 
loopeach itemthe Vec afterwards
for x in &v (or v.iter())&Tunchanged, usable
for x in &mut v (or v.iter_mut())&mut Tmodified, usable
for x in v (or v.into_iter())T, moved outconsumed, gone

Sorting and searching

fn main() {
    let mut v = vec![5, 3, 9, 1, 3];
    v.sort();
    println!("{:?}", v);
    v.dedup();
    println!("{:?} {:?}", v, v.binary_search(&5));
    v.sort_by(|a, b| b.cmp(a));
    println!("{:?}", v);
    let mut words = vec!["pear", "fig", "banana"];
    words.sort_by_key(|w| w.len());
    println!("{:?}", words);
    let mut fs = vec![2.5, -1.0, 3.75];
    fs.sort_by(|a, b| a.partial_cmp(b).unwrap());
    println!("{:?}", fs);
}
[1, 3, 3, 5, 9]
[1, 3, 5, 9] Ok(2)
[9, 5, 3, 1]
["fig", "pear", "banana"]
[-1.0, 2.5, 3.75]

sort is stable (equal elements keep their order). sort_by takes a comparison and sort_by_key a key function, both written as closures (module 11). Floats have no total order (NaN), so sort does not accept them; use partial_cmp or total_cmp. binary_search requires a sorted Vec and returns Ok(index) or Err(insert_position).

Vec and slices

A &Vec<T> converts automatically to &[T], and almost all the read-only methods (len, contains, iter, binary_search, windows...) are actually slice methods. So: store data in a Vec<T>, pass &[T] to functions that read it, and &mut Vec<T> only to functions that need to add or remove elements (a &mut [T] can change elements but not the length).

Common mistakes

Recap

Going further: look up drain, split_off, chunks_exact and concat in the Vec and slice docs, and try Vec::with_capacity(1000) followed by 1000 pushes, printing the capacity at the end.

Exercises

Exercise 1 · write code

Median of the input

Read integers from stdin (one per line) into a Vec<i64>, sort it, and print the count, minimum, maximum and median. For an even count, the median is the mean of the two middle values, printed with one decimal; for an odd count, print the middle value with one decimal as well:

count 5, min -2, max 9, median 4.0

for the input 9 4 -2 7 1, one number per line (sorted, it is -2 1 4 7 9). For no input, print no data.

File: main.rs

Starting point

use std::io;

fn main() {
    let mut v: Vec<i64> = Vec::new();
    for line in io::stdin().lines() {
        v.push(line.unwrap().trim().parse().unwrap());
    }
}

Example

input

9
4
-2
7
1

expected output

count 5, min -2, max 9, median 4.0

Example

input

3
1
4
2

expected output

count 4, min 1, max 4, median 2.5
Hint 1

After v.sort(), the minimum is v[0] and the maximum v[v.len() - 1].

Hint 2

Odd count: v[n / 2]. Even count: the average of v[n / 2 - 1] and v[n / 2].

Reference solution
use std::io;

fn main() {
    let mut v: Vec<i64> = Vec::new();
    for line in io::stdin().lines() {
        v.push(line.unwrap().trim().parse().unwrap());
    }
    if v.is_empty() {
        println!("no data");
        return;
    }
    v.sort();
    let n = v.len();
    let median = if n % 2 == 1 {
        v[n / 2] as f64
    } else {
        (v[n / 2 - 1] + v[n / 2]) as f64 / 2.0
    };
    println!("count {}, min {}, max {}, median {:.1}", n, v[0], v[n - 1], median);
}

Exercise 2 · write code

Clean up a list in place

Write fn clean(v: &mut Vec<i32>) that, in place:

  1. removes all negative numbers (retain),
  2. sorts the rest,
  3. removes duplicates (dedup).

The given main prints:

[0, 2, 3, 7]

File: main.rs

Starting point

fn clean(v: &mut Vec<i32>) {
}

fn main() {
    let mut v = vec![7, -1, 3, 2, 3, -8, 0, 7, 2];
    clean(&mut v);
    println!("{:?}", v);
}

Example

expected output

[0, 2, 3, 7]
Hint 1

v.retain(|&x| x >= 0); keeps only the elements for which the closure returns true.

Hint 2

dedup only removes consecutive duplicates, which is why you sort first.

Reference solution
fn clean(v: &mut Vec<i32>) {
    v.retain(|&x| x >= 0);
    v.sort();
    v.dedup();
}

fn main() {
    let mut v = vec![7, -1, 3, 2, 3, -8, 0, 7, 2];
    clean(&mut v);
    println!("{:?}", v);
}

Exercise 3 · quiz

What happens on push?

A Vec<u64> has len() == 4 and capacity() == 4. You call push. Which statements are true? Select all that apply.

Answer
  • A new, larger heap buffer is allocated (typically double the capacity)

  • The four existing elements are copied into the new buffer and the old buffer is freed

  • Any outstanding reference into the old buffer would dangle, which is why the borrow checker forbids holding one across push

The handle is always three words (pointer, length, capacity). Growing reallocates the heap buffer, moves the elements and frees the old one; doubling keeps the average cost of push constant. Because the elements move, references into the Vec cannot survive a push, and E0502 enforces exactly that.