Problem overview
The method groupAnagrams receives a slice of strings, strs, and returns a slice of groups, where every group holds the words that are anagrams of each other: the same letters, rearranged. The groups may come back in any order.
For ["eat", "tea", "tan", "ate", "nat", "bat"] the groups are eat, tea, ate, then tan, nat, then bat. The example in the statement lists them in a different order, which is fine.
The budget is O(n * L * log L) time and O(n * L) extra space, where n is the number of words and L is the length of the longest word.
Approach: use the sorted word as a key
Comparing every pair of words to see whether they are anagrams would take about n² comparisons. The faster idea is to give every word a label that is the same for all its anagrams and different for everything else. Sorting the letters does exactly that: eat, tea and ate all become aet, while tan and nat become ant.
A map from that label to the list of words that produced it does the grouping. Each word is sorted once, which costs O(L log L), and inserted in the map in about O(L) for hashing the key. After one pass over all words, each map value is a finished group.
Step by step
- Declare the map.
groupBySortedmaps a sorted string to a slice of the original words. An empty map literal is ready to use. - Loop over the words. The index is not needed, so it is discarded with
_. - Copy the word into a byte slice. A Go string can't be modified in place, so the letters are copied into
charsto be sorted. - Sort the bytes and turn them back into a string.
sort.Slicetakes the slice and a function that says whether the element at indexigoes before the one atj. Convertingcharsto a string makes the key, since slices can't be map keys but strings can. - Add the original word to its group. Append
s(not the sorted text) under the key and store the result back in the map. - Collect the groups. Make an empty
[][]stringcalledresultand loop over the map's values, appending each group. - Return
result.
The solution in Go
func (sol Solution) groupAnagrams(strs []string) [][]string {
groupBySorted := map[string][]string{}
for _, s := range strs {
chars := []byte(s)
sort.Slice(chars, func(i, j int) bool { return chars[i] < chars[j] })
sortedString := string(chars)
groupBySorted[sortedString] = append(groupBySorted[sortedString], s)
}
result := [][]string{}
for _, words := range groupBySorted {
result = append(result, words)
}
return result
}
Go details in this code
- The method on
Solution.(sol Solution)is the receiver:groupAnagramsis attached to theSolutiontype, and the receiver isn't used in the body. The code also needs thesortpackage imported. - Strings are immutable.
[]byte(s)makes a copy, so sortingcharsnever changess, which is why the original word can still be appended later. - Bytes, not letters. A string indexes as bytes, so this sorts bytes. That is exactly right for ASCII words such as the lowercase English ones in this problem, where one letter is one byte.
sort.Sliceneeds a less function, and it sorts in place. It returns nothing, so there is no result to assign.- Appending to a missing key works. Reading a missing key from a map gives the zero value, and for a slice that is
nil.append(nil, s)creates a new slice with one element. The result ofappendmust be stored back, because the slice may have been reallocated. - Map iteration order is random. The groups come out in a different order from run to run, and so does the order of the groups in the result. That is allowed here, since the problem accepts any order. The words inside a group keep the order of the input.
Complexity
Time: O(n * L * log L). Each of the n words is copied in O(L), sorted in O(L log L), converted to a string in O(L), and hashed in O(L). Sorting dominates. Collecting the groups takes O(n).
Space: O(n * L). The map stores a key of length up to L per distinct group and keeps every original word in some group.
Edge cases and common mistakes
- Empty input. A nil or empty
strsgives an empty result. Becauseresultis created as[][]string{}, it is an empty slice rather thannil. - The empty string. Its sorted form is also the empty string, so
[""]gives one group containing the empty word. - Words that are the same. Duplicates such as
"abc"twice land in the same group, once each. - Same letters, different counts.
"aab"and"ab"sort toaabandab, which differ, so they are correctly kept apart. - Appending the sorted text. The groups must contain the original words, not their keys.
- Expecting a fixed order. Comparing the result to a hard-coded order of groups fails at random because of map iteration. Compare groups regardless of their order.
- Forgetting to assign
append. Callingappendwithout storing its result discards the new element.
Testing
["eat", "tea", "tan", "ate", "nat", "bat"]gives three groups:eat, tea, ate;tan, nat;bat.[""]gives one group with one empty string.[]gives no groups.