Problem overview
The method letterCombinations receives digits, a string of the digits 2 to 9, and returns every string that those digits could spell on a phone keypad. Each digit stands for the letters on its key (2 is abc, 3 is def, and so on up to 9, which is wxyz; 7 has pqrs). A combination takes one letter from each key, in the order of the digits.
The combinations must come in keypad order: all those starting with the first letter of the first key, then the second letter, and so on, with the same rule applied to the later digits. For "23" the result is ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]: three letters for 2 times three for 3 makes 9 strings. For "7" it is ["p", "q", "r", "s"]. An empty digits returns an empty array, not an array holding an empty string.
Approach: choose one letter per digit, then go deeper
The number of letters to pick depends on the length of digits, so a fixed number of nested loops can't do the job. Backtracking handles it with a function that calls itself once per digit.
The function keeps the position i of the digit it is spelling and the letters chosen so far in current. It tries each letter of that digit's key, in the key's order, and for each one calls itself for position i + 1 with the letter added to the end of current. When i reaches the number of digits, every digit has a letter and current is a finished combination, so it is saved.
Trying the letters in the key's order gives exactly the keypad order. For "23", the first letter a is tried first, and the whole subtree below it (ad, ae, af) is finished before b is tried.
In many backtracking problems a choice has to be undone after the recursive call. Here it doesn't: current + String(letter) builds a new string for each call, and current itself never changes, so the loop's next letter starts from the same prefix.
Step by step
- Create
result, an empty[String], for the finished combinations. - Return early for an empty
digitswithdigits.isEmpty. Without this check the search would reach "every digit has a letter" at once, with zero digits, and save the empty string, soletterCombinations("")would return[""]instead of[]. - Create the
keypadconstant, an array of strings indexed by digit: two empty strings for the digits 0 and 1, which have no letters, then"abc"up to"wxyz". The empty places keep each key at its own index, sokeypad[7]is"pqrs". - Start the search with
backtrack(Array(digits), 0, "", keypad, &result): position 0, nothing chosen yet.Array(digits)turns the string into an array ofCharactervalues that can be read by position. - Return
result. The helper has added to the same array, so it now holds everything. - Declare the helper
backtrackwith the parametersdigits,i,current,keypadandresult, the last one markedinoutso that the method can change the caller's array. - Check for the end:
if i == digits.count. - Save and return. Inside that
if, appendcurrenttoresultandreturn. No digit is left to spell. - Look up the key.
digits[i].wholeNumberValue!turns the character into its number, andkeypad[...]gives the letters of that key. - Loop over the letters of that key, in order.
- Recurse with
i + 1andcurrent + String(letter), passing&resultagain.
The solution in Swift
class Solution {
func letterCombinations(_ digits: String) -> [String] {
var result = [String]()
if digits.isEmpty {
return result
}
let keypad = ["", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"]
backtrack(Array(digits), 0, "", keypad, &result)
return result
}
func backtrack(_ digits: [Character], _ i: Int, _ current: String, _ keypad: [String], _ result: inout [String]) {
if i == digits.count {
result.append(current)
return
}
let letters = keypad[digits[i].wholeNumberValue!]
for letter in letters {
backtrack(digits, i + 1, current + String(letter), keypad, &result)
}
}
}
The Swift details worth a closer look:
- Strings can't be indexed with an
Int. Writingdigits[0]on aStringis a compile error: 'subscript(_:)' is unavailable: cannot subscript String with an Int, use a String.Index instead. Converting once withArray(digits)gives an array ofCharactervalues, anddigits[i]works on that array. wholeNumberValue. It is a property ofCharacterand returns an optionalInt: 7 for"7", andnilfor a character such as"x". The!unwraps it, which is safe here because the input holds only digits.inoutand&. Arrays are values and are copied when passed. Aninoutparameter lets the helper change the caller's array, and the caller marks the argument with&. Every recursive call receives the sameresultthis way.String(letter). Iterating over aStringproducesCharactervalues, and+joins strings, so each letter is converted before it is added tocurrent.- Values, not references. A
Stringis a value type. Appendingcurrenttoresultstores its own copy, and the strings built by other calls are never touched.
Complexity
Time: O(n · 4^n) for n digits. A key has at most 4 letters, so there are at most 4^n combinations, and building each one takes n steps. With the digits "9999" the method returns 256 strings, from "wwww" to "zzzz".
Space: O(n) besides the answer. The recursion is n calls deep, and each call holds a string of at most n letters.
Edge cases and common mistakes
- Empty input. Without the
isEmptycheck the method returns[""], one empty combination. The lesson expects[]. - One digit.
"7"returns the four letters of its key, and"2"returns three. The keys of 7 and 9 are the ones with four letters. - Counting results. The number of combinations is the product of the key sizes:
"79"gives 4 × 4 = 16 strings, from"pw"to"sz", and"234"gives 3 × 3 × 3 = 27, from"adg"to"cfi". - Index of the keypad. The array is indexed by the digit itself, which is why it has two empty strings at the start. If they were dropped, every key would be off by two.
Testing
"23"gives["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]."7"gives["p", "q", "r", "s"]."2"gives["a", "b", "c"].""gives[]."79"gives 16 strings, the first"pw"and the last"sz".
