Group the words that are anagrams
You're given an array words of lowercase strings (some may be empty). I'd like you to
put the words into groups so that two words are in the same group exactly when one is an anagram of the other,
which means they use the same letters the same number of times.
So that there's only one right answer, return each group as the list of its indices in increasing order, and list the groups in the order of their first index.
Examples
| Input | Output | Why |
|---|---|---|
words = ["eat", "tea", "tan", "ate", "nat", "bat"] | [[0, 1, 3], [2, 4], [5]] | The eat, tea and ate group starts at index 0, then tan and nat start at 2, and bat is on its own. |
words = [""] | [[0]] | The empty word is still a word and it gets a group of its own. |
words = ["ab", "ba", "ab"] | [[0, 1, 2]] | A word repeated is an anagram of itself, so all three go in one group. |
Constraints
- 1 ≤ n ≤ 10⁴
- 0 ≤ words[i].length ≤ 100
- words[i] holds lowercase English letters only
Hints
Hint 1
Comparing every pair of words does work but it costs you O(n²) comparisons, and what you'd really like is a way for each word to find the group it belongs to straight away.
Hint 2
I'd think about what the words in one group all have in common with each other and how you might write that down as a single value that a hash map could use as its key.
Hint 3
Sorting the letters of a word gives you exactly such a key and so does the list of its 26 letter counts, and either of them can go into a hash map that sends each key to its group.
Where people lose marks
- I've seen people use an array of 26 counts as the key in a language that hashes arrays by identity, which is what JavaScript does with objects and Java does with int[], and then every word ends up in a group of its own. You'll want to turn the counts into a string before you use them as a key.
- If you build the counts key by gluing the numbers together with nothing between them then a count of 1 followed by 11 and a count of 11 followed by 1 both come out as 111, and you'll put two words in the same group when they aren't anagrams at all.
- It's easy to forget about the empty string, but its sorted key is just the empty string and it should get a group of its own like any other word you're given.
Dry run on ["eat","tea","tan","ate","nat","bat"]
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 8wordseat0tea1tan2ate3nat4bat5groups[]
Every word is turned into its letters in sorted order and a map from that key to the number of a group does the rest of the work as we go along.
-
Step 2 / 8wordseat0itea1tan2ate3nat4bat5key"aet"groups[0]
When we sort the letters of "eat" we get "aet" and since that key isn't in the map yet the word opens a new group with the number 0.
-
Step 3 / 8wordseat0tea1itan2ate3nat4bat5key"aet"groups[0, 1]
When we sort the letters of "tea" we get "aet" and the map already sends that key to group 0, so index 1 goes into the same group as the words before it.
-
Step 4 / 8wordseat0tea1tan2iate3nat4bat5key"ant"groups[0, 1] [2]
When we sort the letters of "tan" we get "ant" and since that key isn't in the map yet the word opens a new group with the number 1.
-
Step 5 / 8wordseat0tea1tan2ate3inat4bat5key"aet"groups[0, 1, 3] [2]
When we sort the letters of "ate" we get "aet" and the map already sends that key to group 0, so index 3 goes into the same group as the words before it.
-
Step 6 / 8wordseat0tea1tan2ate3nat4ibat5key"ant"groups[0, 1, 3] [2, 4]
When we sort the letters of "nat" we get "ant" and the map already sends that key to group 1, so index 4 goes into the same group as the words before it.
-
Step 7 / 8wordseat0tea1tan2ate3nat4bat5ikey"abt"groups[0, 1, 3] [2, 4] [5]
When we sort the letters of "bat" we get "abt" and since that key isn't in the map yet the word opens a new group with the number 2.
-
Step 8 / 8wordseat0tea1tan2ate3nat4bat5answer[0, 1, 3] [2, 4] [5]
After one pass through the words we have 3 groups and they're already in the order of their first index, so we can hand them back as they are.
How to think about it
You'll often see this one called Group Anagrams and I'd start by asking yourself what two anagrams really have in common. All they share is how many times each letter turns up in them, so if you can turn every word into a canonical key that depends on those counts and on nothing else then the words with the same key are the ones that belong together.
There are two keys that people tend to use for this. If you sort the letters then "tea" turns into
"aet" and so do "eat" and "ate", which is quick to write and costs you
O(L log L) for a word of length L. If you'd rather not sort you can count the 26 letters instead, which takes O(L)
and pays off once the words get long, and you'll find that both keys give you exactly the same groups.
Once you've got a key you just walk through the words with a hash map that sends each key to the number of its group. A key you haven't met yet opens a new group at the end of the list and a key you've met before sends the index to the group it already has. Because the groups open in the order their first word turns up and the indices go in from left to right you'll find the answer is already in the order the question asked for.
Complexity. Time O(n · L log L) with sorted keys, O(n · L) with counts · Space O(n · L)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
def group_anagrams(words):
where = {} # key -> position in groups
groups = []
for i, w in enumerate(words):
key = ''.join(sorted(w)) # anagrams share this key
if key not in where:
where[key] = len(groups)
groups.append([])
groups[where[key]].append(i)
return groups#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
using namespace std;
vector<vector<int>> solve(const vector<string>& words) {
unordered_map<string, int> where; // key -> position in groups
vector<vector<int>> groups;
for (int i = 0; i < (int)words.size(); i++) {
string key = words[i];
sort(key.begin(), key.end()); // anagrams share this key
auto it = where.find(key);
if (it == where.end()) {
where[key] = groups.size();
groups.push_back({});
groups.back().push_back(i);
} else {
groups[it->second].push_back(i);
}
}
return groups;
}import java.util.*;
class Solution {
public int[][] groupAnagrams(String[] words) {
Map<String, Integer> where = new HashMap<>(); // key -> position in groups
List<List<Integer>> groups = new ArrayList<>();
for (int i = 0; i < words.length; i++) {
char[] c = words[i].toCharArray();
Arrays.sort(c); // anagrams share this key
String key = new String(c);
Integer g = where.get(key);
if (g == null) {
g = groups.size();
where.put(key, g);
groups.add(new ArrayList<>());
}
groups.get(g).add(i);
}
int[][] r = new int[groups.size()][];
for (int k = 0; k < r.length; k++)
r[k] = groups.get(k).stream().mapToInt(Integer::intValue).toArray();
return r;
}
}function groupAnagrams(words) {
const where = new Map(); // key -> position in groups
const groups = [];
words.forEach((w, i) => {
const key = [...w].sort().join(''); // anagrams share this key
if (!where.has(key)) { where.set(key, groups.length); groups.push([]); }
groups[where.get(key)].push(i);
});
return groups;
}use std::collections::HashMap;
fn solve(words: &[&str]) -> Vec<Vec<i64>> {
let mut where_: HashMap<Vec<u8>, usize> = HashMap::new(); // key -> position in groups
let mut groups: Vec<Vec<i64>> = Vec::new();
for (i, w) in words.iter().enumerate() {
let mut key = w.as_bytes().to_vec();
key.sort(); // anagrams share this key
let g = *where_.entry(key).or_insert_with(|| {
groups.push(Vec::new());
groups.len() - 1
});
groups[g].push(i as i64);
}
groups
}import Data.List (sort, sortOn)
import qualified Data.Map.Strict as M
-- Key each word by its sorted letters; each group keeps its indices in order, and the groups
-- are put in order of their first index at the end.
solve :: [String] -> [[Int]]
solve ws = map snd (sortOn fst [(head is, is) | is <- M.elems groups])
where
groups = M.fromListWith (flip (++)) [(sort w, [i]) | (i, w) <- zip [0 ..] ws]
Variants to try
- The words can hold any Unicode letters. (Sorted keys still work, but you'll need a map of counts in place of the 26-slot array.)
- Return only the largest group. (Keep the size of each group in the same map and track the largest as you go.)
- Find every place in a long text where an anagram of a short word starts. (That's a sliding window over counts, which the anagram-starts problem works through.)
Next problems
Are two words anagrams?
Two words are anagrams when every letter occurs equally often in both. One table of counts, added for one word and taken away for the other.
Open the problem → Medium15 minThe k most frequent values
A hash map does the counting in one pass. The interesting part is ordering what you counted, and saying precisely how ties break.
Open the problem → Easy10 minCut a note out of a magazine
You count the letters the magazine gives you and then spend them on the note one at a time, and the first count that would go below zero tells you the answer is no.
Open the problem →Write a solution and run it against the real test table.
Keep going
Are two words anagrams?
Two words are anagrams when every letter occurs equally often in both. One table of counts, added for one word and taken away for the other.
Open the problem → Medium15 minThe k most frequent values
A hash map does the counting in one pass. The interesting part is ordering what you counted, and saying precisely how ties break.
Open the problem → Easy10 minCut a note out of a magazine
You count the letters the magazine gives you and then spend them on the note one at a time, and the first count that would go below zero tells you the answer is no.
Open the problem → Arrays and hashingThe pattern behind it
Trade memory for time: a hash map answers "have I seen this?" in O(1), and most array problems reduce to asking it the right question.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.