Group Anagrams
Anagrams share a key, either their sorted letters or their letter counts, so a hash map from key to group sorts every word in one pass. Solved two ways, a sorted key in O(n · k log k) and a count key in O(n · k), with a step-through, code in five languages and the Big O worked out from the code.
The problem
Section titled “The problem”You are given a list words. Call its length n. Put the words into groups, so that two words share a group
exactly when they are anagrams of each other. Return the groups.
Two words are anagrams when they use the same letters the same number of times, in any order. The groups, and the words inside each group, may come back in any order.
Example 1
Input: words = ["arc", "dog", "car", "god", "act"]
Output: [["arc", "car"], ["dog", "god"], ["act"]]
“arc” and “car” use a, c and r. “dog” and “god” use d, g and o. “act” has a t, so it stands alone.
Example 2
Input: words = ["", "b", ""]
Output: [["", ""], ["b"]]
The two empty words have the same letters (none), so they are anagrams of each other.
Example 3
Input: words = ["x"]
Output: [["x"]]
One word makes one group.
Constraints
Section titled “Constraints”- 1 ≤ n ≤ 10,000
- 0 ≤ words[i].length ≤ 100. Below, k is the length of the longest word.
- Every word holds only lowercase English letters.
Clarifying questions
Section titled “Clarifying questions”- Does the order of the groups matter?
- No. Any order of groups, and of words inside a group, is accepted.
- Can a word be empty?
- Yes. All empty words form one group together.
- Can the same word appear twice?
- Yes. Both copies go into the same group.
- Which letters can appear?
- Only a to z. That is why 26 counters are enough for the count key.
Spot the pattern
Section titled “Spot the pattern”We need to put together things that are “the same” under some rule. The trick is to turn each word into a key that is equal for all anagrams and different otherwise. Then a hash map from key to group does the grouping in one pass. That is the hashing pattern.
Solutions
Section titled “Solutions”This problem has two approaches. Comparing every word with every group also works, but it costs O(n² · k) and is no simpler than the sorted key.
Sorted key O(n · k log k)
The idea
Section titled “The idea”Sort the letters of each word. Anagrams hold the same letters, so they sort into the same string. Words that are not anagrams sort into different strings.
So the sorted word is a perfect key. Keep a hash map from key to a list of words. For each word, build its key and add the word to that key’s list. At the end, the lists are the groups.
Walkthrough
Section titled “Walkthrough”- groups = defaultdict(list)
- for word in words:
- key = "".join(sorted(word))
- groups[key].append(word)
- return list(groups.values())
Start with an empty hash map from key to group. Words that share a key are anagrams.
class Solution { // Sorted key: anagrams sort into the same string, so it can be a map key. // n words of length up to k: O(n * k log k) time, O(n * k) memory. public List<List<String>> groupAnagrams(String[] words) { Map<String, List<String>> groups = new HashMap<>(); for (String word : words) { // Two words are anagrams exactly when their sorted letters match. char[] letters = word.toCharArray(); Arrays.sort(letters); String key = new String(letters); groups.computeIfAbsent(key, newKey -> new ArrayList<>()).add(word); } return new ArrayList<>(groups.values()); }}from collections import defaultdict
# Sorted key: anagrams sort into the same string, so it can be a map key.# n words of length up to k: O(n * k log k) time, O(n * k) memory.def group_anagrams(words: list[str]) -> list[list[str]]: groups = defaultdict(list) for word in words: # Two words are anagrams exactly when their sorted letters match. key = "".join(sorted(word)) groups[key].append(word) return list(groups.values())public class Solution{ // Sorted key: anagrams sort into the same string, so it can be a map key. // n words of length up to k: O(n * k log k) time, O(n * k) memory. public IList<IList<string>> GroupAnagrams(string[] words) { var groups = new Dictionary<string, IList<string>>(); foreach (string word in words) { // Two words are anagrams exactly when their sorted letters match. char[] letters = word.ToCharArray(); Array.Sort(letters); string key = new string(letters); if (!groups.ContainsKey(key)) groups[key] = new List<string>(); groups[key].Add(word); } return groups.Values.ToList(); }}// Sorted key: anagrams sort into the same string, so it can be a map key.// n words of length up to k: O(n * k log k) time, O(n * k) memory.func groupAnagrams(words []string) [][]string { groups := make(map[string][]string) for _, word := range words { // Two words are anagrams exactly when their sorted letters match. letters := []byte(word) sort.Slice(letters, func(i, j int) bool { return letters[i] < letters[j] }) key := string(letters) groups[key] = append(groups[key], word) } result := make([][]string, 0, len(groups)) for _, group := range groups { result = append(result, group) } return result}// Sorted key: anagrams sort into the same string, so it can be a map key.// n words of length up to k: O(n * k log k) time, O(n * k) memory.function groupAnagrams(words: string[]): string[][] { const groups = new Map<string, string[]>(); for (const word of words) { // Two words are anagrams exactly when their sorted letters match. const key = [...word].sort().join(""); const group = groups.get(key); if (group) group.push(word); else groups.set(key, [word]); } return [...groups.values()];}1 Find the work that repeats
The loop runs once per word. Inside it, the sort is the costly line: sorting k letters takes about k × log₂ k steps. Adding the word to the map is about 1 step, plus hashing the key, which reads its k letters.
- groups = defaultdict(list)
- for word in words:
- key = "".join(sorted(word))
- groups[key].append(word)
- return list(groups.values())
2 Count the runs on Example 1
Example 1 has 5 words of 3 letters each. Each sort takes about 3 × log₂ 3 ≈ 5 steps. So the loop does about 5 × 5 ≈ 25 steps of sorting in total.
word arc dog car god act sort cost (k = 3) 3 × log₂ 3 ≈ 5 3 × log₂ 3 ≈ 5 3 × log₂ 3 ≈ 5 3 × log₂ 3 ≈ 5 3 × log₂ 3 ≈ 5 map insert 1 1 1 1 1 total5 words × about 5 steps ≈ 253 Write the count for any n
With n words of up to k letters, each word costs one sort plus reading its key once:
n × (k log k + k)Check it with Example 1: 5 × (about 5 + 3) ≈ 40 steps.
4 Keep only the part that grows fastest
- Drop the + k inside. For longer words, k log k grows faster than k, so the sort is the bigger part of each word’s cost.
What is left is n · k log k. Two sizes stay in the answer, because the number of words and their length can grow on their own.
So the time is O(n · k log k).
S Space: count the extra memory the same way
The map ends up holding every word once, plus one key per group. That is at most n × k letters in total. The space is O(n · k). The output itself needs that much anyway.
groups (all n words)key (k letters)word
Count key O(n · k)
The idea
Section titled “The idea”Sorting is more work than needed. Two words are anagrams exactly when each letter appears the same number of times in both. So count the letters instead: 26 counters, one pass over the word.
Use the list of 26 counts as the key. “arc” and “car” both give one a, one c and one r, so their keys match. “act” gives a t instead of an r, so its key differs.
Why it is correct: the count key is equal exactly when the letter counts are equal, and that is the definition of an anagram.
Walkthrough
Section titled “Walkthrough”- groups = defaultdict(list)
- for word in words:
- count = [0] * 26
- for letter in word:
- count[letter] += 1
- key = tuple(count)
- groups[key].append(word)
- return list(groups.values())
Start with an empty hash map from key to group. Words that share a key are anagrams.
class Solution { // Count key: anagrams have the same 26 letter counts, so use them as the key. // n words of length up to k: O(n * k) time, O(n * k) memory. public List<List<String>> groupAnagrams(String[] words) { Map<String, List<String>> groups = new HashMap<>(); for (String word : words) { // Counting is one pass over the word, with no sort. int[] count = new int[26]; for (char letter : word.toCharArray()) count[letter - 'a']++; // Turn the counts into a string, so it can be a map key. String key = Arrays.toString(count); groups.computeIfAbsent(key, newKey -> new ArrayList<>()).add(word); } return new ArrayList<>(groups.values()); }}from collections import defaultdict
# Count key: anagrams have the same 26 letter counts, so use them as the key.# n words of length up to k: O(n * k) time, O(n * k) memory.def group_anagrams(words: list[str]) -> list[list[str]]: groups = defaultdict(list) for word in words: # Counting is one pass over the word, with no sort. count = [0] * 26 for letter in word: count[ord(letter) - ord("a")] += 1 # A list cannot be a dict key, but a tuple can. groups[tuple(count)].append(word) return list(groups.values())public class Solution{ // Count key: anagrams have the same 26 letter counts, so use them as the key. // n words of length up to k: O(n * k) time, O(n * k) memory. public IList<IList<string>> GroupAnagrams(string[] words) { var groups = new Dictionary<string, IList<string>>(); foreach (string word in words) { // Counting is one pass over the word, with no sort. int[] count = new int[26]; foreach (char letter in word) count[letter - 'a']++; // Turn the counts into a string, so it can be a map key. string key = string.Join(",", count); if (!groups.ContainsKey(key)) groups[key] = new List<string>(); groups[key].Add(word); } return groups.Values.ToList(); }}// Count key: anagrams have the same 26 letter counts, so use them as the key.// n words of length up to k: O(n * k) time, O(n * k) memory.func groupAnagrams(words []string) [][]string { // A fixed-size array can be a map key in Go, so the counts are the key. groups := make(map[[26]int][]string) for _, word := range words { // Counting is one pass over the word, with no sort. var count [26]int for i := 0; i < len(word); i++ { count[word[i]-'a']++ } groups[count] = append(groups[count], word) } result := make([][]string, 0, len(groups)) for _, group := range groups { result = append(result, group) } return result}// Count key: anagrams have the same 26 letter counts, so use them as the key.// n words of length up to k: O(n * k) time, O(n * k) memory.function groupAnagrams(words: string[]): string[][] { const groups = new Map<string, string[]>(); for (const word of words) { // Counting is one pass over the word, with no sort. const count = new Array<number>(26).fill(0); for (const letter of word) count[letter.charCodeAt(0) - 97]++; // Turn the counts into a string, so it can be a map key. const key = count.join(","); const group = groups.get(key); if (group) group.push(word); else groups.set(key, [word]); } return [...groups.values()];}1 Find the work that repeats
There are two loops: one over the words, and one over the letters of each word. Each inner run adds 1 to one counter, which is 1 step. After that, building and hashing the key reads all 26 counts, which is 26 steps per word.
- groups = defaultdict(list)
- for word in words:
- count = [0] * 26
- for letter in word:
- count[letter] += 1
- key = tuple(count)
- groups[key].append(word)
- return list(groups.values())
2 Count the runs on Example 1
Example 1 has 5 words of 3 letters each. Each word costs 3 counting steps plus 26 steps for the key.
5 × (3 + 26) = 145 steps. For short words the 26 is the bigger part.
word arc dog car god act letters counted 3 3 3 3 3 key built 26 26 26 26 26 total5 × (3 + 26) = 1453 Write the count for any n
With n words of up to k letters:
n × (k + 26)Check it with Example 1: 5 × (3 + 26) = 145.
4 Keep only the part that grows fastest
- Drop the 26. It is a fixed number that never grows. As words get longer, k takes over.
What is left is n · k. That is the best possible: we have to read every letter of every word at least once.
So the time is O(n · k).
S Space: count the extra memory the same way
The map holds every word once, plus one 26-number key per group. That is O(n · k). The
countarray is 26 numbers and is reused for each word.groups (all n words)count (26 numbers)keywordletter
Tested at the limits
Section titled “Tested at the limits”2,000 random lists of 1 to 8 words, each up to 3 letters from a and b, empty words included, so many words land in the same group. The sorted key and the count key gave the same groups every time.
| Largest inputs | Result |
|---|---|
| 10,000 words of 100 letters (the most allowed), half of them shuffled copies of the other half, so 5,000 groups. Count key. | 0.05 s, and about 2 MB of extra memory: one key of 26 counts for each group, which is the O(n) space. |
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.