Vec<T> is Rust’s most-used collection type. It’s a contiguous, heap-allocated, growable array that forms the backbone of nearly every Rust program. While the concept is familiar from other languages, Rust’s ownership model gives vectors some unique properties worth understanding deeply.
What Is a Vec<T>?
A vector stores elements of a single type (T) in contiguous memory on the heap. The Vec struct itself stores three values on the stack: a pointer to heap memory, the current length (how many elements are stored), and the capacity (how much memory is allocated).
Stack: Heap:
┌─────────────────────┐ ┌────────────────────────────┐
│ ptr ──────────────► │ │ [1] [2] [3] [4] [?] [?] │
│ len = 4 │ │ 0 1 2 3 (unused) │
│ capacity = 6 │ └────────────────────────────┘
└─────────────────────┘
When len == capacity and you push another element, Vec doubles its allocation, copies all elements, and frees the old memory.
Creating Vectors
fn main() {
// Empty vector with type annotation
let mut v1: Vec<i32> = Vec::new();
v1.push(1); v1.push(2); v1.push(3);
// From literal with macro
let v2 = vec![10, 20, 30];
// Pre-allocate to avoid reallocations
let mut v3: Vec<String> = Vec::with_capacity(1000);
println!("len={}, cap={}", v3.len(), v3.capacity()); // 0, 1000
// From iterator
let squares: Vec<u64> = (1..=10).map(|x| x * x).collect();
println!("{:?}", squares);
// Repeat a value n times
let zeros = vec![0i32; 5];
println!("{:?}", zeros); // [0, 0, 0, 0, 0]
// From a range
let hundred: Vec<i32> = (0..100).collect();
println!("len={}", hundred.len()); // 100
}
Accessing Elements
fn main() {
let v = vec![10, 20, 30, 40, 50];
// Index — panics on out of bounds
println!("{}", v[2]); // 30
// get() — returns Option<&T>
println!("{:?}", v.get(2)); // Some(30)
println!("{:?}", v.get(99)); // None
// First and last
println!("{:?}", v.first()); // Some(10)
println!("{:?}", v.last()); // Some(50)
// Slice
let middle = &v[1..4];
println!("{:?}", middle); // [20, 30, 40]
// Binary search (on sorted vec)
let sorted = vec![1, 3, 5, 7, 9];
match sorted.binary_search(&5) {
Ok(idx) => println!("Found at index {}", idx),
Err(idx) => println!("Not found, would insert at {}", idx),
}
}
The Borrow Checker and Vectors
You cannot hold a reference to a vector element while also modifying the vector:
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // immutable borrow of v
// v.push(4); // ERROR: cannot mutably borrow while immutably borrowed
// // Reason: push might reallocate, invalidating `first`
println!("{}", first); // Use first here — borrow ends after this
v.push(4); // Now allowed
}
This prevents an entire class of bugs: if push triggers a reallocation, first would point to freed memory. The borrow checker catches it at compile time.
Modifying Vectors
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
// Push and pop
v.push(6);
println!("{:?}", v.pop()); // Some(6)
// Insert and remove at index
v.insert(2, 99); // [1, 2, 99, 3, 4, 5]
v.remove(2); // [1, 2, 3, 4, 5]
// Extend with another iterable
v.extend([6, 7, 8]);
v.extend(vec![9, 10]);
// Truncate to length
v.truncate(5);
println!("{:?}", v); // [1, 2, 3, 4, 5]
// Clear all elements (keeps allocation)
let cap_before = v.capacity();
v.clear();
println!("len={}, cap={}", v.len(), v.capacity()); // 0, cap_before
// Retain only matching elements
let mut nums = vec![1, 2, 3, 4, 5, 6, 7, 8];
nums.retain(|&x| x % 2 == 0);
println!("{:?}", nums); // [2, 4, 6, 8]
// Drain elements by range (returns iterator of removed elements)
let mut data = vec!["a", "b", "c", "d", "e"];
let removed: Vec<_> = data.drain(1..3).collect();
println!("removed: {:?}", removed); // ["b", "c"]
println!("remaining: {:?}", data); // ["a", "d", "e"]
// Split off tail into new vec
let mut original = vec![1, 2, 3, 4, 5];
let tail = original.split_off(3);
println!("original: {:?}", original); // [1, 2, 3]
println!("tail: {:?}", tail); // [4, 5]
}
Sorting and Ordering
fn main() {
// Sort integers
let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6];
v.sort();
println!("{:?}", v); // [1, 1, 2, 3, 4, 5, 6, 9]
// Sort descending
v.sort_by(|a, b| b.cmp(a));
println!("{:?}", v); // [9, 6, 5, 4, 3, 2, 1, 1]
// Sort floats (requires sort_by with partial_cmp)
let mut floats = vec![3.14, 2.72, 1.41, 1.73];
floats.sort_by(|a, b| a.partial_cmp(b).unwrap());
println!("{:?}", floats);
// Sort structs by field
#[derive(Debug)]
struct Person { name: String, age: u32 }
let mut people = vec![
Person { name: "Alice".to_string(), age: 30 },
Person { name: "Bob".to_string(), age: 25 },
Person { name: "Carol".to_string(), age: 35 },
];
people.sort_by_key(|p| p.age);
for p in &people {
println!("{}: {}", p.name, p.age); // Bob:25, Alice:30, Carol:35
}
// Stable sort (preserves order of equal elements)
people.sort_by(|a, b| a.age.cmp(&b.age));
// Check if sorted
let sorted = vec![1, 2, 3, 4, 5];
println!("{}", sorted.windows(2).all(|w| w[0] <= w[1])); // true
}
Deduplication
fn main() {
// dedup: remove consecutive duplicates (sort first for full dedup)
let mut v = vec![1, 1, 2, 3, 3, 3, 4, 5, 5];
v.dedup();
println!("{:?}", v); // [1, 2, 3, 4, 5]
// Full deduplication
let with_dupes = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
let mut deduped = with_dupes.clone();
deduped.sort();
deduped.dedup();
println!("{:?}", deduped); // [1, 2, 3, 4, 5, 6, 9]
// dedup_by_key: deduplicate by computed key
let mut words = vec!["apple", "APPLE", "banana", "BANANA", "cherry"];
words.dedup_by_key(|s| s.to_lowercase());
println!("{:?}", words); // ["apple", "banana", "cherry"]
}
Iterating
fn main() {
let v = vec![1, 2, 3, 4, 5];
// Immutable references
for x in &v { print!("{} ", x); } println!();
// Mutable references
let mut v2 = v.clone();
for x in &mut v2 { *x *= 2; }
println!("{:?}", v2); // [2, 4, 6, 8, 10]
// Consuming (takes ownership)
let sum: i32 = v.into_iter().sum();
println!("Sum: {}", sum); // 15
// v is gone here
// With index
let words = vec!["alpha", "beta", "gamma"];
for (i, w) in words.iter().enumerate() {
println!("{}: {}", i, w);
}
// windows and chunks
let data = vec![1, 2, 3, 4, 5, 6];
for window in data.windows(3) {
println!("{:?}", window); // [1,2,3], [2,3,4], [3,4,5], [4,5,6]
}
for chunk in data.chunks(2) {
println!("{:?}", chunk); // [1,2], [3,4], [5,6]
}
}
Capacity Management
Understanding capacity is key to writing performant code:
fn main() {
// Without capacity hint — many reallocations
let mut v1: Vec<i32> = Vec::new();
for i in 0..1000 { v1.push(i); } // ~10 reallocations
// With capacity hint — zero reallocations
let mut v2 = Vec::with_capacity(1000);
for i in 0..1000 { v2.push(i); }
println!("v1 cap: {}", v1.capacity()); // 1024 (powers of 2)
println!("v2 cap: {}", v2.capacity()); // 1000 (exactly)
// Reserve more capacity
v2.reserve(500);
println!("After reserve: {}", v2.capacity()); // ≥ 1500
// Shrink to fit actual usage
v2.shrink_to_fit();
println!("After shrink: {}", v2.capacity()); // 1000
// shrink_to minimum viable capacity
v2.shrink_to(800);
// Measure allocation behavior
let mut v = Vec::new();
let mut last_cap = 0;
for i in 0..64 {
v.push(i);
if v.capacity() != last_cap {
println!("len={:2}, new cap={}", v.len(), v.capacity());
last_cap = v.capacity();
}
}
}
Common Patterns
Vec as a Stack
fn main() {
let mut stack: Vec<String> = Vec::new();
stack.push("first".to_string());
stack.push("second".to_string());
stack.push("third".to_string());
// LIFO — pop from end
while let Some(item) = stack.pop() {
println!("{}", item); // third, second, first
}
}
Flattening Nested Vecs
fn main() {
let nested = vec![vec![1, 2], vec![3, 4], vec![5, 6]];
// flat_map
let flat: Vec<i32> = nested.iter()
.flat_map(|v| v.iter().copied())
.collect();
println!("{:?}", flat); // [1, 2, 3, 4, 5, 6]
// concat (for Vec<Vec<T>>)
let also_flat: Vec<i32> = nested.concat();
println!("{:?}", also_flat);
}
Grouping Elements
use std::collections::HashMap;
fn group_by<T, K, F>(items: Vec<T>, key_fn: F) -> HashMap<K, Vec<T>>
where
K: Eq + std::hash::Hash,
F: Fn(&T) -> K,
{
let mut map: HashMap<K, Vec<T>> = HashMap::new();
for item in items {
map.entry(key_fn(&item)).or_default().push(item);
}
map
}
fn main() {
let words = vec!["apple", "ant", "bear", "avocado", "bee", "cherry"];
let groups = group_by(words, |w| w.chars().next().unwrap());
for (letter, words) in &groups {
println!("{}: {:?}", letter, words);
}
}
Partition
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let (evens, odds): (Vec<i32>, Vec<i32>) = numbers.into_iter().partition(|&x| x % 2 == 0);
println!("evens: {:?}", evens); // [2, 4, 6, 8, 10]
println!("odds: {:?}", odds); // [1, 3, 5, 7, 9]
}
Performance Notes
- Prefer
Vec::with_capacity(n)when the final size is known — eliminates reallocations push/popat the end are O(1) amortized;insert/removeat arbitrary positions are O(n)- Iteration over
Vecis as fast as a C array — elements are contiguous in memory - For frequent insertions/removals at both ends, use
VecDequeinstead - For a priority queue, use
BinaryHeap
Summary
Vec<T> is your default container for ordered sequences in Rust. Key methods to know:
| Operation | Method | Complexity |
|---|---|---|
| Add to end | push |
O(1) amortized |
| Remove from end | pop |
O(1) |
| Insert at index | insert(i, v) |
O(n) |
| Remove at index | remove(i) |
O(n) |
| Access by index | v[i], v.get(i) |
O(1) |
| Sort | sort(), sort_by() |
O(n log n) |
| Search | binary_search() |
O(log n) on sorted |
| Iterate | iter(), into_iter() |
O(n) |
Comments