Problem overview
The method letterCombinations receives digits, a string of at most four characters, each from '2' to '9'. Every digit stands for the letters on its phone key: 2 is abc, 3 is def, and so on up to 9 as wxyz, with four letters on keys 7 and 9. The method returns a List[String] with every string that picks one letter for each digit, in the order of the digits.
The order matters: the results must come in keypad order. Every combination that starts with the first letter of the first key comes before those that start with its second letter, and the same rule applies to the next digit inside each group. Since each key's letters are alphabetical, this is also alphabetical order.
For "23" the answer is List(ad, ae, af, bd, be, bf, cd, ce, cf): three letters for 2 times three for 3 gives nine strings. For "7" it is List(p, q, r, s). For the empty string "" it is an empty list: there is nothing to spell, so there are no combinations, not even an empty one.
Approach: choose one letter per digit
The number of combinations is the product of the key sizes, so the loops you would write by hand depend on how many digits there are: two nested loops for two digits, three for three. A recursive method avoids that. It handles one digit and calls itself for the next one, so the depth of the recursion is the number of digits.
This is backtracking. A call at position i tries each letter of the key of digits(i), in the key's order, adds the letter to the string chosen so far, and moves to position i + 1. When i reaches digits.length, every digit has its letter, and the string is a finished combination. Trying the letters in key order produces keypad order with no sorting.
In many backtracking problems the chosen items are removed again after the recursive call. Here nothing needs undoing: a Scala String never changes, so current + letter creates a new string, and the caller's current stays as it was for its next letter.
Step by step
- Create
result, an emptyArrayBuffer[String]()that collects the combinations. - Handle empty input. If
digits.isEmpty, returnresult.toListright away. Without this check, the search would reach the end of the digits immediately and record the empty string as a combination. - Build
keypadas anArrayof strings indexed by digit: two empty strings for the digits 0 and 1, which have no letters, then"abc"up to"wxyz". With this layout the letters of digitdarekeypad(d). - Start the search with
backtrack(digits, 0, "", keypad, result): position 0, nothing chosen yet. - Return
result.toList, sincebacktrackfilled the same buffer. - Write the helper
backtrack, taking the digits, the positioni, the letters chosen so far incurrent, the keypad and the result buffer, and returningUnit. - Stop at the end: when
i == digits.length, every digit has a letter. - Record and return. Add
currenttoresultandreturn. The string is immutable, so it can be stored as it is, with no copy. - Look up the letters of this digit with
digits(i) - '0'. - Loop over the letters with
letter <- letters, in the key's order. - Recurse with
i + 1andcurrent + letter.
The solution in Scala
class Solution {
def letterCombinations(digits: String): List[String] = {
val result = ArrayBuffer[String]()
if (digits.isEmpty) {
return result.toList
}
val keypad = Array("", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz")
backtrack(digits, 0, "", keypad, result)
result.toList
}
def backtrack(digits: String, i: Int, current: String, keypad: Array[String], result: ArrayBuffer[String]): Unit = {
if (i == digits.length) {
result += current
return
}
val letters = keypad(digits(i) - '0')
for (letter <- letters) {
backtrack(digits, i + 1, current + letter, keypad, result)
}
}
}
Scala details worth a look:
digits(i) - '0'subtracts oneCharfrom another, which gives anInt. For'7'the result is7, the index of"pqrs"inkeypad. Converting the character withtoIntwould not do:'7'.toIntis the character code,55.- A
forover aStringvisits its characters, soletteris aChar.current + letterappends the character and gives a newString. ArrayversusArrayBuffer. The keypad never changes size, so a fixedArrayfits. The result grows as combinations are found, so it is anArrayBuffer, converted to aListat the end.- Shared buffer.
resultis passed to every call, and each call adds to the same buffer. That is why the helper returnsUnitand the top-level method returns the buffer's contents after the search. returnin the helper ends the call early. It could be written withoutreturnby putting the loop in anelsebranch; both do the same.
Complexity
Time: O(n * 4^n) for n digits. A key has at most four letters, so there are at most 4^n combinations, and building each takes n steps of string appending.
Space: O(n) beyond the answer. The recursion is n calls deep, and each holds a string of at most n letters.
Edge cases and common mistakes
- Empty input. The answer must have no combinations. Without the
isEmptycheck, the search records one empty string, so the result isList("")with size 1 instead of an empty list. - A single digit.
"7"gives the four letters of its key as four one-letter strings. - Keys of different sizes. Digits 7 and 9 have four letters. For
"79"the answer has 16 strings, frompwtosz. For"2345"it has 81, fromadgjtocfil. - Wrong keypad indexing. Leaving out the two placeholders at the start shifts every key by two: the index
digits(i) - '0'then points to the wrong letters. - Looping over the digits instead of recursing. A fixed number of nested loops only works for one length.
Testing
"23"givesList(ad, ae, af, bd, be, bf, cd, ce, cf).""gives an empty list."7"givesList(p, q, r, s)."79"gives 16 strings, starting withpw, px, py, pz, qwand ending withsz."2345"gives 81 strings, starting withadgj, adgk, adgland ending withcfil.
