Topics › Recursion and Backtracking › Subsequences Pattern
Letter Combinations of a Phone Number
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given a string digits containing digits from 2 to 9. On a classic phone keypad each of these digits carries letters: 2 → abc, 3 → def, 4 → ghi, 5 → jkl, 6 → mno, 7 → pqrs, 8 → tuv, 9 → wxyz (0 and 1 carry none). Return every string that could be typed by choosing one letter for each digit, in order. If digits is empty, return an empty list, not a list containing the empty string. Any output order is accepted.
The problem is a Cartesian product: the answer is every way of picking one element from each of n small sets, so the size of the answer is the product of the set sizes, between 3^n and 4^n.
Constraints: 0 ≤ digits.length ≤ 4, every character is in '2'..'9'.
Example 1
digits = "23"
answer = ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]
// 3 letters for 2 times 3 letters for 3 = 9 strings
Example 2
digits = ""
answer = []
Example 3
digits = "9"
answer = ["w", "x", "y", "z"] // a four-letter key
The letters live in a lookup table indexed by the digit itself, which is shared by all three methods:
static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
Approach 1: Iterative cascade
Intuition. Build the answer digit by digit. Before reading any digit there is exactly one partial string, the empty one. Reading a digit with letters L turns every partial string p into |L| longer strings p + letter. After the last digit the partial strings are complete. This is breadth-first: at each step the whole current list is replaced by the next, larger list. It is easy to reason about and needs no recursion, at the cost of holding every intermediate list in memory and creating a new String object for every extension.
Algorithm.
- If
digitsis empty, return an empty list. - Start with
out = [""]. - For each digit: create
next; for eachprefixinoutand eachletterof the digit, appendprefix + lettertonext; then setout = next. - Return
out.
static List<String> iterative(String digits) {
List<String> out = new ArrayList<>();
if (digits.isEmpty()) return out;
out.add(""); // one empty prefix to start from
for (char ch : digits.toCharArray()) {
String letters = KEYS[ch - '0'];
List<String> next = new ArrayList<>(out.size() * letters.length());
for (String prefix : out) {
for (char letter : letters.toCharArray()) next.add(prefix + letter);
}
out = next;
}
return out;
}
Complexity. Time O(4^n · n): the final list has up to 4^n strings of length n, and the total size of the intermediate lists is dominated by the last one (a geometric series). Space O(4^n · n) for the output; the previous level is garbage once replaced.
Approach 2: Optimal, backtracking with a shared StringBuilder
Intuition. Depth-first instead of breadth-first. Level i of the recursion is responsible for digit i: it tries each letter of that digit in turn, appends it to a single shared StringBuilder, recurses to decide the remaining digits, then removes the letter so the next one can be tried. When i reaches the end, the builder holds one complete combination, which we copy out. Only one partial string exists at a time, so the extra memory is the recursion depth n plus the builder, and the only allocations are the 4^n answer strings themselves.
This is the “for-loop recursion” shape of the subsequence pattern with a twist: every level must choose exactly one option (no “not pick” branch), because every digit must be typed.
Algorithm.
- If
digitsis empty, return an empty list. backtrack(i, cur): ifi == digits.length(), addcur.toString()and return.- For each letter of
KEYS[digits[i]]: append it, recurse withi + 1, delete the last character.
static List<String> optimal(String digits) {
List<String> out = new ArrayList<>();
if (digits.isEmpty()) return out;
backtrack(digits, 0, new StringBuilder(), out);
return out;
}
static void backtrack(String digits, int i, StringBuilder cur, List<String> out) {
if (i == digits.length()) {
out.add(cur.toString());
return;
}
String letters = KEYS[digits.charAt(i) - '0'];
for (int j = 0; j < letters.length(); j++) {
cur.append(letters.charAt(j));
backtrack(digits, i + 1, cur, out);
cur.deleteCharAt(cur.length() - 1); // undo before trying the next letter
}
}
Dry run on digits = "23":
| level i | cur before | letter tried | result |
|---|---|---|---|
| 0 | ”” | a | recurse |
| 1 | “a” | d, e, f | record “ad”, “ae”, “af” (undo after each) |
| 0 | ”” | b | recurse |
| 1 | “b” | d, e, f | record “bd”, “be”, “bf” |
| 0 | ”” | c | recurse |
| 1 | “c” | d, e, f | record “cd”, “ce”, “cf” |
The output order is lexicographic in the keypad order of letters, the same order the cascade produces.
Complexity. Time O(4^n · n): 4^n leaves each copying an n-character string; the internal nodes are fewer than the leaves. Space O(n) for the recursion stack and the builder, beyond the output.
Approach 3: Mixed-radix decoding
Intuition. Number the combinations from 0 to total - 1, where total is the product of the letter counts. A combination index is then a number written in a mixed radix: the last digit’s position has radix |L_last|, the one before it radix |L_{n-2}|, and so on, like an odometer whose wheels have different numbers of positions. Repeated % radix and / radix extracts the wheel positions from the least significant end, and each position selects a letter. The approach produces the t-th combination directly without visiting any other, which is handy when only some combinations are needed (for example, in pages) or when recursion is unwelcome.
Algorithm.
- If
digitsis empty, return an empty list. total = Π |KEYS[digit]|over the digits.- For
tin0 .. total - 1: letrest = t; for positionsi = n - 1down to0, setbuf[i] = letters_i[rest % |letters_i|]andrest /= |letters_i|; addnew String(buf).
static List<String> mixedRadix(String digits) {
List<String> out = new ArrayList<>();
if (digits.isEmpty()) return out;
int n = digits.length();
int total = 1;
for (int i = 0; i < n; i++) total *= KEYS[digits.charAt(i) - '0'].length();
char[] buf = new char[n];
for (int t = 0; t < total; t++) {
int rest = t;
for (int i = n - 1; i >= 0; i--) { // the last digit is the least significant position
String letters = KEYS[digits.charAt(i) - '0'];
buf[i] = letters.charAt(rest % letters.length());
rest /= letters.length();
}
out.add(new String(buf));
}
return out;
}
Decoding t = 100 for "7799" (radices 4, 4, 4, 4): 100 % 4 = 0 → w, 25 % 4 = 1 → x, 6 % 4 = 2 → r, 1 % 4 = 1 → q, giving "qrxw", which is indeed the 101st string the backtracking produces.
Complexity. Time O(4^n · n), n divisions per combination. Space O(n) for the buffer, beyond the output. Because the last digit is treated as least significant, the order matches the other two methods exactly, which the tests rely on.
Edge cases and pitfalls
- Empty input. The cascade would otherwise return
[""](the single empty prefix) and the mixed-radix loop would run once withtotal = 1; both methods, and the backtracking, guard with an explicit check and return[]. - Four-letter keys.
7and9have four letters. Hard-coding three letters per key breaks"79", which must produce 16 strings. - Digits 0 and 1. They are excluded by the constraints. If they do appear, every method returns an empty list, because an empty letter set makes the product empty.
- Shared builder. Forgetting
deleteCharAtafter the recursive call leaves the previous letter in place and produces strings that keep growing. Every append must be undone. - String concatenation cost.
prefix + letterin the cascade allocates a new string each time; that is acceptable here because the output itself is4^nstrings, but the builder approach avoids all intermediate garbage. - Order. The problem accepts any order, but all three methods agree on the same lexicographic-by-keypad order, which makes them easy to test against each other.
Related problems
- Generate Parentheses is another “choose one option per position” backtracking, with a validity constraint between positions.
- Generate Binary Strings Without Consecutive 1s chooses one of two symbols per position with a neighbour constraint.
- Power Set is the two-option-per-position product (in or out) that the subsequence pattern is built on.
- Permutations of a String enumerates arrangements rather than products, with the same append / undo discipline.
- Combination Sum shows the for-loop recursion with a target instead of a fixed length.