Rust for Algorithmic Problem-Solving

Hashing and Frequency Counting

Frequency counting and hashing turn $O(n^2)$ pairwise search and grouping problems—such as finding anagrams, counting subarray sums, or identifying two-sum pairs—into $O(n)$ single-pass algorithms. In Rust, choosing between a fixed-size stack array ([u32; 26] / [u32; 256]) and a HashMap<K, usize> depends on the size of the key alphabet.

Choosing a Frequency Map in Rust:

  Small Bounded Alphabet (`'a'..='z'` or `u8` bytes `0..=255`):
    ---> Fixed Stack Array `[u32; 26]` or `[u32; 256]`
         (0 heap allocations, 0 hash computations, 1-cycle array index!)

  Large / Sparse Domain (`i64` coordinates, `&str` words, `[u32; 26]` signatures):
    ---> `HashMap<K, usize>` with `*map.entry(key).or_insert(0) += 1`

1. Fixed-Array Frequency Signatures for Anagrams and Character Counts

Because fixed-size arrays [u32; 26] implement Eq and Hash in Rust, you can use a 26-element letter frequency array directly as the key in a HashMap<[u32; 26], Vec<&str>>, grouping anagrams in $O(n \cdot L)$ time (without sorting each word in $O(L \log L)$ time!):

use std::collections::HashMap;

/// Computes the 26-letter frequency signature of a lowercase ASCII word in O(L) time.
#[inline]
pub fn letter_signature(word: &str) -> [u32; 26] {
    let mut counts = [0u32; 26];
    for &b in word.as_bytes() {
        if b.is_ascii_lowercase() {
            counts[(b - b'a') as usize] += 1;
        }
    }
    counts
}

/// Groups words that are anagrams of each other in O(N * L) time using `[u32; 26]` keys!
pub fn group_anagrams<'a>(words: &[&'a str]) -> Vec<Vec<&'a str>> {
    let mut groups: HashMap<[u32; 26], Vec<&'a str>> = HashMap::new();
    for &word in words {
        groups
            .entry(letter_signature(word))
            .or_default()
            .push(word);
    }

    let mut result: Vec<Vec<&'a str>> = groups.into_values().collect();
    result.sort_unstable_by_key(|g| g[0]);
    result
}

fn main() {
    let input = ["eat", "tea", "tan", "ate", "nat", "bat"];
    let grouped = group_anagrams(&input);

    assert_eq!(
        grouped,
        vec![vec!["bat"], vec!["eat", "tea", "ate"], vec!["tan", "nat"]]
    );
    println!("Grouped anagrams: {grouped:?}");
}
Grouped anagrams: [["bat"], ["eat", "tea", "ate"], ["tan", "nat"]]

2. Single-Pass Complementary Lookup (Two-Sum / Subarray Sum Equals $K$)

Instead of populating a HashMap in one pass and querying it in a second pass, many pair-matching and prefix-sum problems can query the map before inserting the current element in a single pass:

use std::collections::HashMap;

/// Finds indices `(i, j)` with `i < j` such that `nums[i] + nums[j] == target` in O(n) time.
pub fn two_sum(nums: &[i64], target: i64) -> Option<(usize, usize)> {
    let mut seen: HashMap<i64, usize> = HashMap::with_capacity(nums.len());
    for (i, &val) in nums.iter().enumerate() {
        let complement = target - val;
        if let Some(&prev_idx) = seen.get(&complement) {
            return Some((prev_idx, i));
        }
        seen.insert(val, i);
    }
    None
}

/// Counts contiguous subarrays whose elements sum to `target` in O(n) time.
pub fn subarray_sum_count(nums: &[i64], target: i64) -> u64 {
    let mut prefix_counts: HashMap<i64, u64> = HashMap::with_capacity(nums.len() + 1);
    prefix_counts.insert(0, 1); // Empty prefix has sum 0

    let mut running_sum = 0i64;
    let mut total_matches = 0u64;

    for &x in nums {
        running_sum += x;
        if let Some(&count) = prefix_counts.get(&(running_sum - target)) {
            total_matches += count;
        }
        *prefix_counts.entry(running_sum).or_insert(0) += 1;
    }

    total_matches
}

fn main() {
    assert_eq!(two_sum(&[2, 7, 11, 15], 9), Some((0, 1)));
    assert_eq!(subarray_sum_count(&[1, 2, 3, -3, 3], 3), 5);
    println!(
        "Two-sum indices: {:?}, subarrays summing to 3: {}",
        two_sum(&[2, 7, 11, 15], 9),
        subarray_sum_count(&[1, 2, 3, -3, 3], 3)
    );
}
Two-sum indices: Some((0, 1)), subarrays summing to 3: 5