Skip to main content

Rust Vectors: A Comprehensive Guide to Growable Lists

Published: November 7, 2025 Updated: August 28, 2026 Larry Qu 7 min read

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/pop at the end are O(1) amortized; insert/remove at arbitrary positions are O(n)
  • Iteration over Vec is as fast as a C array — elements are contiguous in memory
  • For frequent insertions/removals at both ends, use VecDeque instead
  • 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)

Resources

Comments

👍 Was this article helpful?