Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Map each digit to its phone letters and return every string formed by picking one letter per digit — a Cartesian product. The canonical answer is backtracking: at digit `index`, loop over that digit's letters, choose one, recurse to `index + 1`, then un-choose. Record when `index` reaches the end. Handle the empty input as a special case (return []). The result size is up to 4^n, so any solution is O(n · 4ⁿ).
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you recognise the Cartesian-product structure and write a clean index-advancing backtracking that draws from the right per-digit alphabet, plus whether you remember the empty-input guard. Bonus: quoting the 4ⁿ bound and naming the iterative BFS-style build as an alternative.
Where candidates slip
Returning [""] for empty input instead of [], reusing a start index (wrong model — each digit is a distinct position), or forgetting that 7 and 9 have four letters.
How to stand out
Name it as a Cartesian product, handle the empty input explicitly, write the index-advancing choose/explore/un-choose, state the O(n · 4ⁿ) bound, and mention you could also build it iteratively by expanding a frontier of partial strings.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachExplore the full treeCollapse the tree
How to read it:Each level corresponds to one digit; each edge picks one of that digit's letters and advances to the next digit. Every leaf (all digits consumed) is a valid combination — there's no pruning.
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 1 · Optimal
Backtracking over Digit Positions (Optimal)
1Hint 1Explore hintHide hint
Each digit maps to its own small set of letters, and a combination uses one letter from each digit in order. What familiar structure is 'one choice from each of several independent sets'?
2Hint 2Explore hintHide hint
Walk the digits left to right with an `index`. At each level, loop over the letters of `digits[index]`, append one, recurse to `index + 1`, then pop.
3Hint 3Explore hintHide hint
Record the built string when `index` reaches the end. Don't forget: if the input is empty, return [] before you even start.
A · Core idea
Treat the digit string as a sequence of independent positions. Walk it with an index; at each call, look up the letters for digits[index], and for each such letter: choose it (append to the path), explore by recursing to index + 1, then un-choose (pop). When index == len(digits) the path holds one letter per digit, so we record the built string.
Why this is a Cartesian product, and why that matters. Each digit offers a fixed alphabet that doesn't depend on what we picked for earlier digits. So the set of answers is exactly {one letter from digit 0} × {one letter from digit 1} × … — the Cartesian product of the per-digit sets. Recognising this tells you two things instantly: there's no pruning (every combination of choices is valid), and the count is the product of the set sizes (3 or 4 each). The recursion is just a systematic enumeration of that product.
Why we advance by digit index, not a start index. In Subsets/Combinations a start index moves through one shared set, skipping forward to avoid duplicates. Here each position has its own set, and we must use every digit exactly once in order. So the recursion variable is the digit index, and we always go to index + 1 after choosing — never reusing a digit, never skipping one. The 'choice-set' changes from level to level (digit 2's letters, then digit 3's), which is the structural difference from single-set problems.
Why the empty-input guard is essential. If digits is empty, the correct answer is [] — there are no digits, so there are no letter combinations. But a naive backtracking that starts by recording at index == 0 == len(digits) would record the empty string and return [""], a list containing one (empty) combination. That's semantically wrong, and it's the single most common bug on this problem, so we special-case empty input up front.
Why a copy/built string at the leaf. The path is a mutable buffer (a StringBuilder in Java, a list in Python) shared across the whole tree. At a leaf we materialise it with toString() / ''.join(path), which produces a fresh immutable string — safe to store. The subsequent pop()/deleteCharAt then rewinds the buffer for sibling branches without affecting the strings we already saved.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
digits = "23"Interactive diagram
Choose / explore / un-choose for "23"
index=0Level 0 draws from {a,b,c}, level 1 from {d,e,f}. Every leaf is a valid combination — no branch is ever pruned.
["ad","ae","af","bd","be","bf","cd","ce","cf"]C · Code with teaching comments
D · Why it works and complexityO(n · 4ⁿ) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
The 'one choice-set per position' shape is the template for any Cartesian-product enumeration: dice rolls, license-plate generators, expanding a grammar's alternatives. The only moving part versus Subsets is that the loop's alphabet depends on the current position, and you advance to the next position unconditionally instead of using a start index to dodge duplicates.
ComplexityO(n · 4ⁿ) time, O(n) space — Let n be the number of digits. Each digit branches into up to 4 letters, so there are up to 4ⁿ leaves; building each combination string costs O(n). Total time is O(n · 4ⁿ). Extra space beyond the output is the recursion depth and the path buffer, both O(n).
E · Edge cases and pitfalls6digits = "" — Caught by the guard → returns [] immediately, never entering the recursion (avoids the [""] bug).Explore edge casesHide edge cases
Edge cases handled
digits = ""Caught by the guard → returns [] immediately, never entering the recursion (avoids the [""] bug).digits = "79"7→pqrs (4 letters), 9→wxyz (4 letters) → 4 × 4 = 16 combinations. Confirms the 4ⁿ branching, not 3ⁿ.
Pitfalls
- Returning [""] for empty input instead of [] — the #1 bug on this problem.
- Forgetting the 4-letter alphabets for 7 (pqrs) and 9 (wxyz).
- Using a start index as if it were one shared set — each digit is its own position with its own alphabet.
- Storing a mutable buffer reference instead of a built string at the leaf.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic List<String> letterCombinations(String digits) { List<String> results = new ArrayList<>(); if (digits == null || digits.isEmpty()) return results; // empty input → [] String[] map = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; backtrack(digits, 0, map, new StringBuilder(), results);Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Returning [""] for empty input
With no digits there are no combinations, so the answer is []. A backtracking that records at index 0 (== len) for empty input emits the empty string, giving [""] — semantically wrong. Guard up front.
Explore the code comparisonHide the code comparison
Returning [""] for empty input
With no digits there are no combinations, so the answer is []. A backtracking that records at index 0 (== len) for empty input emits the empty string, giving [""] — semantically wrong. Guard up front.
Don't write this
def letterCombinations(self, digits):
results = []
backtrack(0, []) # no guard → records "" and returns [""]
return resultsWrite this instead
def letterCombinations(self, digits):
if not digits:
return [] # guard the empty input
results = []
backtrack(0, [])
return results2Wrong alphabet for 7 and 9
Most digits map to 3 letters, but 7→pqrs and 9→wxyz have 4. A 3-letter-everywhere map silently drops 's' and 'z' from the output.
Explore the code comparisonHide the code comparison
Wrong alphabet for 7 and 9
Most digits map to 3 letters, but 7→pqrs and 9→wxyz have 4. A 3-letter-everywhere map silently drops 's' and 'z' from the output.
Don't write this
mapping = {"7": "prs", "9": "wxy"} # missing 's'/'z' tails!Write this instead
mapping = {"7": "pqrs", "9": "wxyz"} # four letters each3Using a start index instead of digit index
A start index belongs to single-set problems (Subsets) where you skip forward to dedup. Here each digit is a distinct position with its own alphabet, so you advance by index and always go to index + 1.
Explore the code comparisonHide the code comparison
Using a start index instead of digit index
A start index belongs to single-set problems (Subsets) where you skip forward to dedup. Here each digit is a distinct position with its own alphabet, so you advance by index and always go to index + 1.
Don't write this
for i in range(start, len(digits)): # wrong model — one shared set
for letter in mapping[digits[i]]:
...Write this instead
for letter in mapping[digits[index]]: # current digit's alphabet
path.append(letter)
backtrack(index + 1, path)
path.pop()Revise
Carry the pattern into the interview
Pattern to remember
Backtracking over a fixed sequence of choice-sets (Cartesian product)
Code pattern to memorize
def backtrack(index, path):
if index == len(digits): results.append(''.join(path)); return
for letter in mapping[digits[index]]: # one choice-set per digit
path.append(letter) # choose
backtrack(index + 1, path) # explore the next digit
path.pop() # un-chooseRules to remember
- 1Advance by digit index, not by a start index — every digit contributes exactly one letter, in order.
- 2The loop is over the current digit's letters; recursion always goes to
index + 1(never reuse the digit). - 3Record only at a complete leaf (
index == len(digits)) — a combination must use all digits. - 4Guard the empty input:
digits == ""must return[], not[""]— the most common edge-case miss. - 5Use a
StringBuilder/list as the path and append a built copy (toString()/''.join) at the leaf. - 6Digits 7 and 9 map to 4 letters (pqrs, wxyz); the rest map to 3 — that's why the bound is 4ⁿ, not 3ⁿ.
Use this pattern when
- Generate the **Cartesian product** of several independent choice-sets
- Each position draws from its own fixed alphabet (digit → letters)
- You need all combined strings/tuples, not just a count
- The number of positions is small (digit string is short)
Choose another pattern when
- Only the **count** of combinations is needed → multiply the choice-set sizes, no enumeration
- Choices depend on previous picks (a constraint links positions) → it's not a plain product
- The same set is reused and order doesn't matter → that's combinations/subsets, not a product
- n is large → 4ⁿ explodes; full enumeration is infeasible
Interview takeaway
This is the 'Cartesian product' member of the backtracking family — the cleanest one, because there's no pruning and no dedup, just one choice-set per position. Master the index-advancing choose/explore/un-choose here and you can generate any product (Combinations of well-formed parentheses, generate-all-from-grammar) with the same skeleton.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternCanonical Backtracking for a Cartesian product (choose → explore → un-choose) advancing by position with a per-position choice-set. Recognise it whenever each position draws independently from its own fixed alphabet and you need every combined string. The skeleton matches Subsets; the differences are the per-digit alphabet and unconditional advance to index + 1 (no start index, no pruning).
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Generate ParenthesesComing soonAlso builds all strings via backtracking, but with a constraint linking choices (open/close counts) — so it prunes, unlike this pure product.
- Combination SumSingle reusable set under a sum target → start-index template with pruning, contrasting with this fixed-position product.
- SubsetsBinary in/out per element with a start index — same backtracking skeleton but one shared set and no per-position alphabet.
- PermutationsOrder-matters enumeration of one set with a used[] tracker — different bookkeeping, same choose/explore/un-choose core.