Group Anagrams in Java DSA: Sorting Key and Count Key, Explained Step by Step
-
Last Updated: August 6, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
Learn Group Anagrams in Java DSA with two clear approaches — a sort key and a faster count key. Full step-by-step dry runs, beginner-friendly code, and complexity.
Group Anagrams in Java is a classic hashing problem that feels tricky at first, then clicks once you see the trick. You get a list of words. Some of them are anagrams of each other, which means they use the same letters, just in a different order. Your job is to put the anagrams together in groups.
Take the words eat, tea and ate. They all use the letters a, e and t. So they belong in one group. A word like bat uses different letters, so it goes in its own group.
The task sounds simple, but the catch is how you decide which words are anagrams. You cannot compare every word with every other word. That is slow. Instead, you give each word a signature, and words with the same signature land in the same bucket.
We build the answer in two clear steps. First we make the signature by sorting each word. Then we make a faster signature by counting letters instead of sorting. Both put anagrams in the same group, but one is quicker.
Every approach gets a full, step-by-step dry run on the same seven words. Nothing is skipped, so you can watch each word get a key and drop into its bucket.
Let us pin down the rules before writing any code.
For our seven words the answer has three groups. The words eat, tea and ate form one group. Then tan and nat form another. Finally bat and tab form the last one. Every word lands in exactly one group.
Two words are anagrams when they hold the same letters the same number of times. So if we can turn each word into a single key that ignores order, all anagrams get the same key.
A HashMap lets us store each key with a list of words that share it. When a new word arrives, we build its key and drop the word into the matching bucket.
For each word, sort its letters to build a key. Anagrams always sort to the same string, so they share a key. Use a HashMap from this sorted key to a list of words. Walk the words once, and drop each into its bucket. At the end, the map values are your groups.
map = empty map from string to list
for word in words:
key = sort(word) // sorted letters of the word
if key not in map:
map[key] = empty list
map[key].add(word) // drop word into its bucket
return all values of mapThink of it like sorting mail into pigeon holes. Every word gets a label, and words with the same label go into the same hole.
import java.util.*;
public class GroupAnagramsSort {
public static List<List<String>> groupAnagrams(String[] words) {
Map<String, List<String>> map = new HashMap<>();
for (String word : words) {
char[] chars = word.toCharArray();
Arrays.sort(chars);
String key = new String(chars);
map.putIfAbsent(key, new ArrayList<>());
map.get(key).add(word);
}
return new ArrayList<>(map.values());
}
public static void main(String[] args) {
String[] words = {"eat","tea","tan","ate","nat","bat","tab"};
System.out.println(groupAnagrams(words));
}
}This is the same plan in Java. The one thing to know is that Java cannot sort a String on its own, so we turn it into a char array first.
Let us trace all seven words: eat, tea, tan, ate, nat, bat, tab. For each word we do two things. First we sort its letters into a key. Then we drop the word into the map bucket for that key.
The sorting itself is where beginners lose the thread. So for each word we show the char array changing letter by letter until it is fully sorted. Then a second table shows the map after the word is placed.
Word 0: eat
The char array starts as [e, a, t]. Sorting walks left to right and slides each letter back to its correct spot.
| step | letter placed | char array now | what happened |
|---|---|---|---|
| start | — | [e, a, t] | unsorted char array of the word |
| 1 | a | [a, e, t] | a is smaller than e, so it shifts left of e |
| 2 | t | [a, e, t] | t is already after e, so nothing moves |
The sorted key is aet. The map is empty, so aet is a new key. A fresh list opens and eat drops in. Map: {aet=[eat]}.
Word 1: tea
The char array starts as [t, e, a]. Both letters after the first need to slide left.
| step | letter placed | char array now | what happened |
|---|---|---|---|
| start | — | [t, e, a] | unsorted char array of the word |
| 1 | e | [e, t, a] | e is smaller than t, so e shifts before t |
| 2 | a | [a, e, t] | a is smallest, so it slides to the very front |
The sorted key is aet again. That key already exists from eat. No new list is needed, so tea joins the bucket. Map: {aet=[eat, tea]}.
Word 2: tan
The char array starts as [t, a, n]. This word has different letters, so watch the key change.
| step | letter placed | char array now | what happened |
|---|---|---|---|
| start | — | [t, a, n] | unsorted char array of the word |
| 1 | a | [a, t, n] | a is smaller than t, so a moves to the front |
| 2 | n | [a, n, t] | n sits between a and t, so it settles in the middle |
The sorted key is ant. This key is new, so a fresh list opens and tan drops in. Map now has two keys: {aet=[eat, tea], ant=[tan]}.
Word 3: ate
This word starts as the char array [a, t, e]. Its first letter is already smallest, so only the last one moves.
| step | letter placed | char array now | what happened |
|---|---|---|---|
| start | — | [a, t, e] | unsorted char array of the word |
| 1 | t | [a, t, e] | t is bigger than a, so it stays where it is |
| 2 | e | [a, e, t] | e is smaller than t, so e slides in before t |
The sorted key is aet, the very first key we made. The aet bucket already holds eat and tea. So ate joins them, and that bucket becomes [eat, tea, ate].
Word 4: nat
The char array starts as [n, a, t]. Only the a needs to move up front.
| step | letter placed | char array now | what happened |
|---|---|---|---|
| start | — | [n, a, t] | unsorted char array of the word |
| 1 | a | [a, n, t] | a is smaller than n, so a shifts to the front |
| 2 | t | [a, n, t] | t is already the largest, so nothing moves |
The sorted key is ant. That key exists from tan, so nat joins it. The ant bucket becomes [tan, nat].
Word 5: bat
The char array starts as [b, a, t]. Just the a jumps ahead of b.
| step | letter placed | char array now | what happened |
|---|---|---|---|
| start | — | [b, a, t] | unsorted char array of the word |
| 1 | a | [a, b, t] | a is smaller than b, so a moves to the front |
| 2 | t | [a, b, t] | t is the largest, so it stays at the end |
The sorted key is abt, a brand new key. A fresh list opens and bat drops in. The map now holds three keys.
Word 6: tab
The char array starts as [t, a, b]. Both letters after the first slide left.
| step | letter placed | char array now | what happened |
|---|---|---|---|
| start | — | [t, a, b] | unsorted char array of the word |
| 1 | a | [a, t, b] | a is smaller than t, so a moves to the front |
| 2 | b | [a, b, t] | b is smaller than t, so b slides in before t |
The sorted key is abt, which bat just created. So tab joins bat, and the last bucket becomes [bat, tab].
Here is the map after each word is placed, so you can see the buckets grow.
| i (read index) | word | sorted key | new key? | map after this step |
|---|---|---|---|---|
| 0 | eat | aet | Yes | {aet=[eat]} |
| 1 | tea | aet | No | {aet=[eat, tea]} |
| 2 | tan | ant | Yes | {aet=[eat, tea], ant=[tan]} |
| 3 | ate | aet | No | {aet=[eat, tea, ate], ant=[tan]} |
| 4 | nat | ant | No | {aet=[eat, tea, ate], ant=[tan, nat]} |
| 5 | bat | abt | Yes | {aet=[...], ant=[...], abt=[bat]} |
| 6 | tab | abt | No | {aet=[...], ant=[...], abt=[bat, tab]} |
After the loop the map holds three keys. The final answer is [[eat, tea, ate], [tan, nat], [bat, tab]].
Look back at the seven sort traces and one pattern jumps out. Words that are anagrams always end at the same sorted array, even though they started in totally different orders.
That is the whole trick. Sorting throws away the original order, so any two anagrams collapse to one identical key. Words with different letters, like bat, can never reach the same sorted string, so they stay apart.
Notice too that the map only ever grows. Each new word either opens a fresh bucket or joins an old one. No word is ever compared with another word directly, which is what makes this fast.
💡 Interview Insight A common question is “why sort the word at all?” Sorting turns every anagram into the same string, so it becomes a perfect group key. Different letters can never sort to the same string.
Sorting is clean and easy to remember. Still, that log factor from sorting is extra work. Next we swap sorting for counting and shave it off.
Skip the sorting. For each word, count how many times each of the 26 letters appears. Turn that count into a string like a signature. Anagrams have the same letter counts, so they build the same signature. Use the signature as the map key, just like before. This avoids sorting, so each word is faster to key.
map = empty map from string to list
for word in words:
count = array of 26 zeros
for ch in word:
count[ch - 'a'] = count[ch - 'a'] + 1
key = join count values with a separator
if key not in map:
map[key] = empty list
map[key].add(word)
return all values of mapSame pigeon-hole idea, but we build the label a different way. Instead of sorting, we just count how many of each letter the word has.
import java.util.*;
public class GroupAnagramsCount {
public static List<List<String>> groupAnagrams(String[] words) {
Map<String, List<String>> map = new HashMap<>();
for (String word : words) {
int[] count = new int[26];
for (char ch : word.toCharArray()) {
count[ch - 'a']++;
}
StringBuilder sb = new StringBuilder();
for (int c : count) {
sb.append(c).append('#');
}
String key = sb.toString();
map.putIfAbsent(key, new ArrayList<>());
map.get(key).add(word);
}
return new ArrayList<>(map.values());
}
public static void main(String[] args) {
String[] words = {"eat","tea","tan","ate","nat","bat","tab"};
System.out.println(groupAnagrams(words));
}
}The Java code follows the same counting idea. The one new helper is the separator hash, which keeps counts apart so they never blur together.
💡 Why the hash separator? Without a separator, counts a=1, b=1 and a=11 could both read as “11”. The hash between numbers keeps every count clearly apart, so different words never clash by accident.
Same seven words: eat, tea, tan, ate, nat, bat, tab. For each word we walk its letters and bump one slot per letter. Then the finished count becomes the key.
The count array has 26 slots, one per lowercase letter. Slot a is index 0, slot b is index 1, and so on. To keep tables short, we show only the slots that are not zero after each letter. Every other slot stays at zero the whole time.
Word 0: eat
The count array starts as all zeros. We read eat one letter at a time.
| step | letter read | slot hit (index) | nonzero slots now |
|---|---|---|---|
| 1 | e | slot 4 (e) -> 1 | e=1 |
| 2 | a | slot 0 (a) -> 1 | a=1, e=1 |
| 3 | t | slot 19 (t) -> 1 | a=1, e=1, t=1 |
The final count is a=1, e=1, t=1. This key is new, so a fresh list opens and eat drops in. Bucket: [eat].
Word 1: tea
Zeros again. The letters arrive in a different order than eat, but watch the final counts.
| step | letter read | slot hit (index) | nonzero slots now |
|---|---|---|---|
| 1 | t | slot 19 (t) -> 1 | t=1 |
| 2 | e | slot 4 (e) -> 1 | e=1, t=1 |
| 3 | a | slot 0 (a) -> 1 | a=1, e=1, t=1 |
The final count is a=1, e=1, t=1, exactly the same as eat. Notice the order the letters came in did not matter. So tea joins eat, and the bucket becomes [eat, tea].
Word 2: tan
Fresh zeros. This word has an n, so its count will differ.
| step | letter read | slot hit (index) | nonzero slots now |
|---|---|---|---|
| 1 | t | slot 19 (t) -> 1 | t=1 |
| 2 | a | slot 0 (a) -> 1 | a=1, t=1 |
| 3 | n | slot 13 (n) -> 1 | a=1, n=1, t=1 |
The final count is a=1, n=1, t=1. The n slot makes this key different from eat. So it is new, a fresh bucket opens, and tan drops in. Bucket: [tan].
Word 3: ate
Zeros to start. The letters are a, t, e this time.
| step | letter read | slot hit (index) | nonzero slots now |
|---|---|---|---|
| 1 | a | slot 0 (a) -> 1 | a=1 |
| 2 | t | slot 19 (t) -> 1 | a=1, t=1 |
| 3 | e | slot 4 (e) -> 1 | a=1, e=1, t=1 |
The final count is a=1, e=1, t=1, matching eat and tea. So ate joins that bucket, which becomes [eat, tea, ate].
Word 4: nat
Zeros again. The letters n, a, t will match tan.
| step | letter read | slot hit (index) | nonzero slots now |
|---|---|---|---|
| 1 | n | slot 13 (n) -> 1 | n=1 |
| 2 | a | slot 0 (a) -> 1 | a=1, n=1 |
| 3 | t | slot 19 (t) -> 1 | a=1, n=1, t=1 |
The final count is a=1, n=1, t=1, the same as tan. So nat joins it, and the bucket becomes [tan, nat].
Word 5: bat
Fresh zeros. Here the b will land in a slot no earlier word touched.
| step | letter read | slot hit (index) | nonzero slots now |
|---|---|---|---|
| 1 | b | slot 1 (b) -> 1 | b=1 |
| 2 | a | slot 0 (a) -> 1 | a=1, b=1 |
| 3 | t | slot 19 (t) -> 1 | a=1, b=1, t=1 |
The final count is a=1, b=1, t=1. The b slot makes this a new key. A fresh bucket opens and bat drops in. Bucket: [bat].
Word 6: tab
Zeros to finish the trace. The letters t, a, b will match bat.
| step | letter read | slot hit (index) | nonzero slots now |
|---|---|---|---|
| 1 | t | slot 19 (t) -> 1 | t=1 |
| 2 | a | slot 0 (a) -> 1 | a=1, t=1 |
| 3 | b | slot 1 (b) -> 1 | a=1, b=1, t=1 |
The final count is a=1, b=1, t=1, the same as bat. So tab joins bat, and the last bucket becomes [bat, tab].
Here is the summary of all seven words, showing the final counts and the bucket after each one.
| i (read index) | word | final nonzero counts | new key? | bucket after this step |
|---|---|---|---|---|
| 0 | eat | a=1, e=1, t=1 | Yes | [eat] |
| 1 | tea | a=1, e=1, t=1 | No | [eat, tea] |
| 2 | tan | a=1, n=1, t=1 | Yes | [tan] |
| 3 | ate | a=1, e=1, t=1 | No | [eat, tea, ate] |
| 4 | nat | a=1, n=1, t=1 | No | [tan, nat] |
| 5 | bat | a=1, b=1, t=1 | Yes | [bat] |
| 6 | tab | a=1, b=1, t=1 | No | [bat, tab] |
The map ends with three keys, giving the same answer: [[eat, tea, ate], [tan, nat], [bat, tab]].
The slot traces make one thing clear. Anagrams reach the same final count no matter what order their letters arrive in.
So counting does the same job as sorting, but it never rearranges anything. It just tallies letters in one quick pass, which is why it runs faster.
💡 Interview Insight If asked “why is counting faster than sorting?”, say that counting a word is a single pass over its letters, which is O(k). Sorting costs O(k log k). Over many words that log factor adds up, so counting wins.
This is faster than sorting because we drop the log factor. For lowercase words, the count key is the version interviewers like to see.
Both approaches return the same groups. They only differ in how they build the key for each word.
| Approach | Key idea | Time | Space | Note |
|---|---|---|---|---|
| Sort key | Sort each word | O(n·k log k) | O(n·k) | Simple and easy to recall |
| Count key | Count 26 letters | O(n·k) | O(n·k) | Faster, the expected answer |
Both use the same amount of space, since both store every word in the map. What separates them is the key-building cost. Sorting pays a log factor per word, while counting does not.
In an interview, mention the sort key first to show the idea clearly. Then move to the count key for speed. That progression tells a clean story and shows you can optimise.
💡 Interview Insight If asked what happens with uppercase or unicode letters, be honest. The count key of size 26 assumes lowercase a to z. For wider input, either grow the array or fall back to the sort key, which works for any characters.
A few small traps catch beginners on Group Anagrams. Keep them in mind.
Run a single-word case and an empty-array case through your code before calling it done. These quiet cases catch more bugs than the obvious ones.
Group Anagrams in Java teaches a habit you will reuse everywhere. When you must group items that look different but mean the same, give each one a signature and let a map do the grouping.
Our seven-word traces showed the payoff clearly. Sorting each word built a shared key, so anagrams met in one bucket. Counting letters built the same shared key without sorting, which runs faster.
So take the pattern, not just the answer. Turn each item into a canonical key, then group by that key in a map. Reach for the count key when the alphabet is small, and keep the sort key handy for anything wider.
That signature habit turns many string and array problems from tricky into routine. It will serve you well on the harder grouping problems waiting further down the list.
A: Give each word a signature and group by it in a HashMap. The count key (counting the 26 letters) is the fastest common approach for lowercase input, running in O(n·k) time.
A: Counting a word is a single pass over its letters, which is O(k). Sorting a word costs O(k log k). Across many words, that log factor adds up, so counting wins.
A: Sorting a word’s letters turns every anagram into the same string. For example, eat, tea, and ate all sort to “aet”. That shared string becomes the map key, so anagrams land in the same bucket.
A: The 26-slot count key assumes lowercase a to z. For wider input, either grow the count array or fall back to the sort key, which works for any characters.
A: The sort key runs in O(n·k log k) time; the count key runs in O(n·k). Both use O(n·k) space to store every word and its key in the map, where n is the number of words and k is the longest word length.