String · 36 of 43
Given a sequence of words, print all anagrams together
Put words made of the same letters into the same group, by giving every word a label that its letter twins share.
- Simple way
- time O(n^2 * m log m)
room O(n * m) - Clever way
- time O(n * m log m)
room O(n * m)
The problem, simply
You get a list of words. Two words are anagrams when they use exactly the same letters the same number of times, only in a different order, like eat and tea.
Put the anagrams next to each other: give back a list of groups, where each group holds words that are anagrams of each other. Here n is the number of words and m is the length of the longest word.
A story to remember it
Picture it
Six words. Each one gets a label: its letters sorted. Words with the same label share a bin in the map.
Examples
A normal case
- In
['eat', 'tea', 'tan', 'ate', 'nat', 'bat']- Out
[[eat, tea, ate], [tan, nat], [bat]]
eat, tea and ate all sort to aet. tan and nat sort to ant. bat is alone.
Same letters, different counts
- In
['ab', 'ba', 'abb', 'bab']- Out
[[ab, ba], [abb, bab]]
ab and abb share letters but not counts, so they are not anagrams. Their sorted labels, ab and abb, are different.
One word
- In
['a']- Out
[[a]]
A single word is a group of one.
Empty words
- In
['', '']- Out
[['', '']]
Two empty words have the same (empty) letters, so they are anagrams of each other.
Way 1: the simple way
Take the first word that has no group yet. Walk through every later word and check: is it an anagram of this one? To check two words, sort both and compare. Mark each word you put in a group so you never use it twice.
- For each word
ithat is not used, start a new group with it. - For each later word
jthat is not used, sort both and compare. - If they match, add
jto the group and mark it used.
import java.util.*;
class Solution {
private boolean sameLetters(String a, String b) {
if (a.length() != b.length()) return false;
char[] x = a.toCharArray(), y = b.toCharArray();
Arrays.sort(x);
Arrays.sort(y);
return Arrays.equals(x, y);
}
public List<List<String>> groupAnagrams(String[] words) {
int n = words.length;
boolean[] used = new boolean[n];
List<List<String>> groups = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (used[i]) continue;
List<String> group = new ArrayList<>();
group.add(words[i]);
used[i] = true;
for (int j = i + 1; j < n; j++) {
if (!used[j] && sameLetters(words[i], words[j])) {
group.add(words[j]);
used[j] = true;
}
}
groups.add(group);
}
return groups;
}
}How long it takes: up to n^2 pairs, and each check sorts two words, so O(n^2 * m log m). Extra room: the groups hold every word, O(n * m).
Way 2: the clever way
Do the sticky note trick. Sort each word's letters once to make its label, the key. Use a map from key to the group of words. Every word goes straight to its bin, so you never compare two words directly.
- For each word, sort its letters to get the key.
- If the map has no bin for that key, make an empty one.
- Add the word to the bin.
- Give back all the bins.
Dropping each word into its bin
01 / 08
Start with an empty map. Each key will be a word with its letters sorted, and each value is the group of words that share it.
- looking now
- seen
- done
- answer
- just added
import java.util.*;
class Solution {
public List<List<String>> groupAnagrams(String[] words) {
// LinkedHashMap keeps the groups in the order they first appear
Map<String, List<String>> bins = new LinkedHashMap<>();
for (String w : words) {
char[] letters = w.toCharArray();
Arrays.sort(letters); // the sticky note
String key = new String(letters);
bins.computeIfAbsent(key, k -> new ArrayList<>()).add(w);
}
return new ArrayList<>(bins.values());
}
}How long it takes: each of the n words is sorted once, O(n * m log m). Extra room: the map stores every word and one key per group, O(n * m).
Way 3: count the letters instead of sorting
If words use only a to z, the label can be the 26 letter counts instead of the sorted word. tea becomes "one a, one e, one t". Counting is a straight walk, so each word costs O(m) instead of O(m log m).
import java.util.*;
class Solution {
public List<List<String>> groupAnagrams(String[] words) {
Map<String, List<String>> bins = new LinkedHashMap<>();
for (String w : words) {
int[] count = new int[26];
for (char c : w.toCharArray()) count[c - 'a']++;
StringBuilder key = new StringBuilder();
for (int x : count) key.append('#').append(x); // '#' keeps 1,12 apart from 11,2
bins.computeIfAbsent(key.toString(), k -> new ArrayList<>()).add(w);
}
return new ArrayList<>(bins.values());
}
}This takes O(n * m) time (plus 26 per word to build the key) and the same O(n * m) room.
Watch out for
- A plain
HashMapgives the groups in a random looking order. UseLinkedHashMapif the order must follow the input. - Upper and lower case:
Teaandeatare only anagrams if you lower case first. Ask the interviewer. - The empty string is a valid word, and its key is the empty string.
- Do not use the sum of letter codes as a key.
adandbchave the same sum but are not anagrams.
What interviewers ask next
- Is
san anagram oft? Count letters ofsup and letters oftdown in one 26 slot table, then check every slot is 0. - Find all places in a long text where an anagram of
pstarts. Slide a window of lengthp.length()and keep a 26 slot count. - What about Unicode words? Sorting still works. For counts, use a
HashMap<Character, Integer>and build a key from the sorted entries. - Could you multiply one prime per letter as the key? Yes in theory, every group gets a unique product, but long words overflow
long. Mention it, then prefer the count key. - Very many words that do not fit in memory? Write
key, wordpairs to disk, sort them by key, and read groups off in order.
Remember this
Grown-up words: the sticky note is a canonical form, the bins are a hash map from key to list, and the letter count label is a frequency signature.