Problem overview
The method groupAnagrams receives an array of strings and returns them grouped so that each group holds strings made of exactly the same letters. The order of the groups, and of the strings inside a group, doesn't matter. The return type is List[List[String]].
For ["eat", "tea", "tan", "ate", "nat", "bat"], the groups are eat, tea, ate, then tan, nat, then bat.
Approach: sort the letters, sort the words
Two strings are anagrams exactly when their letters, put in alphabetical order, give the same string. "eat", "tea" and "ate" all become "aet", while "tan" and "nat" become "ant". That sorted string is a key that is identical for every member of a group.
Now sort the array of words by that key. Words with the same key end up next to each other, so each group is one block of neighbours. A single pass can then cut the sorted array into blocks: keep adding words to the current group, and when the next word has a different key (or there is no next word), the group is complete.
With n words of length at most L, sorting one word's letters costs O(L log L). Sorting the array makes O(n log n) comparisons, and each comparison computes two keys, so the cost is O(n log n * L log L), the bound in the statement. Space is O(n * L) for the sorted copy and the groups. Later lessons replace this with a hash map.
Step by step
- Assume a helper
sorted(s)that returns the letters ofsin alphabetical order. It is written at the end. - Sort the words by key.
strings.sortBy(s => sorted(s))returns a new array in which anagrams are adjacent. The original array is unchanged. - Prepare the output.
resultis avalholding aListBufferof finished groups. A buffer can grow by appending, which aListcan't do cheaply. - Prepare the current group.
currentGroupis avarholding aListBuffer, because the variable will be pointed at a fresh buffer after every group. - Loop over the positions of the sorted array with
strs.indices. - Add the word to the current group. In sorted order it always belongs to the group being built.
- Detect the end of a group. The group ends when
iis the last index, or when the key ofstrs(i)differs from the key ofstrs(i + 1). The last-index test comes first:||doesn't evaluate its right side when the left side is true, sostrs(i + 1)is never read past the end. - Close the group. Append
currentGroup.toListtoresult, then start a new empty buffer. - Return
result.toList. - Write the helper. Copy the characters with
s.toCharArray, because a string is immutable and can't be sorted in place. Sort the copy withjava.util.Arrays.sort, then build a string from it withnew String(temp).
The solution in Scala
class Solution {
def sorted(s: String): String = {
val temp = s.toCharArray
java.util.Arrays.sort(temp)
new String(temp)
}
def groupAnagrams(strings: Array[String]): List[List[String]] = {
val strs = strings.sortBy(s => sorted(s))
val result = scala.collection.mutable.ListBuffer.empty[List[String]]
var currentGroup = scala.collection.mutable.ListBuffer.empty[String]
for (i <- strs.indices) {
currentGroup += strs(i)
if (i == strs.length - 1 || sorted(strs(i)) != sorted(strs(i + 1))) {
result += currentGroup.toList
currentGroup = scala.collection.mutable.ListBuffer.empty[String]
}
}
result.toList
}
}
The Scala details worth knowing:
- Scala uses Java's library.
toCharArrayandjava.util.Arrays.sortcome straight from Java, andnew String(temp)calls Java'sStringconstructor. Scala can call any Java class without a wrapper. - The helper's name. The method is called
sorted, like the collection method. Inside the class,sorted(s)with a string argument refers to this helper, whilestrings.sortBy(...)is unrelated to it. sortByreturns a new array. The result is a sorted copy, and it is stable: words with equal keys keep their input order. In the example, the group comes out aseat, tea, ate, the input order.==and!=compare string contents. There is no need forequals.strs.indicesis the range0 until strs.length.toListmakes an immutable copy. The group is copied into aListbefore the buffer is replaced, which matches the return type and means later changes to a buffer can't alter a finished group.- Last expression.
new String(temp)andresult.toListare the values of their methods; noreturnis written.
Complexity
Let n be the number of strings and L the length of the longest one.
Time: O(n * log n * L * log L). The sort does O(n log n) comparisons, and each recomputes two keys at O(L log L). The grouping pass computes two keys per index, which is O(n * L log L) and is already covered by the first term.
Space: O(n * L). The sorted array of references, the lists of strings and the temporary key arrays each stay within that bound.
Edge cases and common mistakes
- Empty array. The loop doesn't run and the result is an empty list.
- Empty strings.
[""]gives one group that holds the empty string. All empty strings share the key"". - Single words. A word with no anagram forms a group of one, like
bat. - Repeated words. Two equal strings are anagrams of each other and share a group.
- Case.
"Eat"and"tea"have different keys, because uppercase letters sort before lowercase ones. The comparison is exact. - Reading past the end. Testing
sorted(strs(i)) != sorted(strs(i + 1))beforei == strs.length - 1throws anArrayIndexOutOfBoundsExceptionon the last word. - Reusing one buffer. Skipping the line that creates a new
currentGroupwould keep adding later groups to the first one. - Sorting the words themselves. The output must contain the original words. The sorted letters are only the key.
Testing
["eat", "tea", "tan", "ate", "nat", "bat"]gives[[bat], [eat, tea, ate], [tan, nat]].[]gives[].[""]gives one group containing the empty string.["a"]gives[[a]].
