1067-letter-combinations — Phone Keypad Letter Combinations
Functional Programming
Tutorial
The Problem
Given a phone number keypad string (digits 2–9), generate all possible letter combinations. This is a classic Cartesian product problem: each digit maps to a set of letters, and you want all combinations taking one letter from each digit's set.
The problem appears in T9 predictive text input (legacy phones), QR code testing tools that enumerate all encodings, and any system generating all strings from a grammar.
🎯 Learning Outcomes
Code Example
#![allow(clippy::all)]
// 1067: Phone Keypad Letter Combinations
const PHONE_MAP: &[&str] = &[
"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz",
];
// Approach 1: Backtracking
fn letter_combos(digits: &str) -> Vec<String> {
if digits.is_empty() {
return vec![];
}
let mut results = Vec::new();
let mut current = String::new();
let digits: Vec<usize> = digits.bytes().map(|b| (b - b'0') as usize).collect();
fn backtrack(idx: usize, digits: &[usize], current: &mut String, results: &mut Vec<String>) {
if idx == digits.len() {
results.push(current.clone());
return;
}
for c in PHONE_MAP[digits[idx]].chars() {
current.push(c);
backtrack(idx + 1, digits, current, results);
current.pop();
}
}
backtrack(0, &digits, &mut current, &mut results);
results
}
// Approach 2: Iterative with queue
fn letter_combos_iter(digits: &str) -> Vec<String> {
if digits.is_empty() {
return vec![];
}
let mut queue = vec![String::new()];
for b in digits.bytes() {
let letters = PHONE_MAP[(b - b'0') as usize];
let mut next_queue = Vec::new();
for current in &queue {
for c in letters.chars() {
let mut s = current.clone();
s.push(c);
next_queue.push(s);
}
}
queue = next_queue;
}
queue
}
// Approach 3: Functional with fold
fn letter_combos_fold(digits: &str) -> Vec<String> {
if digits.is_empty() {
return vec![];
}
digits.bytes().fold(vec![String::new()], |acc, b| {
let letters = PHONE_MAP[(b - b'0') as usize];
acc.iter()
.flat_map(|prefix| letters.chars().map(move |c| format!("{}{}", prefix, c)))
.collect()
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backtrack() {
let r = letter_combos("23");
assert_eq!(r.len(), 9);
assert!(r.contains(&"ad".to_string()));
assert!(r.contains(&"cf".to_string()));
}
#[test]
fn test_iter() {
let r = letter_combos_iter("23");
assert_eq!(r.len(), 9);
}
#[test]
fn test_fold() {
let r = letter_combos_fold("23");
assert_eq!(r.len(), 9);
}
#[test]
fn test_empty() {
assert!(letter_combos("").is_empty());
}
#[test]
fn test_single() {
assert_eq!(letter_combos("7").len(), 4); // pqrs
}
}Key Differences
String::push and String::pop for in-place character manipulation during backtracking; OCaml uses string concatenation.Vec<String> and extends it explicitly.String::pop**: Rust's current.pop() undoes the last push(c) for backtracking; OCaml's immutable string approach naturally supports backtracking.PHONE_MAP indexing**: Both use digit-minus-base offset (digit - '0') to index the map — identical arithmetic.OCaml Approach
let phone_map = [|""; ""; "abc"; "def"; "ghi"; "jkl"; "mno"; "pqrs"; "tuv"; "wxyz"|]
let letter_combos digits =
if digits = "" then []
else
String.fold_left (fun acc d ->
let letters = phone_map.(Char.code d - Char.code '0') in
if acc = [] then List.map (String.make 1) (String.to_seq letters |> List.of_seq)
else List.concat_map (fun combo ->
List.map (fun c -> combo ^ String.make 1 c)
(String.to_seq letters |> List.of_seq)
) acc
) [] (String.to_seq digits |> List.of_seq)
OCaml's List.concat_map provides the Cartesian product expansion cleanly.
Full Source
#![allow(clippy::all)]
// 1067: Phone Keypad Letter Combinations
const PHONE_MAP: &[&str] = &[
"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz",
];
// Approach 1: Backtracking
fn letter_combos(digits: &str) -> Vec<String> {
if digits.is_empty() {
return vec![];
}
let mut results = Vec::new();
let mut current = String::new();
let digits: Vec<usize> = digits.bytes().map(|b| (b - b'0') as usize).collect();
fn backtrack(idx: usize, digits: &[usize], current: &mut String, results: &mut Vec<String>) {
if idx == digits.len() {
results.push(current.clone());
return;
}
for c in PHONE_MAP[digits[idx]].chars() {
current.push(c);
backtrack(idx + 1, digits, current, results);
current.pop();
}
}
backtrack(0, &digits, &mut current, &mut results);
results
}
// Approach 2: Iterative with queue
fn letter_combos_iter(digits: &str) -> Vec<String> {
if digits.is_empty() {
return vec![];
}
let mut queue = vec![String::new()];
for b in digits.bytes() {
let letters = PHONE_MAP[(b - b'0') as usize];
let mut next_queue = Vec::new();
for current in &queue {
for c in letters.chars() {
let mut s = current.clone();
s.push(c);
next_queue.push(s);
}
}
queue = next_queue;
}
queue
}
// Approach 3: Functional with fold
fn letter_combos_fold(digits: &str) -> Vec<String> {
if digits.is_empty() {
return vec![];
}
digits.bytes().fold(vec![String::new()], |acc, b| {
let letters = PHONE_MAP[(b - b'0') as usize];
acc.iter()
.flat_map(|prefix| letters.chars().map(move |c| format!("{}{}", prefix, c)))
.collect()
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backtrack() {
let r = letter_combos("23");
assert_eq!(r.len(), 9);
assert!(r.contains(&"ad".to_string()));
assert!(r.contains(&"cf".to_string()));
}
#[test]
fn test_iter() {
let r = letter_combos_iter("23");
assert_eq!(r.len(), 9);
}
#[test]
fn test_fold() {
let r = letter_combos_fold("23");
assert_eq!(r.len(), 9);
}
#[test]
fn test_empty() {
assert!(letter_combos("").is_empty());
}
#[test]
fn test_single() {
assert_eq!(letter_combos("7").len(), 4); // pqrs
}
}
✓ Tests
Rust test suite
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backtrack() {
let r = letter_combos("23");
assert_eq!(r.len(), 9);
assert!(r.contains(&"ad".to_string()));
assert!(r.contains(&"cf".to_string()));
}
#[test]
fn test_iter() {
let r = letter_combos_iter("23");
assert_eq!(r.len(), 9);
}
#[test]
fn test_fold() {
let r = letter_combos_fold("23");
assert_eq!(r.len(), 9);
}
#[test]
fn test_empty() {
assert!(letter_combos("").is_empty());
}
#[test]
fn test_single() {
assert_eq!(letter_combos("7").len(), 4); // pqrs
}
}
Deep Comparison
Phone Keypad Letter Combinations — Comparison
Core Insight
Each digit maps to 3-4 letters, creating a tree of combinations. Three approaches: backtracking (DFS), iterative queue (BFS-like), and fold (functional). The fold is most elegant, treating each digit as a transformation on the set of prefixes.
OCaml Approach
Buffer for string building with truncate for backtrackingString.iter to iterate over digit's lettersList.concat_map for functional branchingQueue module for iterative approachRust Approach
String::push/pop for backtrackingPHONE_MAP as const &[&str] arrayflat_map + format! for fold approach — very expressiveVec as queue with swap for iterative approachComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Phone map | let phone_map = [|...|] | const PHONE_MAP: &[&str] |
| Digit to index | Char.code d - Char.code '0' | (b - b'0') as usize |
| String backtrack | Buffer.truncate | String::pop() |
| Functional | List.concat_map | flat_map + format! |
| Queue approach | Queue.t (mutable) | Vec swap per level |
Exercises
* and # symbols and test with phone numbers including these characters.letter_combos_lazy(digits: &str) -> impl Iterator<Item=String> that generates combinations lazily without collecting all into a Vec.