17. Letter Combinations of a Phone Number
Problem Description
You are given a string containing digits from 2
to 9
inclusive. Your task is to return all possible letter combinations that the number could represent, similar to how letters are mapped on a telephone keypad.
The digit-to-letter mapping follows the traditional phone keypad layout:
2
maps to"abc"
3
maps to"def"
4
maps to"ghi"
5
maps to"jkl"
6
maps to"mno"
7
maps to"pqrs"
8
maps to"tuv"
9
maps to"wxyz"
Note that the digit 1
does not map to any letters.
For example:
- If the input is
"23"
, you need to combine all letters from digit2
("abc"
) with all letters from digit3
("def"
), resulting in["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]
. - If the input is an empty string, return an empty list
[]
.
The answer can be returned in any order.
The solution uses an iterative approach where it processes each digit one by one. Starting with an initial list containing an empty string, it iterates through each digit in the input. For each digit, it looks up the corresponding letters and creates new combinations by appending each letter to all existing combinations. The process continues until all digits have been processed, building up the complete set of letter combinations.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem does not involve nodes and edges or graph traversal. We're dealing with generating combinations from digit-to-letter mappings.
Need to solve for kth smallest/largest?
- No: We're not looking for the kth smallest or largest element. We need to generate all possible combinations.
Involves Linked Lists?
- No: The problem doesn't involve linked list data structures or operations.
Does the problem have small constraints?
- Yes: The input string contains digits from
2-9
, and phone numbers typically have a limited length (the problem constraints usually limit the string length to 4 or less). Each digit maps to at most 4 letters, making the total number of combinations manageable.
Brute force / Backtracking?
- Yes: We need to explore all possible letter combinations systematically. For each digit, we must try every possible letter it maps to and combine it with all possible combinations from the remaining digits. This is a classic backtracking scenario where we build combinations incrementally and explore all paths.
Conclusion: The flowchart suggests using a Backtracking approach for generating all letter combinations. This makes sense because we need to:
- Make a choice (pick a letter for the current digit)
- Explore further (move to the next digit)
- Backtrack if needed (try the next letter for the current digit)
- Generate all valid combinations by exploring all possible paths
Intuition
Think about how you would manually write down all possible combinations for a phone number like "23"
. You'd likely start with the first digit 2
which maps to "abc"
, and for each letter (a
, b
, c
), you'd combine it with each letter from the second digit 3
which maps to "def"
(d
, e
, f
).
This naturally forms a tree-like structure where each level represents a digit, and each branch represents choosing a specific letter for that digit. The root starts empty, and as we go deeper, we build up our combinations letter by letter.
The key insight is that we need to explore every possible path from root to leaf in this decision tree. This is where backtracking shines - it's perfect for systematically exploring all paths in a tree structure.
However, the given solution takes a clever iterative approach instead of traditional recursive backtracking. It builds combinations level by level, similar to breadth-first traversal. Starting with [""]
(an empty combination), for each digit encountered:
- Take all existing partial combinations
- For each partial combination, append each possible letter from the current digit
- These new combinations become the foundation for the next digit
For example, with "23"
:
- Start:
[""]
- Process
2
("abc"
):["a", "b", "c"]
- Process
3
("def"
):["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]
This approach elegantly avoids explicit backtracking by maintaining all valid partial solutions at each step and systematically extending them. It's like building a Cartesian product incrementally - for each new digit, we multiply our existing combinations by the number of letters that digit represents.
The beauty of this solution is its simplicity: the list comprehension [a + b for a in ans for b in s]
captures the essence of combining every existing partial solution with every new letter possibility, effectively exploring all branches of our decision tree in just one line.
Learn more about Backtracking patterns.
Solution Approach
The solution uses an iterative traversal approach to build all letter combinations progressively. Let's walk through the implementation step by step:
1. Handle Edge Case:
if not digits: return []
If the input string is empty, return an empty list immediately since there are no combinations to generate.
2. Set Up the Digit-to-Letter Mapping:
d = ["abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"]
Create an array where index i
corresponds to digit i+2
. For example:
d[0] = "abc"
corresponds to digit2
d[1] = "def"
corresponds to digit3
- And so on...
3. Initialize the Result:
ans = [""]
Start with a list containing one empty string. This serves as the base for building combinations - we'll append letters to this empty string as we process each digit.
4. Process Each Digit Iteratively:
for i in digits:
s = d[int(i) - 2]
ans = [a + b for a in ans for b in s]
For each digit in the input string:
- Convert the digit character to an integer and subtract 2 to get the correct index in our mapping array
- Retrieve the corresponding letters (e.g., for digit
'2'
, get"abc"
) - Use a nested list comprehension to create all new combinations:
- For each existing combination
a
inans
- For each letter
b
in the current digit's letterss
- Create a new combination by concatenating
a + b
- For each existing combination
Example Walkthrough for "23"
:
Initial state: ans = [""]
Processing digit '2'
(letters: "abc"
):
- Combine
""
with'a'
→"a"
- Combine
""
with'b'
→"b"
- Combine
""
with'c'
→"c"
- Result:
ans = ["a", "b", "c"]
Processing digit '3'
(letters: "def"
):
- Combine
"a"
with each of"def"
→["ad", "ae", "af"]
- Combine
"b"
with each of"def"
→["bd", "be", "bf"]
- Combine
"c"
with each of"def"
→["cd", "ce", "cf"]
- Result:
ans = ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]
Time Complexity: O(4^n × n)
where n
is the length of the input string. In the worst case, each digit maps to 4 letters (like digit 7 or 9), giving us 4^n
combinations, and each combination has length n
.
Space Complexity: O(4^n × n)
to store all the generated combinations in the result list.
This iterative approach elegantly avoids recursion and explicit backtracking by maintaining all valid partial solutions at each step and systematically extending them with new letters.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with input "23"
to see how the iterative approach builds combinations step by step.
Initial Setup:
- Input:
"23"
- Mapping:
d = ["abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"]
- Start with:
ans = [""]
(list with one empty string)
Step 1: Process digit '2'
- Digit '2' maps to index
2-2 = 0
in our array - Letters for '2':
s = "abc"
- Apply list comprehension:
[a + b for a in ans for b in s]
- Take
""
from ans, append 'a' →"a"
- Take
""
from ans, append 'b' →"b"
- Take
""
from ans, append 'c' →"c"
- Take
- New
ans = ["a", "b", "c"]
Step 2: Process digit '3'
- Digit '3' maps to index
3-2 = 1
in our array - Letters for '3':
s = "def"
- Apply list comprehension:
[a + b for a in ans for b in s]
- Take
"a"
from ans:- Append 'd' →
"ad"
- Append 'e' →
"ae"
- Append 'f' →
"af"
- Append 'd' →
- Take
"b"
from ans:- Append 'd' →
"bd"
- Append 'e' →
"be"
- Append 'f' →
"bf"
- Append 'd' →
- Take
"c"
from ans:- Append 'd' →
"cd"
- Append 'e' →
"ce"
- Append 'f' →
"cf"
- Append 'd' →
- Take
- Final
ans = ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]
Visualization of Growth:
Start: [""] (1 combination of length 0) After '2': ["a", "b", "c"] (3 combinations of length 1) After '3': ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"] (9 combinations of length 2)
Notice how the number of combinations multiplies: we had 3 partial combinations after processing '2', and since '3' has 3 letters, we end up with 3 × 3 = 9 total combinations. Each iteration extends all existing combinations with every possible letter from the current digit.
Solution Implementation
1class Solution:
2 def letterCombinations(self, digits: str) -> List[str]:
3 # Handle empty input case
4 if not digits:
5 return []
6
7 # Map each digit (2-9) to corresponding letters on phone keypad
8 # Index 0 corresponds to digit 2, index 1 to digit 3, etc.
9 digit_to_letters = [
10 "abc", # 2
11 "def", # 3
12 "ghi", # 4
13 "jkl", # 5
14 "mno", # 6
15 "pqrs", # 7
16 "tuv", # 8
17 "wxyz" # 9
18 ]
19
20 # Initialize result list with empty string as starting point
21 result = [""]
22
23 # Process each digit in the input string
24 for digit in digits:
25 # Get the corresponding letters for current digit
26 # Subtract 2 to map digit to correct index (2->0, 3->1, etc.)
27 letters = digit_to_letters[int(digit) - 2]
28
29 # Generate all combinations by appending each letter
30 # to all existing combinations
31 result = [existing + letter
32 for existing in result
33 for letter in letters]
34
35 return result
36
1class Solution {
2 public List<String> letterCombinations(String digits) {
3 // Initialize result list to store all possible combinations
4 List<String> result = new ArrayList<>();
5
6 // Handle edge case: empty input
7 if (digits.length() == 0) {
8 return result;
9 }
10
11 // Start with an empty string to build upon
12 result.add("");
13
14 // Mapping of digit (2-9) to corresponding letters
15 // Index 0 represents digit '2', index 1 represents digit '3', etc.
16 String[] digitToLetters = new String[] {
17 "abc", // 2
18 "def", // 3
19 "ghi", // 4
20 "jkl", // 5
21 "mno", // 6
22 "pqrs", // 7
23 "tuv", // 8
24 "wxyz" // 9
25 };
26
27 // Process each digit in the input string
28 for (char digit : digits.toCharArray()) {
29 // Get the letters corresponding to current digit
30 // Subtract '2' because our array starts at digit 2
31 String letters = digitToLetters[digit - '2'];
32
33 // Temporary list to store new combinations for current digit
34 List<String> tempCombinations = new ArrayList<>();
35
36 // For each existing combination in result
37 for (String existingCombination : result) {
38 // Append each letter of current digit to existing combinations
39 for (char letter : letters.toCharArray()) {
40 tempCombinations.add(existingCombination + letter);
41 }
42 }
43
44 // Update result with new combinations
45 result = tempCombinations;
46 }
47
48 return result;
49 }
50}
51
1class Solution {
2public:
3 vector<string> letterCombinations(string digits) {
4 // Return empty vector if input is empty
5 if (digits.empty()) {
6 return {};
7 }
8
9 // Mapping of digits to corresponding letters (2-9)
10 // Index 0 corresponds to digit '2', index 1 to digit '3', etc.
11 vector<string> digitToLetters = {
12 "abc", // 2
13 "def", // 3
14 "ghi", // 4
15 "jkl", // 5
16 "mno", // 6
17 "pqrs", // 7
18 "tuv", // 8
19 "wxyz" // 9
20 };
21
22 // Initialize result with empty string to build upon
23 vector<string> result = {""};
24
25 // Process each digit in the input string
26 for (char digit : digits) {
27 // Get the corresponding letters for current digit
28 string letters = digitToLetters[digit - '2'];
29
30 // Temporary vector to store new combinations
31 vector<string> newCombinations;
32
33 // For each existing combination in result
34 for (const string& existingCombination : result) {
35 // Append each possible letter to create new combinations
36 for (char letter : letters) {
37 newCombinations.push_back(existingCombination + letter);
38 }
39 }
40
41 // Replace result with the new combinations
42 result = move(newCombinations);
43 }
44
45 return result;
46 }
47};
48
1/**
2 * Generates all possible letter combinations that a phone number could represent.
3 * Each digit maps to a set of letters as on a traditional phone keypad:
4 * 2->abc, 3->def, 4->ghi, 5->jkl, 6->mno, 7->pqrs, 8->tuv, 9->wxyz
5 *
6 * @param digits - A string containing digits from 2-9 inclusive
7 * @returns An array of all possible letter combinations
8 */
9function letterCombinations(digits: string): string[] {
10 // Return empty array if input is empty
11 if (digits.length === 0) {
12 return [];
13 }
14
15 // Initialize result array with empty string to start building combinations
16 const combinations: string[] = [''];
17
18 // Phone keypad mappings: index 0 represents digit 2, index 1 represents digit 3, etc.
19 const digitToLetters: string[] = ['abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz'];
20
21 // Process each digit in the input string
22 for (const digit of digits) {
23 // Get the corresponding letters for current digit (subtract 2 to align with array index)
24 const letters: string = digitToLetters[Number(digit) - 2];
25
26 // Temporary array to store new combinations for this iteration
27 const newCombinations: string[] = [];
28
29 // For each existing combination, append each possible letter
30 for (const existingCombination of combinations) {
31 for (const letter of letters) {
32 // Create new combination by appending current letter to existing combination
33 newCombinations.push(existingCombination + letter);
34 }
35 }
36
37 // Replace all elements in combinations array with new combinations
38 combinations.splice(0, combinations.length, ...newCombinations);
39 }
40
41 return combinations;
42}
43
Time and Space Complexity
The time complexity is O(4^n)
, where n
is the length of the input digits string.
Time Complexity Analysis:
- Each digit maps to at most 4 letters (digits 7 and 9 map to "pqrs" and "wxyz" respectively)
- For each digit in the input, we iterate through all existing combinations and append each possible letter
- If we have
n
digits, and each digit can map to at most 4 letters, the total number of combinations generated is at most4^n
- For each combination, we perform string concatenation operations
- Therefore, the overall time complexity is
O(4^n)
The space complexity is O(4^n)
, where n
is the length of the input digits string.
Space Complexity Analysis:
- The
ans
list stores all possible letter combinations - In the worst case (when all digits map to 4 letters), we generate
4^n
combinations - Each combination has length
n
, so technically the total space used isO(n * 4^n)
- However, since we're counting the number of combinations as the primary factor, the space complexity is considered
O(4^n)
- The intermediate lists created during list comprehension also contribute to space usage, but don't exceed the final result size
- Therefore, the overall space complexity is
O(4^n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Digit-to-Index Mapping
One of the most common mistakes is incorrectly mapping digits to their corresponding letter groups. Since digits start from 2 (not 0), you need to subtract 2 from the digit to get the correct array index.
Incorrect:
# Wrong: Using digit directly as index
letters = digit_to_letters[int(digit)] # This would cause IndexError
# Wrong: Subtracting 1 instead of 2
letters = digit_to_letters[int(digit) - 1] # Maps 2→"def", 3→"ghi" (off by one)
Correct:
letters = digit_to_letters[int(digit) - 2] # Maps 2→"abc", 3→"def" correctly
2. Forgetting to Handle Empty Input
Without checking for empty input, the function would return [""]
instead of []
, which is incorrect according to the problem specification.
Incorrect:
def letterCombinations(self, digits: str) -> List[str]:
digit_to_letters = ["abc", "def", ...]
result = [""]
for digit in digits: # Empty string would skip loop
# ...
return result # Returns [""] for empty input, should be []
Correct:
def letterCombinations(self, digits: str) -> List[str]:
if not digits:
return [] # Explicitly handle empty input
# ... rest of the code
3. Overwriting Result List During Iteration
A subtle bug can occur if you try to modify the result list while iterating over it in the wrong way.
Incorrect:
# This approach modifies result in place incorrectly
result = [""]
for digit in digits:
letters = digit_to_letters[int(digit) - 2]
new_result = []
for existing in result:
for letter in letters:
new_result.append(existing + letter)
result = new_result # Reassigning is fine, but...
# Or worse, trying to modify in place:
for digit in digits:
letters = digit_to_letters[int(digit) - 2]
for i in range(len(result)): # Dangerous if result changes size
for letter in letters:
result.append(result[i] + letter) # This causes infinite loop!
Correct:
# Create a completely new list each iteration
result = [""]
for digit in digits:
letters = digit_to_letters[int(digit) - 2]
result = [existing + letter
for existing in result
for letter in letters]
4. Memory Inefficiency with Large Inputs
While not exactly a bug, the iterative approach stores all intermediate results, which can be memory-intensive. For very long digit strings, consider using a generator or recursive approach with yield to produce results on-demand rather than storing everything at once.
Alternative approach for memory efficiency:
def letterCombinations(self, digits: str):
if not digits:
return []
digit_to_letters = ["abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"]
def backtrack(index, path):
if index == len(digits):
result.append(path)
return
letters = digit_to_letters[int(digits[index]) - 2]
for letter in letters:
backtrack(index + 1, path + letter)
result = []
backtrack(0, "")
return result
This recursive approach uses the call stack instead of maintaining all intermediate combinations, which can be more memory-efficient for certain use cases.
Which of the following shows the order of node visit in a Breadth-first Search?
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!