Problem Overview — LeetCode 17 Letter Combinations of a Phone Number
LeetCode 17 gives you a string of digits from 2 to 9 and asks you to return all possible letter combinations that the number could represent on a phone keypad. For example, the input "23" maps digit 2 to "abc" and digit 3 to "def", producing nine combinations: ["ad","ae","af","bd","be","bf","cd","ce","cf"].
This is a classic backtracking problem. The solution space is a tree where each level corresponds to one digit in the input, and each branch at that level corresponds to one letter for that digit. Traversing all root-to-leaf paths in this tree gives all valid combinations.
The problem has a clean recursive structure: choose one letter for the current digit, recurse on the remaining digits, then backtrack and try the next letter. The base case is when all digits have been processed — at that point, the current combination is complete and added to the result.
- Input: string of digits 2-9, length 0-4
- Output: all possible letter combinations (any order)
- Return empty list [] for empty input string
- Phone keypad mapping: 2→abc, 3→def, 4→ghi, 5→jkl, 6→mno, 7→pqrs, 8→tuv, 9→wxyz
- Digits 7 and 9 map to 4 letters each; all others map to 3
Phone Keypad Mapping — Building the Digit-to-Letter Dictionary
The keypad mapping is fixed: digit 2 maps to "abc", 3 to "def", 4 to "ghi", 5 to "jkl", 6 to "mno", 7 to "pqrs", 8 to "tuv", and 9 to "wxyz". Store this as a Python dict or a Java HashMap/array indexed by digit character. This mapping is the only data structure needed beyond the recursion.
In Python, use a dict like phone = {"2": "abc", "3": "def", ...}. In Java, use a String array indexed 0-9 where phone[2] = "abc" etc., since digit characters can be converted to indices with ch - '0'. The array approach is slightly faster and avoids hash lookups.
Once you have the mapping, the rest of the algorithm is pure backtracking. At each recursive call, you look up the current digit in the map to get the candidate letters, then loop over each letter, append it to the current combination, recurse on the next digit, and remove the last letter before trying the next one.
Clean Backtracking Structure
This is a clean backtracking problem: at each digit, branch into 3 or 4 letters, recurse on the remaining digits. The total number of combinations equals the product of letter counts per digit — for "23" that is 3 * 3 = 9, for "79" it is 4 * 4 = 16. Understanding this product structure helps reason about both the algorithm and its complexity.
Backtracking Approach — Recursive Depth-First Traversal
The backtracking solution defines a recursive helper that takes the current digit index and the current combination string built so far. The base case is index == len(digits): when all digits have been consumed, add the current combination to the result list and return.
In the recursive case, look up digits[index] in the phone map to get its letters. For each letter, append it to the current combination, call the helper with index + 1, then remove the last character (backtrack). This pattern — choose, explore, unchoose — is the canonical backtracking template.
In Python, strings are immutable so you pass current + letter to each recursive call rather than mutating in place. In Java, use a StringBuilder and delete the last character after each recursive call for efficiency. Both approaches produce the same result; the StringBuilder version avoids creating many temporary string objects.
- 1Define helper(index, current): base case index == len(digits) → add current to results
- 2Look up digits[index] in the phone mapping to get the letter candidates
- 3For each candidate letter: recurse with helper(index + 1, current + letter)
- 4Backtracking is implicit in Python (new string each call) or explicit in Java (StringBuilder delete)
- 5Call helper(0, "") to start; return the results list
Iterative BFS Alternative — Queue Expansion Without Recursion
The iterative approach starts with a list containing one empty string: combinations = [""]. For each digit in the input, it expands every existing partial combination by appending each letter that corresponds to the current digit. After processing all digits, the list contains all complete combinations.
In Python this can be written concisely as a list comprehension: combinations = [combo + letter for combo in combinations for letter in phone[digit]]. Repeat this for each digit. The list grows from length 1 to the final product of letter counts per digit.
This approach processes combinations breadth-first — all one-character prefixes before two-character prefixes and so on. It produces the same output as backtracking and has the same time and space complexity, but avoids the call stack entirely.
BFS Builds Combinations Level by Level
The iterative approach builds combinations level by level like BFS: after processing the first digit you have 3 or 4 partial strings, after the second digit you have 9-16, and so on. It avoids recursion stack overhead but uses more intermediate memory for partial results since all partial combinations are held in memory at each step rather than one path at a time.
Complexity Analysis — Product of Letter Counts
The time complexity is O(3^m * 4^n) where m is the number of digits that map to 3 letters (digits 2, 3, 4, 5, 6, 8) and n is the number of digits that map to 4 letters (digits 7 and 9). Each combination requires O(L) time to construct where L is the length of the input, so the full time is O(L * 3^m * 4^n).
The space complexity is also O(3^m * 4^n) to store all combinations in the output list. The recursion stack adds O(L) depth at most, which is dominated by the output space. For the maximum input length of 4, the worst case is 4 digits all being 7 or 9, giving 4^4 = 256 combinations — entirely manageable.
In practice this problem always has small input (length 0-4), so the constants dominate and any correct implementation runs in effectively constant time. The complexity analysis matters more for understanding the scaling behavior in generalized versions of the problem.
- 1Total combinations = product of letter counts for each digit
- 2Digits 2,3,4,5,6,8 each contribute factor 3; digits 7,9 each contribute factor 4
- 3Maximum input length is 4; worst case all 7s or 9s → 4^4 = 256 combinations
- 4Time: O(L * 3^m * 4^n) where L = input length, m = 3-letter digits, n = 4-letter digits
- 5Space: O(3^m * 4^n) for output, O(L) for recursion stack
Code Walkthrough — Python and Java Implementations
The Python backtracking solution is typically 12-15 lines. Define the phone dict, initialize an empty results list, define the recursive helper with index and current string parameters, handle the base case by appending to results, then loop over phone[digits[index]] calling helper(index + 1, current + letter). The iterative Python version is even shorter: initialize combinations = [""] then for each digit use a list comprehension to expand.
The Java backtracking version uses a String array for the phone mapping indexed by digit value (phoneMap[2] = "abc" etc.), a List<String> for results, and a StringBuilder for the current combination. At each recursive call, append a character, recurse, then deleteCharAt(sb.length() - 1) to backtrack. Return results as a List<String>.
Both Python and Java solutions run in O(3^m * 4^n) time and space. The key implementation detail is handling the empty input: check if digits is empty or null at the start and return an empty list immediately — not a list containing an empty string.
Handle Empty Input: Return [] Not [""]
Always check for empty input at the start and return [] not [""]. An empty digit string means no combinations exist — returning [""] would incorrectly report one combination (the empty string). This edge case is explicitly tested in LeetCode 17 and easy to miss if you initialize your results list with [""] and skip the empty-input check.