Phone Keypad Letter Combinations: A Backtracking Interview Problem
Mapping digits to letters and generating every possible combination is a classic interview problem. It tests whether you can model a branching search space, maintain recursive state, and produce results without losing or duplicating paths.
Problem
Given a string containing only digits from 2 to 9, return all letter combinations that the number could represent. The mapping follows a traditional phone keypad:
2maps toa,b,c3maps tod,e,f4maps tog,h,i5maps toj,k,l6maps tom,n,o7maps top,q,r,s8maps tot,u,v9maps tow,x,y,z
For example, the input 23 should produce combinations such as ad, ae, af, bd, and so on.
Input Contract
The Python implementation below accepts a string. For this version of the problem, an empty string returns []: there are no phone digits to expand. Any character outside ASCII 2 through 9, including 0, 1, spaces, or letters, raises ValueError. Confirm these choices with your interviewer instead of silently dropping unsupported characters.
Repeated digits are valid. For example, 22 produces nine combinations, including aa, ab, and ba; the two positions are independent choices. Results follow the digit mapping order and need no final sort.
Core Idea: Backtracking
Each digit creates a set of choices. The final output is built by choosing one letter for each digit in order. That makes the problem a natural fit for depth-first search.
Track two pieces of state:
- The current index in the input string.
- The current partial combination.
When the index reaches the end of the input, the partial combination is complete and can be added to the result list.
Algorithm Steps
- Build a lookup table from each digit to its possible letters.
- Start a recursive search at index
0. - For the current digit, iterate through each mapped letter.
- Append one letter to the current path.
- Recurse into the next digit.
- Remove the last letter after returning, so the next branch starts from the correct state.
Runnable Python Implementation
def letter_combinations(digits: str) -> list[str]:
letters = {
"2": "abc", "3": "def", "4": "ghi", "5": "jkl",
"6": "mno", "7": "pqrs", "8": "tuv", "9": "wxyz",
}
if any(digit not in letters for digit in digits):
raise ValueError("Use only digits 2 through 9")
if not digits:
return []
result = []
path = []
def search(index):
if index == len(digits):
result.append("".join(path))
return
for letter in letters[digits[index]]:
path.append(letter)
search(index + 1)
path.pop()
search(0)
return result
assert letter_combinations("23") == [
"ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"
]
assert letter_combinations("") == []
assert letter_combinations("7") == ["p", "q", "r", "s"]
assert len(letter_combinations("79")) == 16
At the start of search(index), path contains exactly one valid letter for each of the first index digits. Appending a letter extends that invariant by one position. Popping it after the recursive call restores the prefix for the next choice.
Each complete sequence of choices reaches one leaf, so every valid combination is emitted exactly once. Joining the path creates a separate string; storing the mutable path itself would let later backtracking change earlier results.
Worked Example: Input 23
Start with an empty path. The first digit, 2, creates three branches:
- Choose
a, then tryd,e, andffor digit3: emitad,ae,af. - Remove
aand chooseb: emitbd,be,bf. - Remove
band choosec: emitcd,ce,cf.
Within the first branch, emitting ad returns to the call with prefix a. The final d is removed before trying e, so the next result is ae, not ade. Once all three second-digit choices finish, the outer call removes a and moves to b.
The complete result has 3 * 3 = 9 strings, each of length two, in the order shown in the assertion above.
Edge Cases to Check
- Empty input: return
[]before entering the search; otherwise the base case would emit an empty string. - Four-letter digits:
7and9each have four choices, so79produces 16 results. - Repeated digits:
22still produces nine distinct strings; do not deduplicate input digits. - Unsupported characters:
10,2a, and2 3raiseValueErrorbefore any combinations are generated. - Long input: the output grows exponentially. Discuss feasible input sizes; a generator can avoid storing all results at once, but cannot avoid the work of enumerating them.
Complexity
Let N be the number of input digits. In the worst case, every digit maps to four letters, so the number of combinations is 4^N.
The time complexity is O(4^N * N) because each complete combination has length N.
The auxiliary space complexity is O(N) for the recursion stack and current path, excluding the output list.
More precisely, for nonempty input let P be the product of the number of letters for each digit. Materializing all results takes O(N * P) time and O(N * P) output space. For 23, P = 9; for 79, P = 16. Input validation adds O(N) time. The empty-input case takes constant time and returns an empty list.
Related Guide
See Backtracking vs Iterative Generation for a broader comparison of recursive search, iterative expansion, and bit-mask traversal.