Guide contents

Lesson 41 of 94

Vec in Depth

Vec<T> is the collection you'll use most. It's a growable array: elements are stored side by side in one heap allocation, with a length and a capacity.

You met Vec in A First Look at String, Vec & HashMap, and many slice methods in Slices. A Vec<T> automatically derefs to a slice &[T], so every slice method works on a Vec. This lesson covers the rest.

Operation Rust Vec
Create empty Vec::new() or vec![]
Create with capacity Vec::with_capacity(100)
Add at the end / at an index push(x) / insert(i, x)
Remove at an index remove(i) (or swap_remove(i), O(1))
Remove by value retain(...) or position + remove
Remove by predicate retain(|x| !pred(x))
Add all from another collection extend(other) or append(&mut other)
Remove a range drain(a..b)
Sort v.sort()
Binary search v.binary_search(&x)
Reverse / rotate v.reverse() / v.rotate_left(n)
Access by index v[i] (panics) or v.get(i) (Option)

1. Ways to create a Vec

Rust
fn main() {
    let empty: Vec<u32> = Vec::new();
    let literal = vec![1, 2, 3];
    let zeros = vec![0.0; 5];
    let from_array = Vec::from(["a", "b"]);
    let from_range: Vec<u32> = (1..=5).collect();
    let from_iter: Vec<String> = ["x", "y"].iter().map(|s| s.to_uppercase()).collect();
    println!("{:?} {:?} {:?} {:?} {:?} {:?}", empty, literal, zeros, from_array, from_range, from_iter);
}
Output
[] [1, 2, 3] [0.0, 0.0, 0.0, 0.0, 0.0] ["a", "b"] [1, 2, 3, 4, 5] ["X", "Y"]

2. Length vs capacity

len() is how many elements are stored; capacity() is how many fit before the vector must reallocate. When full, a Vec roughly doubles its capacity, so pushes are cheap on average (amortized O(1)).

Rust
fn main() {
    let mut v = Vec::new();
    let mut last_capacity = v.capacity();
    println!("len {} capacity {}", v.len(), v.capacity());
    for i in 0..20 {
        v.push(i);
        if v.capacity() != last_capacity {
            println!("after push #{:>2}: capacity {} -> {}", i + 1, last_capacity, v.capacity());
            last_capacity = v.capacity();
        }
    }
}
Output
len 0 capacity 0
after push # 1: capacity 0 -> 4
after push # 5: capacity 4 -> 8
after push # 9: capacity 8 -> 16
after push #17: capacity 16 -> 32

The exact growth strategy is an implementation detail and may change between Rust versions; don't rely on the numbers, only on "amortized O(1)".

3. Pre-allocating with with_capacity and reserve

When you know roughly how many elements you'll add, reserve space up front and avoid repeated reallocations.

Rust
fn main() {
    let incoming = 1_000;
    let mut ids = Vec::with_capacity(incoming);
    let initial_capacity = ids.capacity();
    for i in 0..incoming {
        ids.push(i as u32);
    }
    println!("len {} capacity {} (unchanged: {})", ids.len(), ids.capacity(), ids.capacity() == initial_capacity);

    let mut log: Vec<String> = Vec::new();
    log.reserve(50);
    println!("reserved at least 50: {}", log.capacity() >= 50);
}
Output
len 1000 capacity 1000 (unchanged: true)
reserved at least 50: true

4. Giving memory back: shrink_to_fit and clear

clear() removes all elements but keeps the capacity for reuse. shrink_to_fit() releases unused capacity.

Rust
fn main() {
    let mut buffer: Vec<u8> = Vec::with_capacity(1024);
    buffer.extend_from_slice(b"hello");
    println!("len {} capacity {}", buffer.len(), buffer.capacity());
    buffer.clear();
    println!("after clear: len {} capacity {}", buffer.len(), buffer.capacity());
    buffer.push(1);
    buffer.shrink_to_fit();
    println!("after shrink: len {} capacity {}", buffer.len(), buffer.capacity());
}
Output
len 5 capacity 1024
after clear: len 0 capacity 1024
after shrink: len 1 capacity 1

Reusing a cleared buffer in a loop instead of allocating a new Vec each iteration is a common performance technique.

5. Adding elements

Rust
fn main() {
    let mut steps = vec!["build", "deploy"];
    steps.push("notify");
    steps.insert(1, "test");
    steps.extend(["cleanup", "report"]);
    steps.extend_from_slice(&["archive"]);
    println!("{:?}", steps);
}
Output
["build", "test", "deploy", "notify", "cleanup", "report", "archive"]

insert shifts every later element, so it is O(n).

6. Removing elements

Rust
fn main() {
    let mut queue = vec!["a", "b", "c", "d", "e"];
    let last = queue.pop();         // O(1)
    let second = queue.remove(1);   // O(n): shifts the rest left
    println!("{:?} {} {:?}", last, second, queue);
    queue.truncate(2);               // keep the first 2
    println!("{:?}", queue);
}
Output
Some("e") b ["a", "c", "d"]
["a", "c"]

7. swap_remove: O(1) removal when order doesn't matter

swap_remove(i) moves the last element into position i instead of shifting everything. Use it whenever element order is irrelevant.

Rust
fn main() {
    let mut active_sessions = vec![101, 102, 103, 104, 105];
    let ended = active_sessions.swap_remove(1);
    println!("ended {ended}, remaining {:?}", active_sessions);
}
Output
ended 102, remaining [101, 105, 103, 104]

8. Removing by value

Vec has no remove(value). Find the position first, or use retain to remove every match.

Rust
fn main() {
    let mut subscribers = vec!["ana", "ben", "cy", "ben"];

    if let Some(i) = subscribers.iter().position(|&s| s == "ben") {
        subscribers.remove(i); // removes only the first "ben"
    }
    println!("{:?}", subscribers);

    subscribers.retain(|&s| s != "ben"); // removes every "ben"
    println!("{:?}", subscribers);
}
Output
["ana", "cy", "ben"]
["ana", "cy"]

9. retain and retain_mut: filter in place

retain keeps elements for which the closure returns true, in one pass, without allocating. retain_mut also lets you change the elements you keep.

Rust
fn main() {
    let mut orders = vec![("ana", 120, false), ("ben", 0, false), ("cy", 45, true), ("dee", 300, false)];
    orders.retain(|&(_, total, cancelled)| total > 0 && !cancelled);
    println!("{:?}", orders);

    let mut stock = vec![("pen", 3), ("ink", 0), ("pad", 1)];
    stock.retain_mut(|(_, qty)| {
        if *qty == 0 {
            return false;
        }
        *qty -= 1; // sell one of each remaining item
        true
    });
    println!("{:?}", stock);
}
Output
[("ana", 120, false), ("dee", 300, false)]
[("pen", 2), ("pad", 0)]

10. drain: remove a range and take ownership of it

drain(range) removes the elements in a range and returns an iterator that yields them by value. It's perfect for processing work in batches.

Rust
fn main() {
    let mut pending: Vec<String> = (1..=7).map(|i| format!("job-{i}")).collect();
    while !pending.is_empty() {
        let n = pending.len().min(3);
        let batch: Vec<String> = pending.drain(..n).collect();
        println!("processing {:?}", batch);
    }
    println!("pending now {:?}", pending);
}
Output
processing ["job-1", "job-2", "job-3"]
processing ["job-4", "job-5", "job-6"]
processing ["job-7"]
pending now []

11. append and split_off

append moves all elements from another vector (leaving it empty). split_off(i) splits a vector in two at index i.

Rust
fn main() {
    let mut today = vec!["a", "b"];
    let mut tomorrow = vec!["c", "d", "e"];
    today.append(&mut tomorrow);
    println!("{:?} {:?}", today, tomorrow);

    let overflow = today.split_off(3);
    println!("kept {:?}, overflow {:?}", today, overflow);
}
Output
["a", "b", "c", "d", "e"] []
kept ["a", "b", "c"], overflow ["d", "e"]

12. Reading safely: get, first, last

Indexing panics on a bad index. get returns an Option, which is the safe choice for indexes you didn't compute yourself.

Rust
fn main() {
    let menu = vec!["pizza", "pasta", "salad"];
    let choice = 7;
    match menu.get(choice) {
        Some(item) => println!("you ordered {item}"),
        None => println!("no item #{choice}; we have {} items", menu.len()),
    }
    println!("{:?} {:?} {:?}", menu.first(), menu.last(), menu.get(1));
}
Output
no item #7; we have 3 items
Some("pizza") Some("salad") Some("pasta")

13. Changing elements: get_mut, first_mut, last_mut

Rust
fn main() {
    let mut scores = vec![10, 20, 30];
    if let Some(s) = scores.get_mut(1) {
        *s += 5;
    }
    if let Some(first) = scores.first_mut() {
        *first = 0;
    }
    *scores.last_mut().unwrap() *= 2;
    println!("{:?}", scores);
    println!("{:?}", scores.get_mut(10).is_none());
}
Output
[0, 25, 60]
true

14. swap, reverse, and rotate

Rust
fn main() {
    let mut playlist = vec!["intro", "verse", "chorus", "bridge", "outro"];
    playlist.swap(1, 2);
    println!("{:?}", playlist);
    playlist.reverse();
    println!("{:?}", playlist);
    playlist.rotate_left(2);
    println!("{:?}", playlist);
    playlist.rotate_right(1);
    println!("{:?}", playlist);
}
Output
["intro", "chorus", "verse", "bridge", "outro"]
["outro", "bridge", "verse", "chorus", "intro"]
["verse", "chorus", "intro", "outro", "bridge"]
["bridge", "verse", "chorus", "intro", "outro"]

15. Sorting: sort, sort_unstable, sort_by, sort_by_key

sort is stable (equal elements keep their order). sort_unstable is faster and uses no extra memory, when stability doesn't matter.

Rust
fn main() {
    let mut ages = vec![34, 21, 55, 21, 40];
    ages.sort_unstable();
    println!("{:?}", ages);

    let mut people = vec![("ana", 34), ("ben", 21), ("cy", 34), ("dee", 21)];
    people.sort_by_key(|&(_, age)| age); // stable: ana stays before cy
    println!("{:?}", people);

    people.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
    println!("{:?}", people);

    let mut words = vec!["banana", "Apple", "cherry"];
    words.sort_by_key(|w| std::cmp::Reverse(w.len()));
    println!("{:?}", words);
}
Output
[21, 21, 34, 40, 55]
[("ben", 21), ("dee", 21), ("ana", 34), ("cy", 34)]
[("ana", 34), ("cy", 34), ("ben", 21), ("dee", 21)]
["banana", "cherry", "Apple"]

std::cmp::Reverse wraps a key to sort in descending order.

16. Checking order: is_sorted

Rust
fn main() {
    let timestamps = vec![100, 250, 250, 900];
    let shuffled = vec![3, 1, 2];
    println!("{} {}", timestamps.is_sorted(), shuffled.is_sorted());
    let names = ["ana", "ben", "cy"];
    println!("{}", names.is_sorted_by_key(|n| n.len()));
}
Output
true false
false

17. Binary search on a sorted Vec

binary_search returns Ok(index) if found, or Err(index) where the value would be inserted to keep the vector sorted. That makes sorted insertion a one-liner.

Rust
fn main() {
    let mut sorted = vec![10, 20, 30, 40];
    println!("{:?} {:?}", sorted.binary_search(&30), sorted.binary_search(&25));

    for x in [25, 5, 50, 30] {
        let pos = sorted.binary_search(&x).unwrap_or_else(|insert_at| insert_at);
        sorted.insert(pos, x);
    }
    println!("{:?}", sorted);

    let users = vec![(1, "ana"), (4, "ben"), (9, "cy")];
    println!("{:?}", users.binary_search_by_key(&4, |&(id, _)| id).map(|i| users[i].1));
}
Output
Ok(2) Err(2)
[5, 10, 20, 25, 30, 30, 40, 50]
Ok("ben")

partition_point(|x| pred(x)) is the related tool for "the first index where a condition becomes false".

18. Removing duplicates: dedup and dedup_by_key

dedup removes consecutive duplicates. Sort first to remove all duplicates.

Rust
fn main() {
    let mut readings = vec![1, 1, 2, 2, 2, 3, 1, 1];
    readings.dedup();
    println!("{:?}", readings);

    let mut emails = vec!["Ana@x.com", "ana@x.com", "ben@x.com", "BEN@x.com", "cy@x.com"];
    emails.dedup_by_key(|e| e.to_lowercase());
    println!("{:?}", emails);

    let mut tags = vec!["rust", "go", "rust", "java", "go"];
    tags.sort();
    tags.dedup();
    println!("{:?}", tags);
}
Output
[1, 2, 3, 1]
["Ana@x.com", "ben@x.com", "cy@x.com"]
["go", "java", "rust"]

19. Filling and resizing

Rust
fn main() {
    let mut seats = vec![false; 5];
    seats[1..3].fill(true);
    println!("{:?}", seats);

    seats.resize(8, false); // grow, filling with false
    println!("{:?}", seats);
    seats.resize(3, false); // shrink
    println!("{:?}", seats);

    let mut ids: Vec<u32> = Vec::new();
    let mut next = 100;
    ids.resize_with(4, || {
        next += 1;
        next
    });
    println!("{:?}", ids);
}
Output
[false, true, true, false, false]
[false, true, true, false, false, false, false, false]
[false, true, true]
[101, 102, 103, 104]

20. Searching: contains, starts_with, ends_with, iter().position

Rust
fn main() {
    let path = vec!["usr", "local", "bin", "rustc"];
    println!("{}", path.contains(&"bin"));
    println!("{}", path.starts_with(&["usr", "local"]));
    println!("{}", path.ends_with(&["rustc"]));
    println!("{:?}", path.iter().position(|&p| p == "local"));
}
Output
true
true
true
Some(1)

21. Joining and concatenating

Rust
fn main() {
    let words = vec!["ownership", "borrowing", "lifetimes"];
    println!("{}", words.join(" -> "));
    let parts = vec![vec![1, 2], vec![3], vec![4, 5]];
    println!("{:?}", parts.concat());
    let lines = vec![String::from("a"), String::from("b")];
    println!("{:?}", lines.concat());
}
Output
ownership -> borrowing -> lifetimes
[1, 2, 3, 4, 5]
"ab"

22. Two-dimensional vectors

Vec<Vec<T>> gives rows of possibly different lengths.

Rust
fn main() {
    let rows = 3;
    let cols = 4;
    let mut grid = vec![vec![0; cols]; rows];
    for (r, row) in grid.iter_mut().enumerate() {
        for (c, cell) in row.iter_mut().enumerate() {
            *cell = r * cols + c;
        }
    }
    for row in &grid {
        println!("{:?}", row);
    }
    let column_2: Vec<usize> = grid.iter().map(|row| row[2]).collect();
    let total: usize = grid.iter().flatten().sum();
    println!("column 2: {:?}, total {total}", column_2);
}
Output
[0, 1, 2, 3]
[4, 5, 6, 7]
[8, 9, 10, 11]
column 2: [2, 6, 10], total 66

For large fixed-size grids, a single Vec of rows * cols indexed with r * cols + c is faster, because all cells are in one allocation.

23. A Vec of structs

Rust
#[derive(Debug, Clone)]
struct Task {
    id: u32,
    title: String,
    done: bool,
    priority: u8,
}

fn main() {
    let mut tasks = vec![
        Task { id: 1, title: String::from("write docs"), done: false, priority: 2 },
        Task { id: 2, title: String::from("fix login bug"), done: false, priority: 5 },
        Task { id: 3, title: String::from("update deps"), done: true, priority: 1 },
    ];
    if let Some(t) = tasks.iter_mut().find(|t| t.id == 1) {
        t.done = true;
    }
    tasks.sort_by(|a, b| a.done.cmp(&b.done).then(b.priority.cmp(&a.priority)));
    for t in &tasks {
        println!("[{}] p{} {}", if t.done { "x" } else { " " }, t.priority, t.title);
    }
    let open: Vec<&Task> = tasks.iter().filter(|t| !t.done).collect();
    println!("{} open", open.len());
}
Output
[ ] p5 fix login bug
[x] p2 write docs
[x] p1 update deps
1 open

24. Vec as a stack

push and pop at the end are O(1), so a Vec is Rust's stack. There is no need for a separate stack type; just use Vec.

Rust
fn balanced(expr: &str) -> bool {
    let mut stack = Vec::new();
    for c in expr.chars() {
        match c {
            '(' | '[' | '{' => stack.push(c),
            ')' => if stack.pop() != Some('(') { return false; },
            ']' => if stack.pop() != Some('[') { return false; },
            '}' => if stack.pop() != Some('{') { return false; },
            _ => {}
        }
    }
    stack.is_empty()
}

fn main() {
    for e in ["(a[b]{c})", "(]", "((", "fn main() { let v = vec![1]; }"] {
        println!("{e:<32} {}", balanced(e));
    }
}
Output
(a[b]{c})                        true
(]                               false
((                               false
fn main() { let v = vec![1]; }   true

25. Moving elements between vectors without cloning

drain, retain, into_iter, and append move elements, so large items change owner without being copied.

Rust
#[derive(Debug)]
struct Email {
    to: String,
    body: String,
}

fn main() {
    let mut outbox = vec![
        Email { to: String::from("a@x.com"), body: String::from("hi") },
        Email { to: String::from("bad-address"), body: String::from("oops") },
        Email { to: String::from("b@x.com"), body: String::from("hello") },
    ];
    let mut sent = Vec::new();
    let mut failed = Vec::new();
    for email in outbox.drain(..) {
        if email.to.contains('@') { sent.push(email) } else { failed.push(email) }
    }
    println!("sent {} failed {:?}, outbox empty: {}", sent.len(), failed, outbox.is_empty());
}
Output
sent 2 failed [Email { to: "bad-address", body: "oops" }], outbox empty: true

26. Vec<Option<T>> for fixed slots

A vector of Options models a fixed set of slots that can be empty, such as parking spaces or connection pool entries.

Rust
fn main() {
    let mut spaces: Vec<Option<&str>> = vec![None; 4];
    let arrivals = ["car-A", "car-B", "car-C"];
    for car in arrivals {
        if let Some(slot) = spaces.iter_mut().find(|s| s.is_none()) {
            *slot = Some(car);
        }
    }
    spaces[1] = None; // car-B leaves
    let free = spaces.iter().filter(|s| s.is_none()).count();
    println!("{:?}, {free} free", spaces);
    let parked: Vec<&str> = spaces.iter().flatten().copied().collect();
    println!("{:?}", parked);
}
Output
[Some("car-A"), None, Some("car-C"), None], 2 free
["car-A", "car-C"]

27. Realistic example: a sorted leaderboard with bounded size

Insert scores in order using binary_search_by, and keep only the top N with truncate.

Rust
struct Leaderboard {
    entries: Vec<(u32, String)>,
    max: usize,
}

impl Leaderboard {
    fn submit(&mut self, name: &str, score: u32) -> Option<usize> {
        let pos = self.entries.binary_search_by(|(s, _)| score.cmp(s)).unwrap_or_else(|p| p);
        if pos >= self.max {
            return None;
        }
        self.entries.insert(pos, (score, name.to_string()));
        self.entries.truncate(self.max);
        Some(pos + 1)
    }
}

fn main() {
    let mut board = Leaderboard { entries: Vec::new(), max: 3 };
    for (name, score) in [("ana", 500), ("ben", 900), ("cy", 700), ("dee", 100), ("eve", 800)] {
        match board.submit(name, score) {
            Some(rank) => println!("{name} enters at #{rank}"),
            None => println!("{name} ({score}) didn't make the board"),
        }
    }
    println!("{:?}", board.entries);
}
Output
ana enters at #1
ben enters at #1
cy enters at #2
dee (100) didn't make the board
eve enters at #2
[(900, "ben"), (800, "eve"), (700, "cy")]

score.cmp(s) compares in reverse (the new score against each entry), which keeps the vector sorted from highest to lowest.

28. Realistic example: an in-memory todo service

Rust
#[derive(Debug)]
struct Todo {
    id: u32,
    title: String,
    done: bool,
}

struct TodoList {
    items: Vec<Todo>,
    next_id: u32,
}

impl TodoList {
    fn add(&mut self, title: &str) -> u32 {
        self.next_id += 1;
        self.items.push(Todo { id: self.next_id, title: title.to_string(), done: false });
        self.next_id
    }

    fn complete(&mut self, id: u32) -> bool {
        match self.items.iter_mut().find(|t| t.id == id) {
            Some(t) => {
                t.done = true;
                true
            }
            None => false,
        }
    }

    fn clear_completed(&mut self) -> Vec<Todo> {
        let (done, open): (Vec<Todo>, Vec<Todo>) = self.items.drain(..).partition(|t| t.done);
        self.items = open;
        done
    }
}

fn main() {
    let mut list = TodoList { items: Vec::new(), next_id: 0 };
    let a = list.add("buy milk");
    list.add("write report");
    let c = list.add("call plumber");
    list.complete(a);
    list.complete(c);
    println!("complete 99: {}", list.complete(99));
    let archived = list.clear_completed();
    println!("archived {:?}", archived.iter().map(|t| &t.title).collect::<Vec<_>>());
    println!("open {:?}", list.items);
}
Output
complete 99: false
archived ["buy milk", "call plumber"]
open [Todo { id: 2, title: "write report", done: false }]

29. Realistic example: merging overlapping time ranges

Sort by start time, then merge each range into the last one when they overlap. A classic interview and calendar problem.

Rust
fn merge(mut ranges: Vec<(u32, u32)>) -> Vec<(u32, u32)> {
    ranges.sort_by_key(|&(start, _)| start);
    let mut merged: Vec<(u32, u32)> = Vec::with_capacity(ranges.len());
    for (start, end) in ranges {
        match merged.last_mut() {
            Some(last) if start <= last.1 => last.1 = last.1.max(end),
            _ => merged.push((start, end)),
        }
    }
    merged
}

fn main() {
    let busy = vec![(900, 1000), (1300, 1400), (930, 1100), (1400, 1500), (1600, 1630)];
    println!("{:?}", merge(busy));
}
Output
[(900, 1100), (1300, 1500), (1600, 1630)]

30. Realistic example: a sliding log buffer that keeps the last N lines

When the buffer is full, the oldest lines are drained off the front. (For very frequent removals from the front, VecDeque is better; see VecDeque & BinaryHeap.)

Rust
struct TailBuffer {
    lines: Vec<String>,
    keep: usize,
    dropped: usize,
}

impl TailBuffer {
    fn push(&mut self, line: &str) {
        self.lines.push(line.to_string());
        if self.lines.len() > self.keep * 2 {
            let excess = self.lines.len() - self.keep;
            self.dropped += self.lines.drain(..excess).count();
        }
    }

    fn tail(&self) -> &[String] {
        let start = self.lines.len().saturating_sub(self.keep);
        &self.lines[start..]
    }
}

fn main() {
    let mut buf = TailBuffer { lines: Vec::new(), keep: 3, dropped: 0 };
    for i in 1..=10 {
        buf.push(&format!("log line {i}"));
    }
    println!("{:?}", buf.tail());
    println!("stored {}, dropped {}", buf.lines.len(), buf.dropped);
}
Output
["log line 8", "log line 9", "log line 10"]
stored 6, dropped 4

Draining only when the buffer reaches twice the target size makes each drain move many lines at once, keeping the average cost per push low.

Summary

  • Vec<T> stores elements contiguously with a length and a capacity. Growth is amortized O(1); use with_capacity or reserve when you know the size.
  • Add with push, insert, extend, append. Remove with pop, remove, swap_remove (O(1)), truncate, clear, drain, retain.
  • Read safely with get, first, last; change with get_mut, first_mut, last_mut, iter_mut.
  • Sort with sort, sort_unstable, sort_by, sort_by_key; search sorted data with binary_search; remove duplicates with dedup after sorting.
  • Every slice method (chunks, windows, contains, join, split_at, ...) works on a Vec.
  • Moving elements between vectors (drain, into_iter, append) never copies them.

Next: HashMap.