LeetCode #17 โ€” Medium

Letter Combinations of
a Phone Number

A complete visual walkthrough of the backtracking approach โ€” from phone keypad to recursive tree.

Patterns Used

Roadmap

  1. The Phone Keypad Mapping
  2. Brute Force โ€” Why Simple Loops Fail
  3. The Core Insight โ€” Backtracking
  4. Algorithm Walkthrough
  5. Edge Cases
  6. Full Annotated Code
  7. Interactive Demo
  8. Complexity Analysis
Step 01

The Phone Keypad Mapping

Each digit on a phone maps to a set of letters. Given a string of digits, we must return every possible letter combination those digits could represent.

Phone Keypad

1  
2 abc
3 def
4 ghi
5 jkl
6 mno
7 pqrs
8 tuv
9 wxyz

Digits 2-9 each map to 3 or 4 letters. Digit 1 maps to nothing.

Example

Input
digits = "23"
โ†’
Output
ad ae af bd be bf cd ce cf

2 โ†’ abc, 3 โ†’ def. Every letter from "2" paired with every letter from "3" gives 3 ร— 3 = 9 combinations.

Step 02

Brute Force โ€” Why Simple Loops Fail

For two digits, we could write a double-nested loop. For three digits, a triple-nested loop. But the number of digits is variable (0 to 4), so we cannot hardcode the nesting depth.

The Nesting Problem

// For digits = "23" โ€” works fine with 2 loops
for (char c1 : "abc".toCharArray())
    for (char c2 : "def".toCharArray())
        result.add("" + c1 + c2);

// For digits = "234" โ€” need 3 loops
for (char c1 : "abc".toCharArray())
    for (char c2 : "def".toCharArray())
        for (char c3 : "ghi".toCharArray())
            result.add("" + c1 + c2 + c3);

// For digits = "2345" โ€” need 4 loops...
// How many loops for n digits? We can't know at compile time!
๐Ÿ’ก

The problem: We need a variable number of nested loops, one per digit. This is a classic signal that we need recursion. Each recursive call acts as one "level" of nesting, and the recursion depth adapts to the input size automatically.

Step 03

The Core Insight โ€” Backtracking

We build each combination one character at a time. At each digit position, we try every letter that digit maps to, recurse deeper for the next digit, then undo our choice (backtrack) before trying the next letter.

The Backtracking Pattern

1. Choose
Pick a letter for the current digit and append it to the path.
2. Explore
Recurse to the next digit position. If we've filled all positions, record the combination.
3. Un-choose (Backtrack)
Remove the last character so we can try the next letter at this position.

This creates a decision tree where each level corresponds to a digit, and each branch to a letter choice. For digits = "23":

Decision Tree for "23"

a b c d e f d e f d e f "" a b c ad ae af bd be bf cd ce cf digit 2 digit 3 result

Each path from root to leaf is one complete combination. The tree has 3 ร— 3 = 9 leaves = 9 combinations.

โšก

Backtracking is DFS on this decision tree. We go deep-first: build "a" โ†’ "ad" (record it), backtrack to "a" โ†’ "ae" (record it), backtrack to "a" โ†’ "af" (record it), then backtrack all the way to root and try "b". The StringBuilder.deleteCharAt call is the "undo" step.

Step 04

Algorithm Walkthrough

Let's trace every recursive call for digits = "23". Watch the path build up and get undone:

Trace for digits = "23"

call backtrack(index=0, path="") letters for '2' = "abc"
append 'a' โ†’ path = "a"
call backtrack(index=1, path="a") letters for '3' = "def"
append 'd' โ†’ path = "ad"
index == length โ†’ result.add("ad")
delete 'd' โ†’ path = "a" (backtrack)
append 'e' โ†’ path = "ae"
index == length โ†’ result.add("ae")
delete 'e' โ†’ path = "a" (backtrack)
append 'f' โ†’ path = "af"
index == length โ†’ result.add("af")
delete 'f' โ†’ path = "a" (backtrack)
delete 'a' โ†’ path = "" (backtrack to root)
append 'b' โ†’ path = "b" ... produces "bd", "be", "bf"
append 'c' โ†’ path = "c" ... produces "cd", "ce", "cf"
result = ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]
๐ŸŽฏ

The StringBuilder acts as a shared, mutable path. We append before recursing and delete after returning. This is more efficient than creating new String objects at each level, and it is the standard backtracking pattern in Java.

Step 05

Edge Cases

The problem has a few important boundary conditions to handle:

Empty Input
digits = ""
Output: [] (empty list, not [""])
Single Digit
digits = "2"
Output: ["a", "b", "c"]
4-Letter Digits
digits = "79"
4 ร— 4 = 16 combinations (pqrs ร— wxyz)
Digit "1"
digits = "1" (not in constraints)
Maps to no letters โ€” loop body never executes, producing nothing.
๐Ÿ“

The empty-check guard is critical. Without if (digits.isEmpty()) return result, we would add an empty string "" to the result when the input is empty. The problem specifies we should return an empty list, not a list containing one empty string.

Step 06

Full Annotated Code

class Solution {

    // Index 0 and 1 are empty โ€” digits 0 and 1 have no letters
    private static final String[] MAPPING = {
        "", "", "abc", "def", "ghi", "jkl",
        "mno", "pqrs", "tuv", "wxyz"
    };

    public List<String> letterCombinations(String digits) {
        List<String> result = new ArrayList<>();

        // Guard: empty input returns empty list (not [""])
        if (digits.isEmpty()) return result;

        // Start backtracking from digit index 0
        backtrack(digits, 0, new StringBuilder(), result);
        return result;
    }

    private void backtrack(String digits, int index,
                           StringBuilder path, List<String> result) {

        // Base case: built a complete combination
        if (index == digits.length()) {
            result.add(path.toString());   // snapshot the path
            return;
        }

        // Get letters for current digit
        String letters = MAPPING[digits.charAt(index) - '0'];

        for (char c : letters.toCharArray()) {
            path.append(c);                  // 1. Choose
            backtrack(digits, index + 1,     // 2. Explore
                      path, result);
            path.deleteCharAt(                // 3. Un-choose
                path.length() - 1);
        }
    }
}
Step 07

Interactive Demo

Enter digits and watch the backtracking tree build up step by step. Each step shows the algorithm choosing a letter, exploring deeper, or backtracking.

โš™ Backtracking Visualizer


Found:
Step 08

Complexity Analysis

Time
O(4^n ยท n)
Space
O(n)

Why O(4^n ยท n)?

Each digit maps to at most 4 letters (digits 7 and 9). With n digits, the tree has at most 4^n leaves (complete combinations). At each leaf we call path.toString() which copies n characters, giving us the extra factor of n.

Why O(n) Space?

The recursion depth is n (one frame per digit). The StringBuilder path also holds at most n characters. We do not count the output list in auxiliary space โ€” it is required to store the answer.

๐Ÿ“

In practice, n is at most 4 (constraint: 0 โ‰ค digits.length โ‰ค 4). So the maximum number of combinations is 4^4 = 256. The asymptotic analysis matters for understanding the algorithm, but this problem's constraints make it trivially fast for any approach.