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
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);
}[] [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)).
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();
}
}
}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 -> 32The 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.
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);
}len 1000 capacity 1000 (unchanged: true)
reserved at least 50: true4. Giving memory back: shrink_to_fit and clear
clear() removes all elements but keeps the capacity for reuse. shrink_to_fit() releases unused capacity.
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());
}len 5 capacity 1024
after clear: len 0 capacity 1024
after shrink: len 1 capacity 1Reusing a cleared buffer in a loop instead of allocating a new Vec each iteration is a common performance technique.
5. Adding elements
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);
}["build", "test", "deploy", "notify", "cleanup", "report", "archive"]insert shifts every later element, so it is O(n).
6. Removing elements
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);
}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.
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);
}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.
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);
}["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.
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);
}[("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.
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);
}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.
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);
}["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.
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));
}no item #7; we have 3 items
Some("pizza") Some("salad") Some("pasta")13. Changing elements: get_mut, first_mut, last_mut
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());
}[0, 25, 60]
true14. swap, reverse, and rotate
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);
}["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.
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);
}[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
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()));
}true false
false17. 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.
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));
}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.
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);
}[1, 2, 3, 1]
["Ana@x.com", "ben@x.com", "cy@x.com"]
["go", "java", "rust"]19. Filling and resizing
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);
}[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
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"));
}true
true
true
Some(1)21. Joining and concatenating
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());
}ownership -> borrowing -> lifetimes
[1, 2, 3, 4, 5]
"ab"22. Two-dimensional vectors
Vec<Vec<T>> gives rows of possibly different lengths.
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);
}[0, 1, 2, 3]
[4, 5, 6, 7]
[8, 9, 10, 11]
column 2: [2, 6, 10], total 66For 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
#[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());
}[ ] p5 fix login bug
[x] p2 write docs
[x] p1 update deps
1 open24. 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.
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));
}
}(a[b]{c}) true
(] false
(( false
fn main() { let v = vec![1]; } true25. Moving elements between vectors without cloning
drain, retain, into_iter, and append move elements, so large items change owner without being copied.
#[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());
}sent 2 failed [Email { to: "bad-address", body: "oops" }], outbox empty: true26. 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.
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);
}[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.
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);
}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
#[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);
}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.
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));
}[(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.)
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);
}["log line 8", "log line 9", "log line 10"]
stored 6, dropped 4Draining 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); usewith_capacityorreservewhen you know the size.- Add with
push,insert,extend,append. Remove withpop,remove,swap_remove(O(1)),truncate,clear,drain,retain. - Read safely with
get,first,last; change withget_mut,first_mut,last_mut,iter_mut. - Sort with
sort,sort_unstable,sort_by,sort_by_key; search sorted data withbinary_search; remove duplicates withdedupafter sorting. - Every slice method (
chunks,windows,contains,join,split_at, ...) works on aVec. - Moving elements between vectors (
drain,into_iter,append) never copies them.
Next: HashMap.
