Rust for Coding Interviews: The Standard Library Cheat Sheet

May 26, 20269 min read
interview-prepdsaalgorithmsdata-structures
Rust for Coding Interviews: The Standard Library Cheat Sheet
TL;DR
  • Vec, VecDeque, HashMap, BTreeMap, BinaryHeap, HashSet are the six collections you need cold before your interview.
  • The entry API (*map.entry(k).or_insert(0) += 1) eliminates the two-lookup anti-pattern for frequency counting in one line.
  • BinaryHeap is a max-heap by default. Wrap values in Reverse<T> to get a min-heap for Dijkstra and k-smallest problems.
  • usize underflow is the most common Rust interview bug. Cast to i64 when index arithmetic might go negative.
  • s[i] does not compile in Rust. Use s.as_bytes()[i] for O(1) ASCII access or collect to Vec<char> for random access.
  • Rust pays off only with 50+ problems of real muscle memory. Below that threshold, switching languages hurts more than it helps.

Rust is a legitimate language for coding interviews. Cloudflare, Databricks, Oxide Computer, and a growing list of systems-heavy shops accept it, and some will visibly perk up when you say it. But it will punish you if you walk in expecting Python ergonomics. The borrow checker doesn't care about your 45-minute clock.

This guide covers the standard library data structures you actually need, the idiomatic patterns that save keystrokes, and the four gotchas that will end your interview if you don't know them cold.

Should You Use Rust in Your Coding Interview?

Only if you would reach for Rust naturally for a real project in this domain.

The friction is real. String manipulation that takes two lines in Python can take ten in Rust. Graph traversal with mutable references often fights the borrow checker hard enough that you end up rewriting your approach mid-interview. You will lose. If you have to think about &str vs String while also thinking about the algorithm, you're losing time on both fronts simultaneously.

Rust earns its keep when the company writes Rust and the interviewer knows the language. At a systems shop, showing idiomatic Rust signals more than correct Python ever could. Outside that context, fluency beats features: pick the language where your fingers don't have to think.

If you do use Rust, commit fully. Know the standard library cold. The sections below are that reference.

Me making an end-to-end ML pipeline in Python (chad) vs me reading a file in Rust (rage face)

Two lines in Python. Twenty-five in Rust. The extra twenty-three are not insights.

The Collections You Will Actually Use

Vec

Your default container. push and pop are O(1) amortized. insert(i, v) and remove(i) are O(n). Indexing is bounds-checked in both debug and release builds, so a bad index panics cleanly instead of corrupting memory.

let mut v: Vec<i32> = Vec::new(); v.push(1); v.push(2); v.pop(); // Some(2) v.sort(); // stable sort, O(n log n) v.sort_unstable(); // faster in practice v.reverse(); v.dedup(); // removes consecutive duplicates (sort first) v.retain(|&x| x > 0);

For two-pointer problems, index directly. Rust will block two &mut references into the same vec simultaneously, but v[i] and v[j] as index expressions are fine.

HashMap

The entry API is the most important thing to learn before your interview. It eliminates the two-lookup anti-pattern and handles "initialize if absent" in one line. Without it, you will write three lines of boilerplate for every frequency count.

use std::collections::HashMap; let mut freq: HashMap<char, i32> = HashMap::new(); // Bad: two lookups if freq.contains_key(&c) { *freq.get_mut(&c).unwrap() += 1; } else { freq.insert(c, 1); } // Good: entry API *freq.entry(c).or_insert(0) += 1; // Grouping let mut groups: HashMap<i32, Vec<i32>> = HashMap::new(); groups.entry(key).or_default().push(value); // Lookup freq.get(&c); // Option<&i32> freq.contains_key(&c); // bool freq.remove(&c); // Option<i32>

Iteration order is random. Never rely on it. If you need sorted keys, use BTreeMap.

BTreeMap

Same API as HashMap but keys are always sorted. O(log n) for all operations. Use it when you need range queries or ordered traversal.

use std::collections::BTreeMap; let mut map: BTreeMap<i32, i32> = BTreeMap::new(); map.insert(3, 30); map.insert(1, 10); // Range queries for (k, v) in map.range(1..=2) { /* sorted */ } // Nearest key lookups map.range(..target).next_back(); // largest key < target map.range(target..).next(); // smallest key >= target

Hash map time complexity covers when the O(1) vs O(log n) tradeoff actually matters. For most interview problems, reach for BTreeMap the moment you need sorted order.

BinaryHeap

Rust's BinaryHeap is a max-heap. To get a min-heap, wrap values in std::cmp::Reverse. This surprises almost every Python developer on their first Dijkstra in Rust.

use std::collections::BinaryHeap; use std::cmp::Reverse; let mut max_heap: BinaryHeap<i32> = BinaryHeap::new(); max_heap.push(3); max_heap.push(1); max_heap.peek(); // Some(&3) max_heap.pop(); // Some(3) // Min-heap via Reverse let mut min_heap: BinaryHeap<Reverse<i32>> = BinaryHeap::new(); min_heap.push(Reverse(3)); min_heap.push(Reverse(1)); min_heap.pop(); // Some(Reverse(1)) -- smallest first

For Dijkstra's or "k smallest elements" problems, this is what you want. The heap data structure article explains why max-heap is the default.

VecDeque

BFS queues. push_back and pop_front are both O(1) amortized. Never use Vec::remove(0) for a queue. That's O(n) per dequeue, which means your O(V+E) BFS is quietly O(V^2).

use std::collections::VecDeque; let mut queue: VecDeque<i32> = VecDeque::new(); queue.push_back(1); queue.push_back(2); queue.pop_front(); // Some(1) queue.front(); // peek without removing

HashSet and BTreeSet

HashSet<T> for O(1) membership checks. BTreeSet<T> when you need sorted order or range iteration. Both have insert, contains, and remove.

use std::collections::HashSet; let mut seen: HashSet<i32> = HashSet::new(); seen.insert(1); seen.contains(&1); // true // Set operations let a: HashSet<i32> = [1, 2, 3].into(); let b: HashSet<i32> = [2, 3, 4].into(); let intersection: HashSet<&i32> = a.intersection(&b).collect();

Sorting: The Part That Bites

sort is stable (Timsort). sort_unstable is faster and uses pattern-defeating quicksort. Prefer sort_unstable unless you specifically need stability.

let mut v = vec![3, 1, 4, 1, 5]; v.sort_unstable(); // ascending v.sort_unstable_by(|a, b| b.cmp(a)); // descending v.sort_unstable_by_key(|&x| std::cmp::Reverse(x));

Floats are the trap. f64 does not implement Ord because NaN != NaN. Calling .sort() on a Vec<f64> won't compile. Use total_cmp, stabilized in Rust 1.62:

let mut floats = vec![3.0_f64, 1.0, f64::NAN, 2.0]; floats.sort_by(|a, b| a.total_cmp(b)); // or if you're certain there's no NaN: floats.sort_by(|a, b| a.partial_cmp(b).unwrap());

Reaching for .partial_cmp(b).unwrap() on real data containing NaN panics at runtime, not compile time. The compiler will not save you here. The interviewer will watch it happen.

The Gotchas That Will End Your Interview

usize Will Underflow and Betray You

usize is unsigned. Subtracting below zero panics in debug mode and wraps silently in release. This hits constantly in index arithmetic, binary search, and Dutch National Flag problems. The first time it happens in an interview you will stare at the panic message for thirty seconds before your brain processes what "attempt to subtract with overflow" actually means.

let i: usize = 0; let j = i - 1; // panic: attempt to subtract with overflow // Fix: checked_sub, or cast to i64 when the value might go negative let j = i.checked_sub(1); // Option<usize> let j = (i as i64) - 1; // safe arithmetic, cast back when needed

The single most common Rust interview bug is usize underflow. Cast to i64 early and convert back with as usize once you've confirmed the value is non-negative.

Borrow Checker in Graph Traversal

You cannot hold a reference into a Vec while also pushing to it.

// This won't compile: for neighbor in &graph[node] { graph[node].push(new_node); // cannot borrow `graph` as mutable } // Fix: clone the neighbor list, or use indices let neighbors = graph[node].clone(); for &neighbor in &neighbors { ... }

Prefer index-based traversal over reference-based traversal. Store nodes as integer indices, not as references or pointers. This completely sidesteps the borrow checker.

Me handing code to the Borrow Checker; Borrow Checker staring back with visible disappointment, actual Rust compiler error shown below

The compiler is not being difficult. It is being correct. This does not make it less annoying at minute 38 of a 45-minute interview.

String Handling

String is owned. &str is a borrowed slice. For interview problems, String is almost always what you want.

let s = String::from("hello"); let bytes = s.as_bytes(); // &[u8], fast, ASCII-safe let chars: Vec<char> = s.chars().collect(); // Unicode-safe s.chars().nth(i); // O(i), not O(1) -- collect to Vec<char> if you need random access let result: String = chars.iter().collect(); let result = format!("{}{}", a, b); let result: String = parts.join("");

s[i] does not compile. Use s.as_bytes()[i] for O(1) ASCII access, or collect to Vec<char> once at the top of the function and index freely from there.

The * You Will Definitely Forget Under Pressure

or_insert returns &mut V, not V. Forgetting the dereference produces a cryptic type error. The error message is not helpful. The fix takes three seconds once you know it, and zero seconds to prevent.

// or_insert returns &mut V, not V let count = *freq.entry(c).or_insert(0); // deref to get the value *freq.entry(c).or_insert(0) += 1; // deref to mutate

Patterns That Save Keystrokes

// Two-pointer swap v.swap(left, right); // Max/min of a vec v.iter().max(); // Option<&i32> v.iter().min(); // Enumerate for (i, &val) in v.iter().enumerate() { ... } // Sliding window slices for window in v.windows(k) { /* &[i32] of length k */ } // Prefix sums let prefix: Vec<i64> = std::iter::once(0) .chain(v.iter().scan(0_i64, |acc, &x| { *acc += x as i64; Some(*acc) })) .collect(); // Count distinct v.sort_unstable(); v.dedup(); let distinct = v.len();

When Rust Is Worth It

Rust makes sense when the compiler is helping, not blocking. The ownership model catches bugs you'd introduce in C++ without noticing. The type system forces you to handle None explicitly. The standard library is smaller than Java's but precise.

It is not worth it when your interviewer doesn't know Rust, or when the problem is string-heavy. A "group anagrams" problem that's eight lines in Python is twenty-five in Rust, and the extra lines are boilerplate, not insight. Go ahead and count them sometime. The number is demoralizing. If a recruiter says "pick any language," that means pick your fastest one, not your favorite.

A reasonable threshold: 50+ LeetCode problems solved in Rust with solid muscle memory for the entry API and Reverse wrapper. Below that, one session of voice-based mock practice in your actual interview language will do more for your score than switching to Rust for credibility. SpaceComplexity runs realistic, rubric-based DSA interviews so you can confirm your muscle memory is actually there before the real thing.

Quick Reference

NeedUse
Dynamic arrayVec<T>
Queue (BFS)VecDeque<T>
Hash mapHashMap<K, V> + entry API
Sorted map / range queriesBTreeMap<K, V>
Max-heapBinaryHeap<T>
Min-heapBinaryHeap<Reverse<T>>
Hash setHashSet<T>
Sorted setBTreeSet<T>
Sort ascendingv.sort_unstable()
Sort descendingv.sort_unstable_by(|a, b| b.cmp(a))
Sort floatsv.sort_by(|a, b| a.total_cmp(b))
Frequency map*map.entry(k).or_insert(0) += 1
Safe subtractioni.checked_sub(j) or cast to i64
Char iterations.chars()
Byte iteration (ASCII)s.as_bytes()

Further Reading