Free Handbook · Every example compiled & verified

Data Structures & Algorithms

Vec, HashMap, VecDeque, BinaryHeap, a hand-built linked list and BST, graphs, binary search, sorting and dynamic programming in Rust.

0 / 140 lessons🔥 0 day streak
ShareXLinkedIn

Module 13 · what you'll be able to do

  • Pick a Rust collection from its cost table instead of by habit
  • Use Vec, VecDeque, HashMap, BTreeMap, HashSet and BinaryHeap the way real Rust code does
  • Build a singly linked list and a binary search tree with Option>, and explain why ownership makes linked structures hard
  • Represent a graph as an adjacency list and run BFS and DFS over it
  • Search and sort with binary_search, sort_by_key and sort_by, and turn slow recursion into dynamic programming
01

The cost table: which collection, and why

Rust's standard library ships most of the structures an interview or a real service needs, in std::collections plus Vec itself. The skill is not memorising their APIs; it is knowing what each operation costs, so the right one is picked before the code is written. This module builds the classic structures by hand where that teaches something (a linked list, a tree), then shows the standard-library type real code would use.

n = number of elements. "Amortised O(1)" means an occasional push reallocates and copies, but averaged over many pushes each one is constant time.
TypeAccessSearchInsertRemoveUse it for
Vec<T>O(1) by indexO(n)O(1) amortised at end, O(n) elsewhereO(1) at end, O(n) elsewhereThe default. Lists, stacks, buffers
VecDeque<T>O(1) by indexO(n)O(1) at both endsO(1) at both endsQueues, BFS, sliding windows
HashMap<K, V>—O(1) averageO(1) averageO(1) averageLookup by key, counting, caching
BTreeMap<K, V>—O(log n)O(log n)O(log n)Sorted keys, range queries, stable output
HashSet<T> / BTreeSet<T>—O(1) avg / O(log n)O(1) avg / O(log n)O(1) avg / O(log n)Membership, de-duplication
BinaryHeap<T>O(1) largest onlyO(n)O(log n)O(log n) (largest)Priority queues, top-k, Dijkstra
Linked list (by hand)O(n)O(n)O(1) at headO(1) at headLearning ownership; rarely production
Binary search tree (by hand)—O(log n) balanced, O(n) worstsamesameLearning recursion; use BTreeMap in production

Vec wins more often than the table suggests: its elements sit next to each other in memory, so the CPU cache loads them in batches. A linear scan of a 1,000-element Vec often beats a "faster" structure that chases pointers around the heap.

rustmain.rs
fn main() {
    let mut v: Vec<i32> = Vec::with_capacity(4);
    v.push(10);
    v.push(20);
    v.extend([30, 40, 50]);

    println!("len = {}, first = {}, last = {:?}", v.len(), v[0], v.last());
    println!("slice [1..3] = {:?}", &v[1..3]);
    println!("contains 30? {}", v.contains(&30));
    println!("get(99) = {:?}", v.get(99));

    v.insert(0, 5); // O(n): shifts every element right
    v.retain(|&x| x % 20 != 0);
    println!("{:?}", v);
}
Outputcompiled & run with real Rust
len = 5, first = 10, last = Some(50)
slice [1..3] = [20, 30]
contains 30? true
get(99) = None
[5, 10, 30, 50]

v[99] would panic; v.get(99) returns None. Prefer get whenever the index comes from outside the function.

Your turn

Replace retain with v.dedup() after pushing a duplicate value twice in a row, and print the result.

02

Vec as a stack

A stack is last in, first out. Rust needs no special type: Vec::push adds to the end and Vec::pop removes from the end, both O(1), and pop returns Option<T> so an empty stack is a value the compiler makes you handle, not a crash. Wrapping it in a small generic struct is still worth doing when the stack is part of a public API, because it hides the operations a stack must not allow (like indexing into the middle).

rustmain.rs
struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    fn new() -> Self { Stack { items: Vec::new() } }
    fn push(&mut self, value: T) { self.items.push(value); }
    fn pop(&mut self) -> Option<T> { self.items.pop() }
    fn peek(&self) -> Option<&T> { self.items.last() }
    fn len(&self) -> usize { self.items.len() }
}

fn main() {
    let mut s = Stack::new();
    s.push("a");
    s.push("b");
    s.push("c");
    println!("peek = {:?}, len = {}", s.peek(), s.len());
    while let Some(top) = s.pop() {
        print!("{} ", top);
    }
    println!();
    println!("empty pop = {:?}", s.pop());
}
Outputcompiled & run with real Rust
peek = Some("c"), len = 3
c b a 
empty pop = None
Your turn

Add an is_empty(&self) -> bool method and use it in the while loop instead of while let.

VisualizeStack operations, step by stepStep 1 / 6
let mut s = Stack::new();
s.push("a");
s.push("b");
s.push("c");
println!("{:?}", s.peek());
while let Some(top) = s.pop() {
print!("{} ", top);
}
Line 1

An empty stack; the Vec inside has length 0.

Variables now
items[]
All 6 steps as a table
StepLineWhat happenedVariables now
11An empty stack; the Vec inside has length 0.items = []
24Three pushes append to the end of the Vec.items = ["a", "b", "c"]
35peek borrows the last element without removing it.items = ["a", "b", "c"]
46pop removes "c" and returns Some("c"); the pattern binds top.top = "c" items = ["a", "b"]
57The loop repeats for "b" then "a".items = []
66pop on an empty Vec returns None, the pattern fails, and the loop ends.
03

HashMap, BTreeMap and HashSet

HashMap<K, V> is the workhorse for "look this up by key". Its iteration order is deliberately unpredictable: Rust seeds its hasher randomly on every run to resist denial-of-service attacks, so the same program can print keys in a different order each time. Whenever order matters for output, either collect and sort the entries, or use BTreeMap, which keeps keys sorted at O(log n) per operation.

rustmain.rs
use std::collections::HashMap;

fn main() {
    let text = "the cat and the hat and the bat";
    let mut counts: HashMap<&str, usize> = HashMap::new();
    for word in text.split_whitespace() {
        *counts.entry(word).or_insert(0) += 1;
    }

    println!("the = {}", counts["the"]);
    println!("dog = {:?}", counts.get("dog"));

    // HashMap order is random per run: sort before printing.
    let mut pairs: Vec<(&str, usize)> = counts.into_iter().collect();
    pairs.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
    for (word, n) in pairs {
        println!("{word}: {n}");
    }
}
Outputcompiled & run with real Rust
the = 3
dog = None
the: 3
and: 2
bat: 1
cat: 1
hat: 1

entry(key).or_insert(0) looks the key up once and returns a mutable reference to its value, inserting 0 first if it was missing. It is the idiomatic counter.

Your turn

Change the sort so ties are broken by word length, longest first.

rustmain.rs
use std::collections::{BTreeMap, HashSet};

fn main() {
    let mut scores = BTreeMap::new();
    scores.insert("mia", 91);
    scores.insert("ada", 88);
    scores.insert("zed", 75);
    scores.insert("bob", 88);

    // BTreeMap iterates in sorted key order, every run.
    for (name, score) in &scores {
        println!("{name} {score}");
    }
    let first_half: Vec<_> = scores.range("a".."m").map(|(k, _)| *k).collect();
    println!("names before m: {:?}", first_half);

    let a: HashSet<i32> = [1, 2, 3, 4].into_iter().collect();
    let b: HashSet<i32> = [3, 4, 5].into_iter().collect();
    let mut both: Vec<_> = a.intersection(&b).copied().collect();
    both.sort();
    println!("in both: {:?}, 5 in a? {}", both, a.contains(&5));
}
Outputcompiled & run with real Rust
ada 88
bob 88
mia 91
zed 75
names before m: ["ada", "bob"]
in both: [3, 4], 5 in a? false
Your turn

Build a HashSet from a Vec with duplicates, then print how many unique values it holds.

Keys must be Hash + Eq (or Ord for BTreeMap)
A struct used as a key needs #[derive(Hash, PartialEq, Eq)] for HashMap, or #[derive(PartialEq, Eq, PartialOrd, Ord)] for BTreeMap. f64 is neither Eq nor Hash (because NaN != NaN), so floats cannot be keys directly; store them as integer cents or use their bit pattern.
04

VecDeque: queues and deques

A queue is first in, first out. The trap is using Vec::remove(0): it shifts every remaining element left, so draining n items costs O(n²). VecDeque is a ring buffer — a Vec whose start index wraps around — so push_back, pop_front, push_front and pop_back are all O(1).

rustmain.rs
use std::collections::VecDeque;

fn main() {
    let mut queue = VecDeque::new();
    queue.push_back("ada");
    queue.push_back("bob");
    queue.push_back("cy");
    println!("serving {:?}", queue.pop_front());
    println!("serving {:?}", queue.pop_front());

    // A deque works from both ends.
    queue.push_front("vip");
    queue.push_back("dee");
    println!("front = {:?}, back = {:?}", queue.front(), queue.back());
    println!("all = {:?}", queue);

    let v: Vec<_> = queue.into_iter().collect();
    println!("as vec = {:?}", v);
}
Outputcompiled & run with real Rust
serving Some("ada")
serving Some("bob")
front = Some("vip"), back = Some("dee")
all = ["vip", "cy", "dee"]
as vec = ["vip", "cy", "dee"]
Your turn

Simulate a round-robin scheduler: pop a task from the front, print it, and push it to the back until each of three tasks has run twice.

05

BinaryHeap and Reverse: priority queues

BinaryHeap<T> is a max-heap: pop always returns the largest element, in O(log n), and peek shows it in O(1). For a min-heap, wrap each value in std::cmp::Reverse, which flips the ordering. Tuples compare field by field, so (priority, name) sorts by priority first — the usual way to attach data to a priority.

rustmain.rs
use std::cmp::Reverse;
use std::collections::BinaryHeap;

fn main() {
    let mut max_heap = BinaryHeap::from(vec![4, 9, 1, 7]);
    max_heap.push(5);
    print!("max-heap pops:");
    while let Some(x) = max_heap.pop() {
        print!(" {x}");
    }
    println!();

    let mut min_heap = BinaryHeap::new();
    for x in [4, 9, 1, 7, 5] {
        min_heap.push(Reverse(x));
    }
    let Reverse(smallest) = min_heap.pop().unwrap();
    println!("smallest = {smallest}, next = {:?}", min_heap.peek());

    // Tasks: lowest number = most urgent.
    let mut tasks = BinaryHeap::new();
    tasks.push(Reverse((2, "write tests")));
    tasks.push(Reverse((1, "fix outage")));
    tasks.push(Reverse((3, "update docs")));
    while let Some(Reverse((p, task))) = tasks.pop() {
        println!("p{p}: {task}");
    }
}
Outputcompiled & run with real Rust
max-heap pops: 9 7 5 4 1
smallest = 1, next = Some(Reverse(4))
p1: fix outage
p2: write tests
p3: update docs
rustmain.rs
use std::cmp::Reverse;
use std::collections::BinaryHeap;

// Top-k largest: keep a min-heap of size k. O(n log k).
fn top_k(nums: &[i32], k: usize) -> Vec<i32> {
    let mut heap = BinaryHeap::new();
    for &n in nums {
        heap.push(Reverse(n));
        if heap.len() > k {
            heap.pop(); // drop the smallest of the k+1
        }
    }
    let mut out: Vec<i32> = heap.into_iter().map(|Reverse(n)| n).collect();
    out.sort_unstable_by(|a, b| b.cmp(a));
    out
}

fn main() {
    println!("{:?}", top_k(&[5, 1, 9, 3, 7, 8, 2], 3));
}
Outputcompiled & run with real Rust
[9, 8, 7]

The heap never holds more than k items, so memory stays O(k) even for a stream of millions of numbers.

Your turn

Change top_k to return the k smallest numbers. Which heap do you need now?

06

A linked list by hand, and why it is hard in Rust

In most languages a linked list is the first structure beginners build. In Rust it is famously awkward, and the reason teaches ownership better than any other exercise. Every node must have exactly one owner. A singly linked list fits that rule — each node owns the next — so the type is Option<Box<Node>>: Box puts the next node on the heap (so the struct has a fixed size), and Option says the chain may end.

Error you will hit

A struct that contains itself has infinite size

rust
struct Node {
    value: i32,
    next: Option<Node>,
}
error[E0072]: recursive type `Node` has infinite size
 --> main.rs:1:1
  |
1 | struct Node {
  | ^^^^^^^^^^^
2 |     value: i32,
3 |     next: Option<Node>,
  |                  ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
Why the compiler said that

Rust must know every type's size at compile time. A Node holding an Option<Node> inline would contain a whole Node, which contains a whole Node, forever.

The fix

Put the next node behind a pointer. Box<Node> is one pointer wide no matter how long the chain is.

rust
struct Node {
    value: i32,
    next: Option<Box<Node>>,
}
rustmain.rs
struct Node {
    value: i32,
    next: Option<Box<Node>>,
}

struct List {
    head: Option<Box<Node>>,
    len: usize,
}

impl List {
    fn new() -> Self { List { head: None, len: 0 } }

    fn push_front(&mut self, value: i32) {
        // take() moves the old head out, leaving None behind.
        let old = self.head.take();
        self.head = Some(Box::new(Node { value, next: old }));
        self.len += 1;
    }

    fn pop_front(&mut self) -> Option<i32> {
        self.head.take().map(|node| {
            self.head = node.next;
            self.len -= 1;
            node.value
        })
    }

    fn reverse(&mut self) {
        let mut prev: Option<Box<Node>> = None;
        let mut cur = self.head.take();
        while let Some(mut node) = cur {
            cur = node.next.take();
            node.next = prev;
            prev = Some(node);
        }
        self.head = prev;
    }

    fn to_vec(&self) -> Vec<i32> {
        let mut out = Vec::new();
        let mut cur = &self.head;
        while let Some(node) = cur {
            out.push(node.value);
            cur = &node.next;
        }
        out
    }
}

fn main() {
    let mut list = List::new();
    for x in [3, 2, 1] {
        list.push_front(x);
    }
    println!("{:?} len={}", list.to_vec(), list.len);
    list.reverse();
    println!("reversed {:?}", list.to_vec());
    println!("pop {:?}, now {:?}", list.pop_front(), list.to_vec());
}
Outputcompiled & run with real Rust
[1, 2, 3] len=3
reversed [3, 2, 1]
pop Some(3), now [2, 1]

Option::take() is the key move: it swaps the value out for None, so a node can be moved without leaving the list temporarily pointing at freed memory — which the borrow checker would never allow.

Your turn

Add a peek(&self) -> Option<&i32> method using self.head.as_ref().map(|n| &n.value).

Visualizereverse(): three pointers, one owner eachStep 1 / 9
let mut prev: Option<Box<Node>> = None;
let mut cur = self.head.take();
while let Some(mut node) = cur {
cur = node.next.take();
node.next = prev;
prev = Some(node);
}
self.head = prev;
Line 1

prev will become the new head. It starts empty.

Variables now
prevNone
All 9 steps as a table
StepLineWhat happenedVariables now
11prev will become the new head. It starts empty.prev = None
22Move the whole chain out of the list; head is now None.cur = 1 -> 2 -> 3 head = None
33Take ownership of node 1.node = 1 cur = (moved)
44Detach the rest of the chain from node 1 and keep it in cur.node = 1 cur = 2 -> 3
55Point node 1 back at prev (None).node = 1 -> None
66node 1 becomes prev. Every value still has exactly one owner.prev = 1 cur = 2 -> 3
76Second pass does the same for node 2.prev = 2 -> 1 cur = 3
86Third pass for node 3; cur is now None, so the loop ends.prev = 3 -> 2 -> 1 cur = None
98Hand the reversed chain back to the list.head = 3 -> 2 -> 1

Why a doubly linked list is harder still

A doubly linked node is pointed at by two neighbours — which breaks "one owner". The safe options are Rc<RefCell<Node>> for forward links and Weak for back links (reference counting plus runtime borrow checks, see Module 10), or raw pointers inside unsafe, which is how std::collections::LinkedList is written. In real code, reach for Vec or VecDeque: they are faster for almost every workload, and an index into a Vec is a perfectly good "pointer" for graph and tree nodes (the arena pattern).

07

A binary search tree

A binary search tree keeps every value in the left subtree smaller than its node and every value in the right subtree larger. Each node owns up to two children, so the same Option<Box<Node>> shape works. Recursion follows the structure: insert goes left or right until it finds an empty slot, and an in-order walk (left, node, right) visits values in sorted order.

rustmain.rs
#[derive(Debug)]
struct Node {
    value: i32,
    left: Option<Box<Node>>,
    right: Option<Box<Node>>,
}

fn insert(slot: &mut Option<Box<Node>>, value: i32) {
    match slot {
        None => *slot = Some(Box::new(Node { value, left: None, right: None })),
        Some(node) if value < node.value => insert(&mut node.left, value),
        Some(node) if value > node.value => insert(&mut node.right, value),
        Some(_) => {} // duplicate: ignore
    }
}

fn contains(slot: &Option<Box<Node>>, value: i32) -> bool {
    match slot {
        None => false,
        Some(node) if value == node.value => true,
        Some(node) if value < node.value => contains(&node.left, value),
        Some(node) => contains(&node.right, value),
    }
}

fn in_order(slot: &Option<Box<Node>>, out: &mut Vec<i32>) {
    if let Some(node) = slot {
        in_order(&node.left, out);
        out.push(node.value);
        in_order(&node.right, out);
    }
}

fn height(slot: &Option<Box<Node>>) -> usize {
    match slot {
        None => 0,
        Some(n) => 1 + height(&n.left).max(height(&n.right)),
    }
}

fn main() {
    let mut root = None;
    for v in [50, 30, 70, 20, 40, 60, 80, 30] {
        insert(&mut root, v);
    }
    let mut sorted = Vec::new();
    in_order(&root, &mut sorted);
    println!("in-order: {:?}", sorted);
    println!("has 60? {}  has 65? {}", contains(&root, 60), contains(&root, 65));
    println!("height = {}", height(&root));
}
Outputcompiled & run with real Rust
in-order: [20, 30, 40, 50, 60, 70, 80]
has 60? true  has 65? false
height = 3
Your turn

Write min_value that walks left until node.left is None. Then insert 1..=7 in ascending order and print the height — why is it 7, not 3?

In production: BTreeMap
A hand-written BST degrades to a linked list (O(n) per operation) when values arrive sorted. BTreeMap and BTreeSet are self-balancing B-trees that stay O(log n) and keep many keys per node for cache efficiency. Write the BST to learn recursion and to pass the interview; use BTreeMap to ship.
08

Graphs: adjacency lists, BFS and DFS

The simplest Rust graph gives each node a number 0..n and stores neighbours in Vec<Vec<usize>>: adj[u] is the list of nodes u points to. Using indices instead of references sidesteps every ownership problem the linked list had. Breadth-first search explores level by level with a VecDeque and finds shortest paths in unweighted graphs; depth-first search goes as deep as it can first, with recursion or an explicit stack.

rustmain.rs
use std::collections::VecDeque;

fn bfs(adj: &Vec<Vec<usize>>, start: usize) -> (Vec<usize>, Vec<Option<usize>>) {
    let mut dist = vec![None; adj.len()];
    let mut order = Vec::new();
    let mut queue = VecDeque::new();
    dist[start] = Some(0);
    queue.push_back(start);
    while let Some(u) = queue.pop_front() {
        order.push(u);
        for &v in &adj[u] {
            if dist[v].is_none() {
                dist[v] = Some(dist[u].unwrap() + 1);
                queue.push_back(v);
            }
        }
    }
    (order, dist)
}

fn dfs(adj: &Vec<Vec<usize>>, u: usize, seen: &mut Vec<bool>, order: &mut Vec<usize>) {
    seen[u] = true;
    order.push(u);
    for &v in &adj[u] {
        if !seen[v] {
            dfs(adj, v, seen, order);
        }
    }
}

fn main() {
    // 0 - 1, 0 - 2, 1 - 3, 2 - 3, 3 - 4   (5 is isolated)
    let edges = [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)];
    let mut adj = vec![Vec::new(); 6];
    for &(a, b) in &edges {
        adj[a].push(b);
        adj[b].push(a);
    }

    let (order, dist) = bfs(&adj, 0);
    println!("BFS order: {:?}", order);
    println!("distance:  {:?}", dist);

    let mut seen = vec![false; adj.len()];
    let mut dfs_order = Vec::new();
    dfs(&adj, 0, &mut seen, &mut dfs_order);
    println!("DFS order: {:?}", dfs_order);
}
Outputcompiled & run with real Rust
BFS order: [0, 1, 2, 3, 4]
distance:  [Some(0), Some(1), Some(1), Some(2), Some(3), None]
DFS order: [0, 1, 3, 2, 4]

Node 5 has no edges, so BFS leaves its distance as None — unreachable, not zero.

Your turn

Count connected components: loop over every node, and each time you find one not yet seen, run dfs from it and add one to a counter. The answer here is 2.

VisualizeBFS from node 0: the queue is the frontierStep 1 / 7
dist[start] = Some(0);
queue.push_back(start);
while let Some(u) = queue.pop_front() {
order.push(u);
for &v in &adj[u] {
if dist[v].is_none() {
dist[v] = Some(dist[u].unwrap() + 1);
queue.push_back(v);
}
}
}
Line 2

Seed the queue with the start node at distance 0.

Variables now
queue[0]
dist[0, -, -, -, -, -]
All 7 steps as a table
StepLineWhat happenedVariables now
12Seed the queue with the start node at distance 0.queue = [0] dist = [0, -, -, -, -, -]
23Dequeue 0.u = 0 queue = []
38Neighbours 1 and 2 are unseen: distance 1, enqueue both.queue = [1, 2] dist = [0, 1, 1, -, -, -]
43Dequeue 1. Neighbour 0 is seen; 3 is new at distance 2.u = 1 queue = [2, 3] dist = [0, 1, 1, 2, -, -]
53Dequeue 2. Neighbours 0 and 3 are both seen already — nothing added.u = 2 queue = [3]
63Dequeue 3. Neighbour 4 is new at distance 3.u = 3 queue = [4] dist = [0, 1, 1, 2, 3, -]
73Dequeue 4; no new neighbours. The queue is empty, the loop ends. Node 5 was never reached.u = 4 queue = []

For weighted graphs, BFS becomes Dijkstra: swap the VecDeque for a BinaryHeap<Reverse<(u64, usize)>> so the node with the smallest known distance always comes out first.

rustmain.rs
use std::cmp::Reverse;
use std::collections::BinaryHeap;

fn dijkstra(adj: &Vec<Vec<(usize, u64)>>, start: usize) -> Vec<u64> {
    let mut dist = vec![u64::MAX; adj.len()];
    let mut heap = BinaryHeap::new();
    dist[start] = 0;
    heap.push(Reverse((0u64, start)));
    while let Some(Reverse((d, u))) = heap.pop() {
        if d > dist[u] {
            continue; // stale entry: a shorter path was already found
        }
        for &(v, w) in &adj[u] {
            let nd = d + w;
            if nd < dist[v] {
                dist[v] = nd;
                heap.push(Reverse((nd, v)));
            }
        }
    }
    dist
}

fn main() {
    let mut adj = vec![Vec::new(); 4];
    adj[0].push((1, 4));
    adj[0].push((2, 1));
    adj[2].push((1, 2));
    adj[1].push((3, 1));
    println!("{:?}", dijkstra(&adj, 0));
}
Outputcompiled & run with real Rust
[0, 3, 1, 4]

The direct edge 0 to 1 costs 4, but 0 to 2 to 1 costs 3, so the shortest distance to node 3 is 4.

09

Binary search and sorting

Binary search finds a value in a sorted slice in O(log n) by halving the range each step. Slices have it built in: binary_search returns Ok(index) if the value is present and Err(index) with the position where it would go if not — which is exactly what an insert-in-order needs. Writing it by hand once is still worth it, because interviewers ask for it and off-by-one mistakes are the whole point.

rustmain.rs
fn binary_search(items: &[i32], target: i32) -> Option<usize> {
    let (mut lo, mut hi) = (0, items.len()); // search [lo, hi)
    while lo < hi {
        let mid = lo + (hi - lo) / 2;
        if items[mid] == target {
            return Some(mid);
        } else if items[mid] < target {
            lo = mid + 1;
        } else {
            hi = mid;
        }
    }
    None
}

fn main() {
    let v = [2, 5, 8, 12, 16, 23, 38];
    println!("by hand: {:?} {:?}", binary_search(&v, 23), binary_search(&v, 7));
    println!("std:     {:?} {:?}", v.binary_search(&23), v.binary_search(&7));

    let mut sorted = vec![10, 20, 40];
    let pos = sorted.binary_search(&30).unwrap_or_else(|i| i);
    sorted.insert(pos, 30);
    println!("inserted: {:?}", sorted);

    // partition_point: first index where the predicate turns false.
    println!("first >= 12 at {}", v.partition_point(|&x| x < 12));
}
Outputcompiled & run with real Rust
by hand: Some(5) None
std:     Ok(5) Err(2)
inserted: [10, 20, 30, 40]
first >= 12 at 3
Your turn

Use partition_point twice to count how many values in v lie in the range 5..=23.

Visualizebinary_search(&v, 23) with a half-open rangeStep 1 / 5
let (mut lo, mut hi) = (0, items.len());
while lo < hi {
let mid = lo + (hi - lo) / 2;
if items[mid] == target {
return Some(mid);
} else if items[mid] < target {
lo = mid + 1;
} else {
hi = mid;
}
}
Line 1

Search the half-open range [0, 7).

Variables now
lo0
hi7
All 5 steps as a table
StepLineWhat happenedVariables now
11Search the half-open range [0, 7).lo = 0 hi = 7
23mid = 3; items[3] = 12 is less than 23, so the answer is to the right.mid = 3
37Discard mid and everything left of it.lo = 4 hi = 7
43mid = 5; items[5] = 23 matches.mid = 5
55Return Some(5) after two probes instead of six.

Sorting: sort, sort_unstable, sort_by_key, sort_by

sort is a stable merge-style sort (equal elements keep their original order), O(n log n). sort_unstable is usually faster and uses no extra memory, but may reorder equal elements. sort_by_key sorts by a derived key; sort_by takes a comparator returning Ordering, and .then_with chains tie-breakers.

rustmain.rs
#[derive(Debug)]
struct Player {
    name: &'static str,
    score: u32,
    age: u32,
}

fn main() {
    let mut nums = vec![5, 3, 9, 1, 3];
    nums.sort_unstable();
    println!("{:?}", nums);
    nums.sort_by(|a, b| b.cmp(a)); // descending
    println!("{:?}", nums);

    let mut players = vec![
        Player { name: "zoe", score: 90, age: 30 },
        Player { name: "ann", score: 75, age: 25 },
        Player { name: "bob", score: 90, age: 22 },
    ];
    players.sort_by_key(|p| p.age);
    let names: Vec<_> = players.iter().map(|p| p.name).collect();
    println!("by age: {:?}", names);

    players.sort_by(|a, b| b.score.cmp(&a.score).then_with(|| a.name.cmp(b.name)));
    for p in &players {
        println!("{} {} {}", p.name, p.score, p.age);
    }

    let mut prices: Vec<f64> = vec![3.5, 1.25, 2.0];
    prices.sort_by(|a, b| a.total_cmp(b));
    println!("{:?}", prices);
}
Outputcompiled & run with real Rust
[1, 3, 3, 5, 9]
[9, 5, 3, 3, 1]
by age: ["bob", "ann", "zoe"]
bob 90 22
zoe 90 30
ann 75 25
[1.25, 2.0, 3.5]
Your turn

Sort players by name length, then alphabetically, using sort_by_key(|p| (p.name.len(), p.name)).

Error you will hit

Sorting a Vec of floats with sort()

rust
fn main() {
    let mut prices = vec![3.5, 1.25, 2.0];
    prices.sort();
    println!("{:?}", prices);
}
error[E0277]: the trait bound `{float}: Ord` is not satisfied
   --> main.rs:3:12
    |
  3 |     prices.sort();
    |            ^^^^ the trait `Ord` is not implemented for `{float}`
Why the compiler said that

sort requires a total order (Ord). Floats only have a partial order, because NaN is not less than, equal to or greater than anything — including itself.

The fix

Say how to compare explicitly. f64::total_cmp gives every float, NaN included, a fixed position.

rust
fn main() {
    let mut prices: Vec<f64> = vec![3.5, 1.25, 2.0];
    prices.sort_by(|a, b| a.total_cmp(b));
    println!("{:?}", prices);
}

For interviews that ask you to write a sort yourself, merge sort is the one to know: split in half, sort each half recursively, merge. It is O(n log n) in every case and stable.

rustmain.rs
fn merge_sort(v: &[i32]) -> Vec<i32> {
    if v.len() <= 1 {
        return v.to_vec();
    }
    let mid = v.len() / 2;
    let (left, right) = (merge_sort(&v[..mid]), merge_sort(&v[mid..]));
    let mut out = Vec::with_capacity(v.len());
    let (mut i, mut j) = (0, 0);
    while i < left.len() && j < right.len() {
        if left[i] <= right[j] {
            out.push(left[i]);
            i += 1;
        } else {
            out.push(right[j]);
            j += 1;
        }
    }
    out.extend_from_slice(&left[i..]);
    out.extend_from_slice(&right[j..]);
    out
}

fn main() {
    println!("{:?}", merge_sort(&[38, 27, 43, 3, 9, 82, 10]));
}
Outputcompiled & run with real Rust
[3, 9, 10, 27, 38, 43, 82]
10

Recursion and dynamic programming

Recursion solves a problem by solving smaller copies of it, and needs a base case that stops. Rust has no guaranteed tail-call optimisation and a main-thread stack of a few megabytes, so recursion tens of thousands of levels deep can overflow — for deep inputs, convert to a loop with an explicit Vec stack. Dynamic programming fixes the other recursion problem: solving the same sub-problem over and over.

rustmain.rs
use std::collections::HashMap;

fn fib_naive(n: u64, calls: &mut u64) -> u64 {
    *calls += 1;
    if n < 2 { n } else { fib_naive(n - 1, calls) + fib_naive(n - 2, calls) }
}

fn fib_memo(n: u64, memo: &mut HashMap<u64, u64>) -> u64 {
    if n < 2 {
        return n;
    }
    if let Some(&v) = memo.get(&n) {
        return v;
    }
    let v = fib_memo(n - 1, memo) + fib_memo(n - 2, memo);
    memo.insert(n, v);
    v
}

fn fib_table(n: usize) -> u64 {
    let (mut a, mut b) = (0u64, 1u64);
    for _ in 0..n {
        (a, b) = (b, a + b);
    }
    a
}

fn main() {
    let mut calls = 0;
    println!("naive fib(25) = {} after {} calls", fib_naive(25, &mut calls), calls);
    println!("memo  fib(90) = {}", fib_memo(90, &mut HashMap::new()));
    println!("table fib(90) = {}", fib_table(90));
}
Outputcompiled & run with real Rust
naive fib(25) = 75025 after 242785 calls
memo  fib(90) = 2880067194370816120
table fib(90) = 2880067194370816120

The naive version makes 242,785 calls for n = 25 because it recomputes the same values; memoisation and the bottom-up table each do about n steps.

The DP recipe: (1) define what dp[i] means in one sentence, (2) write how dp[i] is built from smaller entries, (3) set the base cases, (4) fill the table in an order where every entry's inputs already exist. Coin change — the fewest coins that make an amount — is the classic.

rustmain.rs
// dp[a] = fewest coins that sum to exactly a, or None if impossible.
fn min_coins(coins: &[usize], amount: usize) -> Option<usize> {
    let mut dp: Vec<Option<usize>> = vec![None; amount + 1];
    dp[0] = Some(0);
    for a in 1..=amount {
        for &c in coins {
            if c <= a {
                if let Some(prev) = dp[a - c] {
                    dp[a] = Some(match dp[a] {
                        Some(best) => best.min(prev + 1),
                        None => prev + 1,
                    });
                }
            }
        }
    }
    dp[amount]
}

fn lcs(a: &str, b: &str) -> usize {
    let (a, b): (Vec<char>, Vec<char>) = (a.chars().collect(), b.chars().collect());
    let mut dp = vec![vec![0; b.len() + 1]; a.len() + 1];
    for i in 1..=a.len() {
        for j in 1..=b.len() {
            dp[i][j] = if a[i - 1] == b[j - 1] {
                dp[i - 1][j - 1] + 1
            } else {
                dp[i - 1][j].max(dp[i][j - 1])
            };
        }
    }
    dp[a.len()][b.len()]
}

fn main() {
    println!("coins for 11 with [1,2,5]: {:?}", min_coins(&[1, 2, 5], 11));
    println!("coins for 3 with [2]: {:?}", min_coins(&[2], 3));
    println!("LCS(\"abcde\", \"ace\") = {}", lcs("abcde", "ace"));
}
Outputcompiled & run with real Rust
coins for 11 with [1,2,5]: Some(3)
coins for 3 with [2]: None
LCS("abcde", "ace") = 3

Using Option<usize> instead of a sentinel like usize::MAX means an impossible amount can never be accidentally added to and overflow.

Your turn

Write count_ways(coins, amount): how many coin combinations sum to the amount. Hint: loop coins on the outside and amounts on the inside, with dp[0] = 1.

Quick check

Why does fib_memo take &mut HashMap instead of creating its own map?

11

Cheat sheet

You need…Reach forWatch out for
An ordered list, a stackVec<T>insert(0, x) and remove(0) are O(n)
A FIFO queue, BFSVecDeque<T>Never Vec::remove(0) in a loop
Lookup / count by keyHashMap<K, V> + entry()Random iteration order; sort before printing
Sorted keys, rangesBTreeMap / BTreeSetO(log n), not O(1)
Membership, de-dupHashSet<T>Keys need Hash + Eq; floats are neither
Always take the largestBinaryHeap<T>Max-heap; wrap in Reverse for min
Tree or graph nodesVec of nodes + usize indicesSelf-referential structs fight the borrow checker
Search a sorted slicebinary_search, partition_pointSlice must already be sorted
Sort structssort_by_key, sort_by + then_withFloats need total_cmp
Overlapping sub-problemsDP table in a VecDeep recursion can overflow the stack
Amortised O(1)
An operation that is occasionally expensive (a Vec reallocating) but constant time on average over many calls.
VecDeque
A growable ring buffer with O(1) push and pop at both ends; Rust's queue and deque type.
BinaryHeap
A max-heap priority queue; pop returns the largest element in O(log n).
Reverse
std::cmp::Reverse, a wrapper that inverts ordering; turns BinaryHeap into a min-heap.
Option<Box<Node>>
The standard link type for owned recursive structures: Box gives a fixed size, Option allows the end of the chain.
Option::take
Moves the value out of an Option and leaves None, letting you restructure owned links without breaking borrow rules.
Adjacency list
A graph stored as Vec>, where adj[u] lists the neighbours of node u.
Arena pattern
Storing all nodes in one Vec and linking them by index instead of by reference, avoiding ownership cycles.
Stable sort
A sort that keeps equal elements in their original relative order; slice::sort is stable, sort_unstable is not.
Memoisation
Caching the result of a function call so the same input is never computed twice.
Quick check

You need a min-priority queue of u32 values. What do you write?

Frequently asked questions

Does Rust have a built-in linked list?
Yes, std::collections::LinkedList is a doubly linked list, but it is rarely the right choice. Vec and VecDeque are faster for almost every workload because their elements are contiguous in memory. Linked lists are mostly written by hand in Rust to learn ownership.
Why does my HashMap print in a different order each time?
Rust seeds its default hasher randomly per process to resist hash-flooding attacks, so iteration order changes between runs. Collect the entries into a Vec and sort it, or use BTreeMap, whenever output order matters.
How do I make a min-heap in Rust?
Wrap values in std::cmp::Reverse before pushing them into a BinaryHeap: BinaryHeap>. Pop returns Reverse(smallest), which you unwrap with a pattern like let Reverse(x) = heap.pop().unwrap().

Finish the Rust handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.