Rust Guide
Rust Guide: From Basics to LeetCode-Ready
A practical reference for writing LeetCode and interview solutions in Rust. Pick Rust in the LeetCode language dropdown when submitting. Submissions are an impl Solution block: the judge already provides struct Solution, and you fill in the method.
New to LeetCode? Start with the Beginner’s Guide for the platform, difficulty levels, and which problems to solve first.
The language facts below follow the Rust Book, std::collections, and the Rust 2024 edition (stable since Rust 1.85, February 2025). Let chains need Rust 1.88 or newer and edition = "2024".
Why Rust for Algorithms
| Strength | What it means on LeetCode |
|---|---|
| Ownership | Each value has one owner. The compiler rejects use-after-move and dangling references before the code runs. |
| Borrowing | &T and &mut T share data without copying it. Large vectors stay in place. |
Vec and HashMap |
The collections guide says these two cover most storage. Sorted maps, deques, and heaps are there when you need them. |
| Iterators | map, filter, fold, and windows compile down to tight loops. |
| No hidden GC pause | drop runs when a value goes out of scope, so allocation cost stays visible. |
The cost is the borrow checker. Most early compile errors are about who owns a value and who is allowed to mutate it. Parts 1 and 5 are built around that.
Part 1: Language Essentials
Hello World and the Compiler
fn main() {
println!("Hello, World!");
}
Run a single file with rustc, or a Cargo project when you want tests:
rustc solution.rs -o solution && ./solution
cargo new practice --bin
cargo run
cargo test
| Command | Purpose |
|---|---|
rustc solution.rs |
Compile one file. LeetCode compiles your impl Solution itself. |
cargo run |
Build and run a binary crate |
cargo test |
Run #[test] functions |
rustfmt solution.rs |
Format to the standard style |
cargo clippy |
Extra lints |
On your machine, set the edition in Cargo.toml:
[package]
edition = "2024"
Types You Will Actually Use
let x: i32 = 42; // LeetCode's default integer
let y: i64 = 1_000_000_000_000; // products, prefix sums, MOD math
let n: usize = 10; // lengths and indices
let pi: f64 = 3.14159;
let c: char = 'A'; // one Unicode scalar value
let flag: bool = true;
let s: String = String::from("hello"); // owned, growable, UTF-8
let t: &str = "hello"; // borrowed string slice
| Type | Width | Range you should remember |
|---|---|---|
i32 |
32-bit | $-2^{31}$ … $2^{31}-1$ (i32::MIN … i32::MAX) |
i64 |
64-bit | about $\pm 9.2 \times 10^{18}$ |
u32 |
32-bit | $0$ … $4\,294\,967\,295$ |
usize |
pointer-sized | indices into Vec and slices. 64-bit on typical judges. |
f64 |
64-bit | binary search on answers, geometry |
char |
4 bytes | one Unicode scalar, so 'é' is one char |
String / &str |
UTF-8 bytes | s.len() is a byte count |
Overflow. In debug builds, i32 overflow panics. In release builds it wraps. LeetCode compiles in release mode, so a wrapping product can pass locally in --release and still be the wrong answer. Be explicit:
let area = (width as i64) * (height as i64);
let safe = a.checked_add(b); // Option<i32>
let wrapped = a.wrapping_add(b); // two's complement, every build
let clamped = a.saturating_add(b); // sticks at MIN / MAX
i32::midpoint(a, b) (stable since 1.87) computes the average rounded toward zero without overflowing. Use it for binary-search midpoints when both ends are signed.
Control Flow
if x > 0 {
// positive
} else if x < 0 {
// negative
} else {
// zero
}
for i in 0..n {
// i is usize: 0, 1, ..., n - 1
}
for i in 0..=n {
// inclusive end
}
let mut i = 0;
while i < n {
i += 1;
}
loop {
if done {
break;
}
}
0..n is exclusive. 0..=n is inclusive. A reverse loop is (0..n).rev().
match is the way to take Option and Result apart:
match map.get(&key) {
Some(value) => *value,
None => 0,
}
if let covers the one pattern you care about:
if let Some(&j) = seen.get(&need) {
return vec![j, i as i32];
}
Functions, Moves, and Copies
fn add(a: i32, b: i32) -> i32 {
a + b // last expression is the return value; no semicolon
}
fn sum_of(nums: &[i32]) -> i32 {
nums.iter().sum()
}
fn scale(x: &mut i32) {
*x *= 2;
}
Integers, bool, char, and shared references implement Copy: assignment duplicates the bits, and the original stays usable. String, Vec<T>, and HashMap<K, V> do not. Assignment moves them:
let s1 = String::from("hello");
let s2 = s1; // s1 is moved
// println!("{s1}"); // compile error: s1 was moved
let n1 = 5_i32;
let n2 = n1; // Copy: n1 is still 5
When a non-Copy value goes out of scope, Rust calls drop and frees its heap buffer. You do not write free.
Clone when you truly need a second owned value: let s3 = s2.clone();. On a hot LeetCode path, prefer a borrow.
References and the Borrow Checker
A reference is an address the compiler proves is valid for its whole lifetime.
fn length(s: &str) -> usize {
s.len()
}
fn push_bang(s: &mut String) {
s.push('!');
}
fn main() {
let mut name = String::from("rust");
let n = length(&name); // shared borrow
push_bang(&mut name); // exclusive borrow, after n is done
println!("{n} {name}");
}
Rules that show up in every solution:
- You may have many shared borrows (
&T), or one exclusive borrow (&mut T). - A borrow must not outlive the value it points at.
- Holding
&nums[0]and then callingnums.push(...)is rejected:pushmay reallocate and invalidate the reference.
Pass large inputs as slices (&[i32], &mut [i32]) in your own helpers. LeetCode signatures usually take ownership (Vec<i32>, String); move those into the helper or reborrow with nums.as_slice() / &nums.
Option Instead of Null
Rust has no null. Absence is Option<T>: Some(value) or None.
let first: Option<&i32> = nums.get(0); // index may be out of range
let value = first.copied().unwrap_or(0);
nums[i] panics when i is out of range. nums.get(i) returns Option<&i32>. In a solution you have already proved the index, indexing is fine and clearer.
Part 2: std Collections
The standard library’s own advice: start with Vec or HashMap. Reach for the others when their extra operation is the one you need. Import them explicitly; only Vec is in the prelude.
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque};
use std::cmp::Reverse;
LeetCode accepts the standard library. Third-party crates such as a faster hasher are not available in the submission box.
Which Collection?
| You need | Use |
|---|---|
| Indexed sequence, stack, sort, binary search | Vec<T> |
| Queue or deque (push/pop both ends) | VecDeque<T> |
| Expected $O(1)$ lookup by key | HashMap<K, V> / HashSet<T> |
| Keys in sorted order, successor, or a key range | BTreeMap<K, V> / BTreeSet<T> |
| Repeated min or max extraction | BinaryHeap<T> (max-heap; wrap in Reverse for a min-heap) |
| A linked list | Almost never. Vec and VecDeque are the interview answer. |
Complexity (from the std::collections performance table)
| Operation | Vec |
VecDeque |
HashMap |
BTreeMap |
BinaryHeap |
|---|---|---|---|---|---|
| Index / get by key | $O(1)$ | $O(1)$ | $O(1)$ average | $O(\log n)$ | peek $O(1)$ |
| Push back / insert | amortized $O(1)$ | amortized $O(1)$ at ends | $O(1)$ average | $O(\log n)$ | $O(\log n)$ |
| Remove at index / key | $O(n)$ | $O(\min(i, n-i))$ | $O(1)$ average | $O(\log n)$ | pop $O(\log n)$ |
| Sorted range | sort, then slice | — | — | range $O(\log n)$ to start |
— |
HashMap is average-case. The default hasher is SipHash-1-3, chosen to resist hash-flooding attacks. It is correct for interviews. For tiny integer keys it is slower than a raw open-addressed table, and that is the tradeoff you accept inside std.
Vec — the Default Sequence
let mut nums: Vec<i32> = Vec::new();
nums.push(1);
nums.push(2);
let last = nums.pop(); // Option<i32>
let n = nums.len();
let first = nums[0]; // panics if empty
let ready = vec![0; n]; // n zeros
let known = vec![1, 2, 3];
nums.reserve(n); // one allocation when you know the size
| Method | Effect |
|---|---|
push / pop |
stack |
len / is_empty |
size |
sort / sort_unstable |
ascending. sort_unstable is faster when equal elements may reorder |
binary_search |
on a sorted vec. Ok(index) or Err(insertion_point) |
windows(k) |
overlapping slices of length k |
reverse / swap(i, j) |
in place |
iter / iter_mut / into_iter |
shared, mutable, or consuming |
for (i, &x) in nums.iter().enumerate() {
println!("{i} {x}");
}
for x in &mut nums {
*x += 1;
}
enumerate yields usize. LeetCode often wants i as i32 in the returned vector.
String and &str
String owns its UTF-8 bytes. &str borrows them. Indexing with s[i] does not compile, because a byte offset is not always a character boundary.
let mut s = String::from("rust");
s.push('!');
s.push_str("ace");
let bytes = s.len(); // byte length
let chars = s.chars().count(); // scalar count, O(n)
let b = s.as_bytes(); // &[u8], O(1) random access for ASCII
let head = &s[..4]; // byte range; must be on a char boundary
assert!(s.starts_with("rust"));
assert!(s.ends_with("ace"));
LeetCode strings are usually ASCII, so s.as_bytes()[i] is the $O(1)$ read. Use chars() when the problem talks about Unicode characters. Build an answer with String::with_capacity(n) and push.
split_whitespace, split, and lines return iterators of &str.
HashMap and HashSet
let mut seen: HashMap<i32, i32> = HashMap::new();
seen.insert(nums[0], 0);
if let Some(&j) = seen.get(&key) {
// j is the stored index
}
seen.contains_key(&key);
seen.remove(&key);
*seen.entry(key).or_insert(0) += 1; // frequency count
entry returns a mutable slot: insert the default when the key is missing, then update it. That is the frequency-map and memoization idiom.
get returns Option<&V>. seen[&key] panics when the key is absent, so keep it for keys you just inserted.
let mut set = HashSet::new();
set.insert(42);
set.contains(&42);
Keys must be Eq + Hash. i32, i64, String, &str, and tuples of those all qualify. A Vec<i32> key works when you own it; a borrowed slice key needs the stored key to match.
Iteration order of a HashMap is arbitrary. Sort the keys when the problem wants a stable order.
BTreeMap and BTreeSet
Use these when the algorithm needs order: predecessor, successor, or every key inside an interval.
let mut book: BTreeMap<i32, i32> = BTreeMap::new();
book.insert(10, 1);
book.insert(30, 2);
let floor = book.range(..=15).next_back(); // greatest key <= 15
let ceil = book.range(15..).next(); // least key >= 15
for (&k, &v) in book.range(10..40) { // 10 <= key < 40
println!("{k} {v}");
}
BTreeSet<i32> has the same ordered queries via range. First and last keys are iter().next() and iter().next_back().
BinaryHeap
BinaryHeap is a max-heap: pop returns the greatest item.
let mut max_heap: BinaryHeap<i32> = BinaryHeap::new();
max_heap.push(3);
max_heap.push(1);
assert_eq!(max_heap.peek(), Some(&3));
assert_eq!(max_heap.pop(), Some(3));
let mut min_heap: BinaryHeap<Reverse<i32>> = BinaryHeap::new();
min_heap.push(Reverse(3));
min_heap.push(Reverse(1));
assert_eq!(min_heap.pop(), Some(Reverse(1)));
Reverse comes from std::cmp. For a Dijkstra heap of (dist, node), store Reverse((dist, node)) so the smallest distance comes out first. dist should be a type whose order matches what you want; i64 is the usual choice.
VecDeque — Queues
let mut q = VecDeque::new();
q.push_back(start);
while let Some(u) = q.pop_front() {
q.push_back(u + 1);
}
push_back + pop_front is a queue. push_back + pop_back is a stack; Vec is the simpler stack.
Part 3: Patterns You’ll Use Every Day
Sorting
nums.sort(); // ascending, stable
nums.sort_unstable(); // faster when ties may reorder
nums.sort_by(|a, b| b.cmp(a)); // descending
nums.sort_by_key(|&x| x.abs());
let mut pairs = vec![(1, 5), (1, 2), (0, 9)];
pairs.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
sort_by_key(|&x| -x) overflows on i32::MIN. Prefer sort_by(|a, b| b.cmp(a)).
Closures borrow their environment. |x: &i32| *x + 1 is a closure. Add move when it must own captured values: move |x| owned + x.
Binary Search
binary_search requires a sorted slice and returns Result<usize, usize>.
match nums.binary_search(&target) {
Ok(i) => i, // found
Err(i) => i, // first index where target could be inserted
}
Lower bound (“first index with value ≥ target”) on a sorted slice:
let i = nums.partition_point(|&x| x < target);
Upper bound (“first index with value > target”):
let i = nums.partition_point(|&x| x <= target);
Search on the answer (minimum feasible speed, capacity, day):
let mut lo: i64 = 1;
let mut hi: i64 = 1_000_000_000;
while lo < hi {
let mid = lo.midpoint(hi); // no overflow
if feasible(mid) {
hi = mid;
} else {
lo = mid + 1;
}
}
Iterators
let sum: i32 = nums.iter().sum();
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();
let positives: Vec<i32> = nums.into_iter().filter(|x| *x > 0).collect();
let best = nums.iter().copied().max(); // Option<i32>
let any = nums.iter().any(|x| *x < 0);
let pos = nums.iter().position(|&x| x == target); // Option<usize>
let total = nums.iter().fold(0_i64, |acc, &x| acc + x as i64);
| Adapter | Yields |
|---|---|
iter() |
&T, collection stays |
iter_mut() |
&mut T |
into_iter() |
owned T, collection is consumed |
enumerate |
(usize, item) |
zip |
pairs until the shorter iterator ends |
take(k) / skip(k) |
a prefix or the tail |
collect |
build a Vec, HashSet, String, … |
windows and chunks are the slice forms of a sliding window and a blocked scan:
for w in nums.windows(3) {
// w: &[i32] of length 3
}
Counting, Prefix Sums, and Differences
let mut freq: HashMap<i32, i32> = HashMap::new();
for &x in &nums {
*freq.entry(x).or_insert(0) += 1;
}
let mut prefix = vec![0_i64; nums.len() + 1];
for (i, &x) in nums.iter().enumerate() {
prefix[i + 1] = prefix[i] + x as i64;
}
let range_sum = prefix[right + 1] - prefix[left];
Cast to i64 before the multiply or the running sum when $n \cdot \max |
a_i | $ can exceed $2^{31}-1$. |
Graph Adjacency and DFS
let mut adj = vec![Vec::<usize>::new(); n];
adj[u].push(v);
fn dfs(u: usize, adj: &[Vec<usize>], seen: &mut [bool]) {
if seen[u] {
return;
}
seen[u] = true;
for &v in &adj[u] {
dfs(v, adj, seen);
}
}
The slice arguments are shared (adj) and exclusive (seen). That split is what lets the recursive call compile: seen is reborrowed for the call and returned when it ends.
BFS uses the VecDeque loop from Part 2, with the same seen array marked on push so a node is enqueued once.
Two Pointers
let mut i = 0;
let mut j = nums.len() - 1;
while i < j {
let sum = nums[i] + nums[j];
if sum == target {
break;
} else if sum < target {
i += 1;
} else {
j -= 1;
}
}
nums.len() - 1 underflows when the vector is empty. Guard with if nums.len() < 2.
Part 4: LeetCode Solution Template
The judge defines struct Solution. You submit the impl and any helpers. A main function is for local runs only.
use std::collections::HashMap;
impl Solution {
pub fn two_sum(nums: Vec<i32>, target: i32) -> Vec<i32> {
let mut seen = HashMap::new();
for (i, &num) in nums.iter().enumerate() {
let need = target - num;
if let Some(&j) = seen.get(&need) {
return vec![j, i as i32];
}
seen.insert(num, i as i32);
}
vec![]
}
}
Local harness:
struct Solution;
fn main() {
let ans = Solution::two_sum(vec![2, 7, 11, 15], 9);
assert_eq!(ans, vec![0, 1]);
}
Signature Habits
| LeetCode type | Rust |
|---|---|
int |
i32 |
long |
i64 |
int[] |
Vec<i32> |
string |
String on input and output; take &str in helpers |
ListNode* |
Option<Box<ListNode>> |
TreeNode* |
Option<Rc<RefCell<TreeNode>>> |
| boolean | bool |
index you computed with enumerate |
i as i32 |
Linked list and tree stubs from the judge already declare the node types. Use those names; do not invent a second ListNode.
// List helpers, once the judge's ListNode is in scope.
fn push_front(head: Option<Box<ListNode>>, val: i32) -> Option<Box<ListNode>> {
Some(Box::new(ListNode { val, next: head }))
}
Trees share a node across parent links, so the judge uses Rc<RefCell<TreeNode>>: Rc for shared ownership, RefCell for interior mutability checked at runtime.
use std::cell::RefCell;
use std::rc::Rc;
fn dfs(node: &Option<Rc<RefCell<TreeNode>>>) -> i32 {
let Some(n) = node else {
return 0;
};
let n = n.borrow();
n.val + dfs(&n.left) + dfs(&n.right)
}
borrow() and borrow_mut() panic if you already hold the other kind of borrow on that RefCell. Finish the borrow (end the scope) before you borrow the same node again.
Patterns Mapped to Rust
| Pattern | Shape |
|---|---|
| Frequency / index map | HashMap + entry().or_insert |
| Sliding window | two indices, or windows(k) when the width is fixed |
| Monotonic stack | Vec<i32> with push / pop |
| Heap top-k | BinaryHeap<Reverse<T>> of size k |
| Ordered multiset | BTreeMap<i32, i32> of value → count |
| Union-find | parent: Vec<usize>, find with path compression |
| DP row | let mut dp = vec![0_i64; n + 1]; |
| Backtracking | recursive fn taking &mut Vec<i32> and &mut Vec<Vec<i32>> |
fn subsets(start: usize, nums: &[i32], path: &mut Vec<i32>, out: &mut Vec<Vec<i32>>) {
out.push(path.clone());
for i in start..nums.len() {
path.push(nums[i]);
subsets(i + 1, nums, path, out);
path.pop();
}
}
Part 5: Learning Path
Stage 1 — Syntax (about a week)
Read chapters 1–3 and 6 of the Book. Write small fn main programs that use i32, i64, for, while, and match.
Check that you can:
- Explain
0..nversus0..=n - Return a value without
returnby leaving the semicolon off - Call
i32::MAX,checked_add, and a castas i64
Stage 2 — Ownership and Borrowing (one to two weeks)
Read chapter 4. This is the stage that makes later solutions compile.
Check that you can:
- Say which of
i32andStringisCopy - Write a function that takes
&[i32]and one that takes&mut Vec<i32> - Read a borrow-checker error and shorten the borrow (end the scope, clone, or index instead of holding a reference)
Stage 3 — Collections (one to two weeks)
Read chapter 8. Solve array, string, and hash-map problems: Two Sum, Group Anagrams, Valid Anagram, Contains Duplicate.
Check that you can:
- Build a frequency map with
entry - Decide
HashMapversusBTreeMapfrom the table in Part 2 - Walk a
Stringwithas_bytesfor ASCII andcharsfor Unicode
Stage 4 — Iterators, Sort, and Search (ongoing)
Solve binary search, sliding window, and sorting problems. Keep iterator docs open.
Check that you can:
- Choose
iter,iter_mut, orinto_iteron purpose - Write a lower bound with
partition_point - Implement a min-heap with
BinaryHeap<Reverse<_>>
Stage 5 — Algorithm Templates (ongoing)
Use the templates index and implement each pattern once in Rust: BFS, DFS, union-find, prefix sums, monotonic stack, interval sweep, and top-k. The Rust shapes are in Part 4.
Part 6: Modern Rust Worth Knowing
Rust 2024 is the current edition, stabilized in Rust 1.85.0 (20 February 2025). Editions are opt-in: a 2024 crate still links with older crates. New code should set edition = "2024".
Let Chains (Rust 1.88, edition 2024)
Stable since 26 June 2025, and only in edition 2024. if and while conditions can mix let patterns and booleans with &&. Bindings from an earlier pattern are visible later in the chain.
let release = Some((1, 88));
if let Some((major, minor)) = release && major == 1 && minor >= 88 {
println!("let chains are available");
}
On edition 2021 the same code is a compile error. Nested if let still works everywhere.
let … else
let Some(node) = head else {
return None;
};
When the pattern fails, the else block must diverge (return, break, continue, or panic).
Matches That Stay Short
let digit = matches!(c, '0'..='9');
let value = option.unwrap_or(0);
let value = option.unwrap_or_else(|| expensive());
Overflow-Safe Midpoint
let mid = lo.midpoint(hi); // signed: stable since 1.87; unsigned: since 1.85
This is (lo + hi) / 2 rounded toward zero, computed so the addition cannot overflow. Binary search on i64 bounds should use it.
What to Leave Alone on LeetCode
async, threads, and channels. Solutions are single-threaded and synchronous.unsafe. Safestdis enough, andunsafedrops the guarantees you are practicing.- Nightly-only syntax (
genblocks, the!type as a fully stable alias in older toolchains). If it needs#![feature(...)], the judge will reject it. - External crates.
stdis the whole toolbox inside the editor.
dbg!(expr) prints the file, line, and value to stderr and returns the value. Strip those calls before you submit if the problem is strict about stdout.
Quick Reference Card
Collection Costs
| Operation | Vec |
HashMap |
BTreeMap |
BinaryHeap |
|---|---|---|---|---|
| Read | $O(1)$ index | $O(1)$ avg get |
$O(\log n)$ | $O(1)$ peek |
| Insert | amortized $O(1)$ push |
$O(1)$ avg | $O(\log n)$ | $O(\log n)$ push |
| Delete | $O(n)$ in the middle, $O(1)$ pop |
$O(1)$ avg | $O(\log n)$ | $O(\log n)$ pop |
| Order | after sort |
arbitrary | sorted keys | max (or min via Reverse) |
Integer Limits
| Constant | Value |
|---|---|
i32::MIN / i32::MAX |
$-2\,147\,483\,648$ / $2\,147\,483\,647$ |
i64::MIN / i64::MAX |
$-9\,223\,372\,036\,854\,775\,808$ / $9\,223\,372\,036\,854\,775\,807$ |
u32::MAX |
$4\,294\,967\,295$ |
const MOD: i64 = 1_000_000_007;
let sum = (a + b) % MOD;
Snippets
use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap, VecDeque};
let mut freq = HashMap::new();
*freq.entry(x).or_insert(0) += 1;
let mut heap = BinaryHeap::new();
heap.push(Reverse(dist));
let mut q = VecDeque::new();
q.push_back(0);
let i = nums.partition_point(|&x| x < target);
let area = (w as i64) * (h as i64);
Resources
- The Rust Programming Language — ownership,
Vec,String, andHashMapin the official book stddocumentation — method-level referencestd::collections— which collection to pick, plus the complexity table- Rust by Example — short runnable snippets
- Rust 2024 edition guide — edition changes, including the temporary-scope rules let chains rely on
- Rust 1.85 announcement — Rust 2024 becomes stable
- Rust 1.88 announcement — let chains
- Rustlings — small exercises for ownership and iterators
- LeetCode templates on this blog — algorithm patterns
- LeetCode Beginner’s Guide — the platform itself