title: Letter Combinations of a Phone Number slug: letter-combinations-of-a-phone-number difficulty: medium leetcode_id: 17 leetcode_url: https://leetcode.com/problems/letter-combinations-of-a-phone-number/ categories: - strings - hash-tables - recursion patterns: - backtracking function_signature: "def letter_combinations(digits: str) -> list[str]:" test_cases: visible: - input: { digits: "23" } expected: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"] - input: { digits: "" } expected: [] - input: { digits: "2" } expected: ["a", "b", "c"] hidden: - input: { digits: "7" } expected: ["p", "q", "r", "s"] - input: { digits: "79" } expected: ["pw", "px", "py", "pz", "qw", "qx", "qy", "qz", "rw", "rx", "ry", "rz", "sw", "sx", "sy", "sz"] - input: { digits: "234" } expected: ["adg", "adh", "adi", "aeg", "aeh", "aei", "afg", "afh", "afi", "bdg", "bdh", "bdi", "beg", "beh", "bei", "bfg", "bfh", "bfi", "cdg", "cdh", "cdi", "ceg", "ceh", "cei", "cfg", "cfh", "cfi"] - input: { digits: "9" } expected: ["w", "x", "y", "z"] - input: { digits: "22" } expected: ["aa", "ab", "ac", "ba", "bb", "bc", "ca", "cb", "cc"] - input: { digits: "8" } expected: ["t", "u", "v"] - input: { digits: "56" } expected: ["jm", "jn", "jo", "km", "kn", "ko", "lm", "ln", "lo"] description: | Given a string containing digits from `2-9` inclusive, return all possible letter combinations that the number could represent. Return the answer in **any order**. A mapping of digits to letters (just like on the telephone buttons) is given below. Note that `1` does not map to any letters. | Digit | Letters | |-------|---------| | 2 | a, b, c | | 3 | d, e, f | | 4 | g, h, i | | 5 | j, k, l | | 6 | m, n, o | | 7 | p, q, r, s | | 8 | t, u, v | | 9 | w, x, y, z | constraints: | - `0 <= digits.length <= 4` - `digits[i]` is a digit in the range `['2', '9']` examples: - input: 'digits = "23"' output: '["ad","ae","af","bd","be","bf","cd","ce","cf"]' explanation: "Digit 2 maps to 'abc' and digit 3 maps to 'def'. Combining each letter from 2 with each letter from 3 gives 9 combinations." - input: 'digits = ""' output: "[]" explanation: "Empty input returns an empty list." - input: 'digits = "2"' output: '["a","b","c"]' explanation: "Digit 2 maps to 'abc', so we return all three letters." explanation: intuition: | Think of this problem like an old-school phone where you had to press buttons multiple times to type letters. Each digit opens up a **set of choices**, and we need to explore every possible combination of those choices. Imagine you're at a crossroads where each path branches into multiple smaller paths. For the input `"23"`: - First, you see three paths: `a`, `b`, `c` (from digit `2`) - From each of those paths, you see three more paths: `d`, `e`, `f` (from digit `3`) - You must walk down every possible route to collect all combinations This is a classic **backtracking** scenario: build a solution character by character, explore all possibilities at each step, and backtrack to try other options. The key insight is that we're essentially computing a **Cartesian product** of letter sets. For `n` digits where each digit maps to `k` letters on average, we'll generate roughly `k^n` combinations — and we need to visit them all. approach: | We solve this using **Backtracking (DFS)**: **Step 1: Handle the edge case** - If the input `digits` is empty, return an empty list immediately - This avoids unnecessary processing and edge case bugs   **Step 2: Create the digit-to-letter mapping** - Build a dictionary mapping each digit (`'2'` through `'9'`) to its corresponding letters - Example: `'2'` → `'abc'`, `'7'` → `'pqrs'`   **Step 3: Define a recursive backtracking function** - `backtrack(index, current_combination)`: - `index`: which digit we're currently processing - `current_combination`: the string built so far   **Step 4: Base case — combination complete** - If `index == len(digits)`, we've processed all digits - Add `current_combination` to our results list   **Step 5: Recursive case — explore all letters for current digit** - Get the letters corresponding to `digits[index]` - For each letter: - Append it to `current_combination` - Recursively call `backtrack(index + 1, ...)` - The recursion naturally "backtracks" when it returns   **Step 6: Return all collected combinations** - After the recursion completes, return the results list common_pitfalls: - title: Forgetting the Empty Input Case description: | If `digits = ""`, you should return `[]`, not `[""]`. A common mistake is initializing the result with an empty string and building from there, which would incorrectly return `[""]` for empty input. Always check for empty input at the start and return an empty list. wrong_approach: "Returning [''] for empty input" correct_approach: "Check if digits is empty and return [] immediately" - title: Using Iteration Instead of Backtracking description: | While you can solve this iteratively by building combinations level by level, it's harder to visualise and more error-prone. The iterative approach works but misses the opportunity to practice the fundamental backtracking pattern that's essential for harder problems like N-Queens, permutations, and subsets. wrong_approach: "Complex iterative logic with nested loops" correct_approach: "Clean recursive backtracking with clear base case" - title: String Concatenation in Loops description: | In Python, repeatedly concatenating strings with `+` in a loop creates new string objects each time, leading to O(n^2) behaviour. For this problem with `digits.length <= 4`, it's not a performance issue. But for larger inputs, use a list and `''.join()` at the end. wrong_approach: "current = current + letter in tight loops" correct_approach: "Use list append and join, or accept small overhead for clarity" key_takeaways: - "**Backtracking template**: This problem demonstrates the core backtracking pattern — make a choice, explore, unmake the choice (implicitly via recursion)" - "**Cartesian product**: Combining elements from multiple sets is a fundamental operation that backtracking handles elegantly" - "**Hash map for mapping**: Using a dictionary to map digits to letters keeps the code clean and extensible" - "**Foundation for harder problems**: This exact pattern scales to permutations, combinations, subsets, and constraint satisfaction problems" time_complexity: "O(4^n * n) where `n` is the length of `digits`. In the worst case (all 7s or 9s), each digit maps to 4 letters. We generate up to 4^n combinations, and each combination takes O(n) time to build." space_complexity: "O(n) for the recursion stack depth, not counting the output. The maximum recursion depth equals the number of digits." solutions: - approach_name: Backtracking (DFS) is_optimal: true code: | def letter_combinations(digits: str) -> list[str]: # Edge case: empty input if not digits: return [] # Mapping of digits to letters (like a phone keypad) phone_map = { '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz' } result = [] def backtrack(index: int, current: str) -> None: # Base case: we've processed all digits if index == len(digits): result.append(current) return # Get letters for current digit letters = phone_map[digits[index]] # Try each letter and recurse for letter in letters: backtrack(index + 1, current + letter) # Start backtracking from index 0 with empty string backtrack(0, "") return result explanation: | **Time Complexity:** O(4^n * n) — We generate up to 4^n combinations (when digits are 7 or 9), and building each string takes O(n). **Space Complexity:** O(n) — Recursion stack depth equals the number of digits. The backtracking approach naturally explores all paths in the decision tree. Each recursive call handles one digit, trying all its letters before returning. This pattern is fundamental to many combinatorial problems. - approach_name: Iterative (BFS-like) is_optimal: false code: | def letter_combinations(digits: str) -> list[str]: # Edge case: empty input if not digits: return [] phone_map = { '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz' } # Start with empty combination result = [""] # Process each digit for digit in digits: letters = phone_map[digit] # Build new combinations by appending each letter new_result = [] for combination in result: for letter in letters: new_result.append(combination + letter) result = new_result return result explanation: | **Time Complexity:** O(4^n * n) — Same as backtracking, we still generate all combinations. **Space Complexity:** O(4^n) — We store all intermediate combinations at each level. This iterative approach builds combinations level by level. While it works, it uses more space than backtracking and doesn't teach the fundamental recursive pattern. It's included to show an alternative perspective.