Rust for Algorithmic Problem-Solving

Big-O Analysis in Rust

Big-O analysis characterizes how an algorithm's execution time and memory consumption scale as the input size $N$ grows. In Rust, writing algorithms that meet strict latency budgets or 1.0-second competitive programming time limits requires knowing both the input-size-to-complexity budget and the hidden asymptotic costs of standard library methods.

Rule of Thumb for a 1.0-Second Time Budget in Release Rust (~10^8 Simple Ops/Sec):

  Input Size `N`          Maximum Feasible Time Complexity      Typical Algorithms
  ----------------------------------------------------------------------------------------------
  N <= 11                 O(N!)                                 Permutations / Backtracking
  N <= 20                 O(2^N) or O(N * 2^N)                  Bitmask DP / Meet-in-the-Middle
  N <= 500                O(N^3)                                Floyd-Warshall / Matrix Mul
  N <= 5,000              O(N^2)                                Quadratic DP / All-Pairs
  N <= 200,000            O(N log N) or O(N log^2 N)            Sorting, Dijkstra, Segment Tree
  N <= 5,000,000          O(N)                                  Sliding Window, Hashing, BFS/DFS
  N >= 10^9               O(log N) or O(sqrt(N)) or O(1)        Binary Search, Math, Matrix Exp

Hidden $O(n)$ Traps in Standard Rust Methods (That Turn $O(n)$ Loops into $O(n^2)$!)

One of the most common causes of unexpected slowdowns in Rust is calling an innocent-looking $O(n)$ method inside an $O(n)$ loop:

Hidden $O(n)$ Trap Inside a Loop Actual Complexity in $N$-Step Loop Fast $O(1)$ / $O(n)$ Total Replacement
s.chars().nth(i) $O(n^2)$! (Walks UTF-8 from byte 0 every call) Index s.as_bytes()[i] ($O(1)$) or collect Vec<char> once upfront!
vec.remove(0) / vec.insert(0, x) $O(n^2)$! (Shifts all $n$ elements on every call) Use VecDeque::pop_front() / push_front() ($O(1)$) or vec.swap_remove(i) ($O(1)$)!
vec.contains(&x) $O(n^2)$! (Linear scan on every check) Use HashSet::contains(&x) ($O(1)$) or slice.binary_search(&x) ($O(\log n)$)!
acc = format!("{acc}{piece}") $O(n^2)$! (Allocates & copies entire prefix every step) Use acc.push_str(piece) ($O(1)$ amortized) or write!(&mut acc, ...)!
Passing vec.clone() into recursive calls $O(n \cdot \text{calls})$! (Deep-copies entire vector at every node) Pass a shared slice &[T] (or &mut Vec<T> with push + pop backtracking)!

Demonstrating $O(n)$ In-Place Backtracking vs $O(n \cdot 2^n)$ Cloning

When recursion explores subsets or paths, mutating a single shared &mut Vec<T> with path.push(x) before recursing and path.pop() after returning avoids allocating a new Vec at every node of the recursion tree:

/// Generates all subsets of `nums` whose sum equals `target`, mutating a single
/// `current_path` buffer in-place (`push` + `pop`) so zero heap allocations occur on dead-end branches!
pub fn find_subsets_summing_to(nums: &[i64], target: i64) -> Vec<Vec<i64>> {
    let mut results = Vec::new();
    let mut current_path = Vec::with_capacity(nums.len());

    fn backtrack(
        idx: usize,
        running_sum: i64,
        nums: &[i64],
        target: i64,
        path: &mut Vec<i64>,
        results: &mut Vec<Vec<i64>>,
    ) {
        if idx == nums.len() {
            if running_sum == target {
                results.push(path.clone()); // Only clone when a valid answer is found!
            }
            return;
        }

        // Branch 1: Exclude `nums[idx]`
        backtrack(idx + 1, running_sum, nums, target, path, results);

        // Branch 2: Include `nums[idx]` using in-place push + pop
        path.push(nums[idx]);
        backtrack(
            idx + 1,
            running_sum + nums[idx],
            nums,
            target,
            path,
            results,
        );
        path.pop(); // Restore `path` in O(1) time!
    }

    backtrack(0, 0, nums, target, &mut current_path, &mut results);
    results
}

fn main() {
    let subsets = find_subsets_summing_to(&[2, 4, 6, 10], 10);
    assert_eq!(subsets, vec![vec![10], vec![4, 6]]);
    println!("Subsets summing to 10: {subsets:?}");
}
Subsets summing to 10: [[10], [4, 6]]