4.Group Anagrams
Problem
Given an array of strings strs, group the strings that are anagrams of each other and return the groups. The groups, and the strings inside each group, may be returned in any order.
Two strings are anagrams when they contain exactly the same letters with the same counts, e.g. "eat", "tea" and "ate".
Examples
Input: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
Output: [["bat"], ["nat", "tan"], ["ate", "eat", "tea"]]
Explanation: "eat", "tea", "ate" share letters {a,e,t}; "tan", "nat" share {a,n,t}; "bat" is alone.Input: strs = [""]
Output: [[""]]Input: strs = ["a"]
Output: [["a"]]Constraints
1 <= strs.length <= 10^40 <= strs[i].length <= 100strs[i]consists of lowercase English letters.
π‘ Key Insight
Give every string a canonical key that is identical for all its anagrams and different for everything else, then bucket strings by key in a HashMap<String, List<String>>. Two good keys exist: the sorted string ("eat" β "aet"), or the 26-letter frequency signature ("eat" β 1#0#0#0#1#...#1#...). The frequency key avoids the per-string sort.
Brute Force
TimeO(nΒ² Β· k)SpaceO(n)Intuition
Take the first ungrouped string, start a new group, then scan all remaining ungrouped strings and pull in those that are anagrams of it (checked with a 26-letter count comparison). Repeat until every string is grouped.
Algorithm
- Keep a
boolean[] used. - For each
inot yet used: startgroup = [strs[i]], markused[i]. - For each
j > inot used: ifisAnagram(strs[i], strs[j]), add it togroupand markused[j]. - Append
groupto the result.
Visualization
strs = [eat, tea, tan, ate, nat, bat]
i=0 eat: tea β, tan β, ate β, nat β, bat β β [eat, tea, ate]
i=2 tan: nat β, bat β β [tan, nat]
i=5 bat: β [bat]Code
import java.util.ArrayList;
import java.util.List;
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
int n = strs.length;
boolean[] used = new boolean[n];
List<List<String>> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (used[i]) {
continue;
}
List<String> group = new ArrayList<>();
group.add(strs[i]);
used[i] = true;
for (int j = i + 1; j < n; j++) {
if (!used[j] && isAnagram(strs[i], strs[j])) {
group.add(strs[j]);
used[j] = true;
}
}
result.add(group);
}
return result;
}
private boolean isAnagram(String a, String b) {
if (a.length() != b.length()) {
return false;
}
int[] count = new int[26];
for (int k = 0; k < a.length(); k++) {
count[a.charAt(k) - 'a']++;
count[b.charAt(k) - 'a']--;
}
for (int c : count) {
if (c != 0) {
return false;
}
}
return true;
}
}Complexity
- Time: O(nΒ² Β· k) β up to
nΒ²pair comparisons, each O(k) wherekis the max string length. - Space: O(n) β the
usedarray (output not counted).
Sorted String Key
TimeO(n Β· k log k)SpaceO(n Β· k)Intuition
All anagrams sort to the same string: "eat", "tea", "ate" all become "aet". Use that sorted string as a map key, and every anagram lands in the same bucket in a single pass.
Algorithm
- Create
Map<String, List<String>> groups. - For each
s: sort its characters intokey, thengroups.computeIfAbsent(key, ...).add(s). - Return
new ArrayList<>(groups.values()).
Visualization
eat β aet β
tea β aet ββ "aet": [eat, tea, ate]
ate β aet β
tan β ant β
nat β ant β΄β "ant": [tan, nat]
bat β abt ββ "abt": [bat]Code
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> groups = new HashMap<>();
for (String s : strs) {
char[] chars = s.toCharArray();
Arrays.sort(chars);
String key = new String(chars); // all anagrams share this sorted form
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(groups.values());
}
}Complexity
- Time: O(n Β· k log k) β each of the
nstrings is sorted in O(k log k). - Space: O(n Β· k) β the map stores every string plus its key.
Character Count KeyOptimal
TimeO(n Β· k)SpaceO(n Β· k)Intuition
Sorting each string costs O(k log k), but counting its letters costs only O(k). The 26 counts uniquely identify an anagram class, so turn them into a string key such as "1#0#0#0#1#...#1#...". A separator is required so that counts like 1,11 and 11,1 produce different keys.
Algorithm
- Create
Map<String, List<String>> groups. - For each
s:- Count its letters into
int[26] count. - Build
keyby joining the 26 counts with#. - Add
stogroups[key].
- Count its letters into
- Return the map's values.
Dry Run
flowchart LR
A["eat"] --> K1["a1 e1 t1"]
B["tea"] --> K1
C["ate"] --> K1
D["tan"] --> K2["a1 n1 t1"]
E["nat"] --> K2
F["bat"] --> K3["a1 b1 t1"]
K1 --> G1["[eat, tea, ate]"]
K2 --> G2["[tan, nat]"]
K3 --> G3["[bat]"]Code
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> groups = new HashMap<>();
for (String s : strs) {
int[] count = new int[26];
for (int i = 0; i < s.length(); i++) {
count[s.charAt(i) - 'a']++;
}
// Build a key like "1#0#0#...": the '#' keeps multi-digit counts unambiguous
StringBuilder key = new StringBuilder();
for (int c : count) {
key.append(c).append('#');
}
groups.computeIfAbsent(key.toString(), k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(groups.values());
}
}Complexity
- Time: O(n Β· k) β counting is O(k) per string and building the key is O(26).
- Space: O(n Β· k) β the stored strings plus one O(26) key per group.
Complexity Comparison
| Approach | Time | Space |
|---|---|---|
| Brute Force | O(nΒ² Β· k) | O(n) |
| Sorted String Key | O(n Β· k log k) | O(n Β· k) |
| Character Count Key | O(n Β· k) | O(n Β· k) |
Common Pitfalls
- Using
int[]directly as a map key β arrays use identityhashCode/equals, so no two strings ever share a bucket. Convert to aString(orArrays.toString). - Building the count key without separators β counts
[1, 11]and[11, 1]would both become"111". - Using
chars.toString()instead ofnew String(chars)β it returns something like[C@1b6d3586, not the characters. - Forgetting the empty string:
""is a valid input and must form its own group.