LeetCode #17 — Medium

Letter Combinations of
a Phone Number

A complete visual walkthrough of the backtracking approach — from phone keypad to recursive tree.

Solve on LeetCode
The Problem

Problem Statement

Given a string containing digits from 2-9 inclusive, return all possible letter combinations that the number could represent. Return the answer in any order.

A mapping of digits to letters (just like on the telephone buttons) is given below. Note that 1 does not map to any letters.

Example 1:

Input: digits = "23"
Output: ["ad","ae","af","bd","be","bf","cd","ce","cf"]

Example 2:

Input: digits = "2"
Output: ["a","b","c"]

Constraints:

  • 1 <= digits.length <= 4
  • digits[i] is a digit in the range ['2', '9'].
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

Make sure your solution handles these correctly before submitting:

Empty String Input

When digits = "", there are no digits to map and no combinations to form. The correct output is an empty array []. This must be checked first — if the algorithm seeds the result list with [""] before processing digits, an empty input would incorrectly return [""] instead of []. Guard with an upfront if (!digits.length) return []; check.

Single Digit

Input "2" should return ["a", "b", "c"] — exactly the three letters mapped to that key. There is only one level of the recursion tree, so no combining happens. The result size equals the number of letters on that key: three for most digits, four for 7 (pqrs) and 9 (wxyz).

Digits 0 and 1

Traditional phone keypads assign no letters to 0 and 1. The problem statement guarantees the input contains only digits 2–9, so you do not need to handle them. However, if your keypad map is an array indexed from '2', accessing index 0 - '2' or 1 - '2' would be out of bounds — keep this in mind if you extend the solution beyond the problem constraints.

Digits 7 and 9 Have Four Letters

Most keys map to three letters, but 7 maps to pqrs and 9 maps to wxyz. This means the combination count is not simply 3^n — it depends on which digits appear. An input of "79" produces 4 × 4 = 16 combinations, while "23" produces 3 × 3 = 9. Ensure your keypad map stores all four letters for these two keys.

Long Input — Combination Explosion

The output size grows exponentially: roughly 3^n to 4^n combinations for an input of length n. LeetCode caps the input at length 4, so the worst case is 4^4 = 256 combinations ("7999" or "9999"). In a real system with no length cap, this blows up fast — length 10 could yield over a million combinations. The algorithm's time complexity is inherently O(4^n) and cannot be improved; all combinations must be enumerated.

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(4n · n)
Space
O(4n · n)

Each digit maps to at most 4 letters (digits 7 and 9). With n digits, the decision tree has up to 4n leaves. At each leaf we copy n characters into the result, giving O(4n · n) time. The output itself stores 4n strings of length n, so space is also O(4n · n). Auxiliary space (recursion stack + path buffer) is only O(n).

Approach Breakdown

BRUTE FORCE
O(4n · n) time
O(4n · n) space

An iterative approach builds combinations by creating new strings at each digit level. For n digits with up to 4 letters each, this produces up to 4n combinations of length n, costing O(4n · n) for both string concatenation work and output storage.

OPTIMAL (BACKTRACKING)
O(4n · n) time
O(4n · n) space

Backtracking explores a decision tree of depth n where each node has up to 4 children. The tree has 4n leaves, and at each leaf we copy n characters into the result. Auxiliary space is only O(n) for the recursion stack and path buffer, but the output itself dominates at O(4n · n).

🎯

The output size IS the complexity. When the problem requires generating all possible combinations, there is no way to beat O(4n) — every combination must be produced. Backtracking is already optimal; the only difference between approaches is constant-factor overhead, not asymptotic class. In practice, n ≤ 4, so the maximum output is 44 = 256 strings.

Coach Notes

Common Mistakes

Review these before coding to avoid predictable interview regressions.

Missing undo step on backtrack

Wrong move: Mutable state leaks between branches.

Usually fails on: Later branches inherit selections from earlier branches.

Fix: Always revert state changes immediately after recursive call.