A detailed explanation of generating all possible phone keypad letter combinations using backtracking.
Problem Restatement
We are given a string digits.
Each character is a digit from 2 to 9.
Each digit maps to letters like on a phone keypad:
| Digit | Letters |
|---|---|
2 | abc |
3 | def |
4 | ghi |
5 | jkl |
6 | mno |
7 | pqrs |
8 | tuv |
9 | wxyz |
We need to return all possible letter combinations that the digit string could represent.
The answer may be returned in any order. The statement notes that 1 does not map to any letters. The input digits are from 2 to 9.
Input and Output
| Item | Meaning |
|---|---|
| Input | A string digits |
| Output | All possible letter combinations |
| Digit range | Each digit is from 2 to 9 |
| Empty input | Return [] |
| Order | Any order is accepted |
Example function shape:
def letterCombinations(digits: str) -> list[str]:
...Examples
Example 1:
digits = "23"Digit 2 maps to:
a, b, cDigit 3 maps to:
d, e, fAll combinations are:
ad, ae, af
bd, be, bf
cd, ce, cfOutput:
["ad","ae","af","bd","be","bf","cd","ce","cf"]Example 2:
digits = ""There are no digits, so there are no combinations.
Output:
[]Example 3:
digits = "2"Output:
["a","b","c"]First Thought: Nested Loops
For digits = "23", we can write two loops:
for a in "abc":
for b in "def":
result.append(a + b)This works only when the input has exactly two digits.
If the input has three digits, we need three loops.
If the input has four digits, we need four loops.
The number of loops depends on the input length, so fixed nested loops are not a good general solution.
Key Insight
This is a combination-building problem.
At each digit, choose one letter from that digit’s mapped letters.
For example, with:
digits = "23"we first choose one of:
a, b, cThen for each choice, choose one of:
d, e, fEach path from the first digit to the last digit forms one complete combination.
This is exactly what backtracking does.
Backtracking Tree
For:
digits = "23"The tree is:
start
a
ad
ae
af
b
bd
be
bf
c
cd
ce
cfEach level corresponds to one digit.
Each edge chooses one letter.
When the path length equals len(digits), we have one complete combination.
Algorithm
- If
digitsis empty, return[]. - Store the digit-to-letter mapping.
- Create an empty result list.
- Define a backtracking function with:
index: which digit we are processingpath: letters chosen so far
- If
index == len(digits), addpathto the result. - Otherwise:
- get the letters for
digits[index] - try each letter
- recurse to the next index
- get the letters for
- Return the result list.
Correctness
Each recursive level handles exactly one digit.
For the digit at position index, the algorithm tries every letter mapped from that digit.
So after processing k digits, path contains one valid choice for each of the first k digits.
When index == len(digits), the path contains exactly one letter for every input digit, so it is a valid complete combination.
The algorithm explores every letter choice for every digit, so no valid combination is missed.
It appends a result only when one letter has been chosen per digit, so no invalid partial combination is returned.
Therefore the algorithm returns exactly all possible letter combinations.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(4^n * n) | There are at most 4^n combinations, and each output string has length n |
| Space | O(4^n * n) | The result list stores all combinations |
Here:
n = len(digits)The recursion stack itself uses O(n) space.
The factor 4 comes from digits 7 and 9, which each map to four letters.
Implementation
class Solution:
def letterCombinations(self, digits: str) -> list[str]:
if not digits:
return []
phone = {
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
}
result = []
def backtrack(index: int, path: list[str]) -> None:
if index == len(digits):
result.append("".join(path))
return
digit = digits[index]
for letter in phone[digit]:
path.append(letter)
backtrack(index + 1, path)
path.pop()
backtrack(0, [])
return resultCode Explanation
Handle the empty input first:
if not digits:
return []The mapping follows the phone keypad:
phone = {
"2": "abc",
"3": "def",
...
"9": "wxyz",
}The result list stores complete combinations:
result = []The helper function:
backtrack(index, path)means:
We have already chosen letters for digits before index.
Now choose a letter for digits[index].When all digits have been processed:
if index == len(digits):
result.append("".join(path))
returnTry every possible letter for the current digit:
for letter in phone[digit]:Choose a letter:
path.append(letter)Recurse:
backtrack(index + 1, path)Undo the choice:
path.pop()This lets the next letter reuse the same path list.
Finally:
return resultTesting
def normalize(values):
return sorted(values)
def run_tests():
s = Solution()
assert normalize(s.letterCombinations("23")) == normalize([
"ad", "ae", "af",
"bd", "be", "bf",
"cd", "ce", "cf",
])
assert s.letterCombinations("") == []
assert normalize(s.letterCombinations("2")) == ["a", "b", "c"]
assert normalize(s.letterCombinations("7")) == [
"p", "q", "r", "s",
]
assert normalize(s.letterCombinations("79")) == normalize([
"pw", "px", "py", "pz",
"qw", "qx", "qy", "qz",
"rw", "rx", "ry", "rz",
"sw", "sx", "sy", "sz",
])
print("all tests passed")
run_tests()| Test | Why |
|---|---|
"23" | Standard two-digit example |
"" | Empty input returns no combinations |
"2" | Single digit with three letters |
"7" | Single digit with four letters |
"79" | Both digits have four letters |