๐Ÿฆ€ Functional Rust

264: String Anagram Check

Difficulty: 1 Level: Beginner Two approaches to check if strings are anagrams โ€” sort-and-compare (O(n log n)) and frequency counting (O(n)).

The Problem This Solves

You're building a word game, a spell checker, or a search feature that suggests rearrangements. You need to determine whether two words use the exact same letters in different orders: "listen" and "silent" are anagrams; "listen" and "enlist" are too. "listen" and "listens" are not. Without a built-in anagram check, you'd manually count character occurrences, handle case normalization, and compare frequency maps โ€” all boilerplate that distracts from your actual problem. For filtering a list of candidates (the common real-world use case), you'd loop over each candidate and repeat the comparison logic. The sort-based approach mirrors functional programming idioms: transform both strings the same way, compare results. The frequency-count approach is more efficient for large strings. Knowing both gives you the right tool for each situation.

The Intuition

Two strings are anagrams if sorting their characters produces the same sequence โ€” or equivalently, if they have identical character frequency maps.

How It Works in Rust

// Sort-based: O(n log n) โ€” mirrors OCaml approach
pub fn is_anagram_sort(s1: &str, s2: &str) -> bool {
 let sorted_chars = |s: &str| -> Vec<char> {
     let mut chars: Vec<char> = s.to_lowercase().chars().collect();
     chars.sort_unstable();  // in-place sort, faster than sort()
     chars
 };
 // Same word is not an anagram of itself
 s1.to_lowercase() != s2.to_lowercase() && sorted_chars(s1) == sorted_chars(s2)
}

// Frequency-based: O(n) โ€” no sorting, HashMap counts each char
pub fn is_anagram_freq(s1: &str, s2: &str) -> bool {
 use std::collections::HashMap;
 let freq = |s: &str| -> HashMap<char, i32> {
     let mut map = HashMap::new();
     for c in s.to_lowercase().chars() {
         *map.entry(c).or_insert(0) += 1;
     }
     map
 };
 s1.to_lowercase() != s2.to_lowercase() && freq(s1) == freq(s2)
}

// Filter candidates: the classic use case
pub fn find_anagrams<'a>(word: &str, candidates: &[&'a str]) -> Vec<&'a str> {
 candidates.iter().copied()
     .filter(|c| is_anagram_sort(word, c))
     .collect()
}
// 'a lifetime: candidates outlive the returned Vec โ€” borrows, not copies

What This Unlocks

Key Differences

ConceptOCamlRust
String to chars`String.to_seq \> List.of_seq``.chars().collect::<Vec<char>>()`
Sort`List.sort Char.compare` (new list)`.sort_unstable()` (in-place, faster)
Frequency mapManual fold with `Map``HashMap` with `.entry().or_insert(0)`
Borrowing candidatesN/A (GC manages)`'a` lifetime ties return to input slice
Pipeline`\>` operatorMethod chaining `.filter().collect()`
/// String Anagram Check โ€” Sorting and Frequency Counting Approaches

/// Sort-based: mirrors OCaml's approach.
pub fn is_anagram_sort(s1: &str, s2: &str) -> bool {
    let normalize = |s: &str| -> String { s.to_lowercase() };
    let sorted_chars = |s: &str| -> Vec<char> {
        let mut chars: Vec<char> = s.to_lowercase().chars().collect();
        chars.sort_unstable();
        chars
    };
    normalize(s1) != normalize(s2) && sorted_chars(s1) == sorted_chars(s2)
}

/// Frequency-based: O(n) using HashMap.
pub fn is_anagram_freq(s1: &str, s2: &str) -> bool {
    use std::collections::HashMap;
    let lower1 = s1.to_lowercase();
    let lower2 = s2.to_lowercase();
    if lower1 == lower2 { return false; }
    let freq = |s: &str| -> HashMap<char, i32> {
        let mut map = HashMap::new();
        for c in s.chars() { *map.entry(c).or_insert(0) += 1; }
        map
    };
    freq(&lower1) == freq(&lower2)
}

pub fn find_anagrams<'a>(word: &str, candidates: &[&'a str]) -> Vec<&'a str> {
    candidates.iter().copied().filter(|c| is_anagram_sort(word, c)).collect()
}

fn main() {
    let results = find_anagrams("listen", &["enlists", "google", "inlets", "silent"]);
    println!("Anagrams of 'listen': {:?}", results);
    println!("is_anagram('listen', 'silent') = {}", is_anagram_sort("listen", "silent"));
    println!("is_anagram('listen', 'listen') = {}", is_anagram_sort("listen", "listen"));
}

/* Output:
   Anagrams of 'listen': ["inlets", "silent"]
   is_anagram('listen', 'silent') = true
   is_anagram('listen', 'listen') = false
*/
let to_sorted_chars s =
  s |> String.lowercase_ascii
    |> String.to_seq |> List.of_seq
    |> List.sort Char.compare

let is_anagram s1 s2 =
  let s1' = String.lowercase_ascii s1 in
  let s2' = String.lowercase_ascii s2 in
  s1' <> s2' && to_sorted_chars s1 = to_sorted_chars s2

let find_anagrams word candidates =
  List.filter (is_anagram word) candidates

let () =
  assert (is_anagram "listen" "silent" = true);
  assert (is_anagram "hello" "world" = false);
  assert (is_anagram "listen" "listen" = false);
  let results = find_anagrams "listen" ["enlists";"google";"inlets";"silent"] in
  assert (results = ["inlets";"silent"]);
  print_endline "ok"

๐Ÿ“Š Detailed Comparison

OCaml vs Rust: String Anagram Check

Side-by-Side Code

OCaml

๐Ÿช Show OCaml equivalent
let to_sorted_chars s =
s |> String.lowercase_ascii
 |> String.to_seq |> List.of_seq
 |> List.sort Char.compare

let is_anagram s1 s2 =
let s1' = String.lowercase_ascii s1 in
let s2' = String.lowercase_ascii s2 in
s1' <> s2' && to_sorted_chars s1 = to_sorted_chars s2

let find_anagrams word candidates =
List.filter (is_anagram word) candidates

Rust (idiomatic โ€” sort-based)

pub fn is_anagram_sort(s1: &str, s2: &str) -> bool {
 let normalize = |s: &str| -> String { s.to_lowercase() };
 let sorted_chars = |s: &str| -> Vec<char> {
     let mut chars: Vec<char> = s.to_lowercase().chars().collect();
     chars.sort_unstable();
     chars
 };
 normalize(s1) != normalize(s2) && sorted_chars(s1) == sorted_chars(s2)
}

pub fn find_anagrams<'a>(word: &str, candidates: &[&'a str]) -> Vec<&'a str> {
 candidates.iter().copied().filter(|c| is_anagram_sort(word, c)).collect()
}

Rust (functional โ€” frequency counting)

pub fn is_anagram_freq(s1: &str, s2: &str) -> bool {
 use std::collections::HashMap;
 let lower1 = s1.to_lowercase();
 let lower2 = s2.to_lowercase();
 if lower1 == lower2 { return false; }
 let freq = |s: &str| -> HashMap<char, i32> {
     let mut map = HashMap::new();
     for c in s.chars() { *map.entry(c).or_insert(0) += 1; }
     map
 };
 freq(&lower1) == freq(&lower2)
}

Type Signatures

ConceptOCamlRust
Anagram check`val is_anagram : string -> string -> bool``fn is_anagram_sort(s1: &str, s2: &str) -> bool`
Find anagrams`val find_anagrams : string -> string list -> string list``fn find_anagrams<'a>(word: &str, candidates: &[&'a str]) -> Vec<&'a str>`
Sorted chars`val to_sorted_chars : string -> char list`closure: `s: &str-> Vec<char>`
String type`string` (immutable)`&str` (borrowed slice)

Key Insights

1. Pipeline vs method chains: OCaml's `|>` operator chains `String.to_seq |> List.of_seq |> List.sort`; Rust chains `.chars().collect()` then `.sort_unstable()` โ€” same idea, different syntax 2. Closures as helpers: Both languages use local functions/closures for `sorted_chars`; Rust closures capture nothing here (pure transformations) 3. Lifetime annotations: Rust's `find_anagrams` returns `Vec<&'a str>` โ€” the compiler needs to know the returned references live as long as the input candidates. OCaml's GC handles this implicitly 4. HashMap for O(n): Rust's standard library makes frequency counting natural with `HashMap::entry`; OCaml's stdlib doesn't have a convenient hash map for this pattern 5. In-place sorting: Rust sorts `Vec<char>` in-place (no allocation); OCaml creates a new sorted list

When to Use Each Style

Use sort-based when: strings are short and code clarity matters more than performance Use frequency-counting when: strings are long or you're doing many comparisons โ€” O(n) beats O(n log n)