Skip to main content

Group Anagrams Problem Solution

Complete Java implementation for grouping anagrams from a string array, including optimal approach and time/space complexity analysis.

The group anagrams problem requires organizing strings that are anagrams of each other into the same groups. This classic coding interview question tests understanding of hash tables, string sorting, and algorithmic efficiency in a practical scenario.

1. Problem Restatement

Clarifying questions to ask the interviewer:

  1. What is the expected size of the input array and individual strings? (e.g., up to 10^4 strings, each up to 100 characters)
  2. Are duplicate strings allowed in the input, and should they be grouped together?
  3. What is the expected return format? Should it be a list of lists, or can we return a map from sorted string to list?
  4. Can we mutate the input array, or must we work with a copy?
  5. What should we return for edge cases like empty input array or single string?

Restatement in my own words: The problem asks us to group anagrams together from a given list of strings. An anagram is a word formed by rearranging the letters of another word, using all the original letters exactly once. For example, "eat", "tea", and "ate" are anagrams of each other.

Clarification of inputs, outputs, and constraints:

  • Input: An array of strings containing only lowercase English letters
  • Output: A list of lists, where each inner list contains all anagrams from the input array
  • Constraints:
    • Each string length is between 0 and 100
    • Total number of strings can be up to 10^4
    • Strings may be empty

Assumptions I'm making:

  • All strings contain only lowercase English letters (a-z)
  • The order of groups in the result doesn't matter
  • The order of strings within each group doesn't matter
  • Empty strings are valid input and should be handled appropriately

Data shape and hidden structure: The key insight is that anagrams share the same character frequency distribution. If we sort the characters in each string, all anagrams will produce identical sorted strings. This creates a natural grouping mechanism where we can use the sorted string as a key to group all original strings that are anagrams of each other.

The problem has a hidden structure where we're essentially performing a classification task - classifying strings into groups based on their character composition, with the classification key being the sorted version of each string.

2. Pattern Recognition

The problem asks us to group anagrams together from a list of strings. Looking at the problem statement, I need to identify patterns that signal the approach.

The key wording that signals the pattern is "anagrams" - strings that contain the same letters with the same frequency but in different orders. This is a classic grouping problem where we need to identify equivalence classes based on some property.

The core insight is that two strings are anagrams if and only if they have the same character frequency distribution. This means we can use a canonical representation of each string's character count as the key for grouping.

The pattern here is:

  1. Grouping by equivalence relation - strings that are anagrams are equivalent under the character frequency relation
  2. Canonical representation pattern - we need to transform each string into a normalized form that can serve as a hash key
  3. Frequency counting pattern - we're essentially counting character frequencies to create unique identifiers

This fits the pattern because:

  • We have a set of strings that need to be partitioned into groups
  • The grouping criterion is based on character frequency matching (anagrams have identical frequency distributions)
  • We can use a hash map to efficiently group items by their canonical form

The core insight is that sorting characters or counting character frequencies both create a unique identifier for each anagram group, allowing us to use the frequency distribution as our grouping key.

This is fundamentally a hash-based grouping problem where we're creating a normalized representation to identify equivalent items in the input set.

3. Approach Evolution

My first instinct is to group words that are anagrams of each other - meaning they contain the same letters with the same frequencies but in different orders. The brute force approach would be to compare every word with every other word to check if they're anagrams, which would be O(n²) time complexity because for each word we'd need to check all other words.

But that's inefficient because we're doing redundant work. Let me think about how to optimize this by leveraging the fact that anagrams have the same character frequency distribution.

I notice that if I sort the characters of each string, all anagrams will produce the same sorted string. For example, "eat", "tea", and "ate" all become "aet" when sorted. This gives me a key to group by - the sorted character string.

So my improved approach is:

  1. For each string, sort its characters to create a canonical form
  2. Use this canonical form as a key in a hash map to group all anagrams together
  3. Return the grouped lists

This reduces the complexity from O(n²) to O(n × m log m) where n is the number of strings and m is the average length of strings, because we're sorting each string once.

The key invariant that must hold is that all anagrams must produce identical canonical keys, and no non-anagrams should share the same key. This is guaranteed by the fact that sorting preserves character frequencies but orders them consistently.

Let me trace through this approach:

  • Each string gets processed once
  • Sorting each string takes O(m log m) time where m is the string length
  • Hash map operations are O(1) average case
  • Overall complexity becomes O(n × m log m) time and O(n × m) space

This is much better than the brute force approach because we eliminate redundant pairwise comparisons and leverage a deterministic canonical representation for grouping.

4. Data Structure Choice

For this problem, I need to choose data structures that will efficiently group anagrams together while maintaining the ability to quickly identify when two strings are anagrams of each other.

The primary data structure I'll use is a HashMap<String, List<String>> where:

  • The key is the sorted version of each string (which serves as a canonical representation for anagrams)
  • The value is a list of original strings that are anagrams of each other

Why HashMap?

  • It provides O(1) average time complexity for both lookups and insertions
  • It naturally groups all strings with the same key (anagram signature) together
  • It's perfect for the "grouping" requirement of the problem

Why String as Key?

  • For anagrams, sorting the characters produces identical strings
  • This creates a canonical form that can be used as a hash key
  • It's simple to implement and understand

Why List as Value?

  • Each group needs to maintain the original strings in their input order
  • Lists naturally support appending new elements and preserving insertion order
  • The problem asks for grouping, so we need to collect all strings that belong together

Helper Structure - Character Array:

  • I'll use a char[] to convert strings for sorting, which is more efficient than creating multiple String objects
  • This avoids unnecessary string allocations during the sorting process

This choice of data structures enables an efficient O(nmlog(m)) solution where n is the number of strings and m is the average string length, with clean code structure that's easy to reason about and maintain.

5. Algorithm Walkthrough

Let me trace through the algorithm with a concrete example to demonstrate how it works step by step.

Example Input: ["eat", "tea", "tan", "ate", "nat", "bat"]

Step 1: Initialize empty result list and map to store grouped anagrams

  • result = []
  • anagramMap = {}

Step 2: Process first string "eat"

  • Sort characters: sorted("eat") = "aet"
  • Check if "aet" exists in map: No
  • Add new entry: anagramMap["aet"] = ["eat"]

Step 3: Process second string "tea"

  • Sort characters: sorted("tea") = "aet"
  • Check if "aet" exists in map: Yes
  • Append to existing list: anagramMap["aet"] = ["eat", "tea"]

Step 4: Process third string "tan"

  • Sort characters: sorted("tan") = "ant"
  • Check if "ant" exists in map: No
  • Add new entry: anagramMap["ant"] = ["tan"]

Step 5: Process fourth string "ate"

  • Sort characters: sorted("ate") = "aet"
  • Check if "aet" exists in map: Yes
  • Append to existing list: anagramMap["aet"] = ["eat", "tea", "ate"]

Step 6: Process fifth string "nat"

  • Sort characters: sorted("nat") = "ant"
  • Check if "ant" exists in map: Yes
  • Append to existing list: anagramMap["ant"] = ["tan", "nat"]

Step 7: Process sixth string "bat"

  • Sort characters: sorted("bat") = "abt"
  • Check if "abt" exists in map: No
  • Add new entry: anagramMap["abt"] = ["bat"]

Step 8: Convert map to result list

  • result = [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]

The algorithm correctly groups all anagrams together by using sorted characters as the key. Each group maintains the original order of strings from the input, and the final result contains lists of anagrams in any order.

Edge Case Handling:

  • Empty strings are handled correctly since sorted("") = ""
  • Single character strings work because sorted("a") = "a"
  • Duplicate strings are grouped properly by their sorted key
  • The map ensures no duplicate keys and maintains proper grouping invariant

The algorithm terminates correctly because every string is processed exactly once, and the map ensures that all anagrams are grouped under their canonical sorted form key.

6. Java Solution

import java.util.*;

public class GroupAnagrams {
    public List<List<String>> groupAnagrams(String[] strs) {
        // Map to store groups of anagrams
        Map<String, List<String>> anagramGroups = new HashMap<>();
        
        // Process each string in the input array
        for (String str : strs) {
            // Create a sorted version of the string to use as key
            String sortedKey = getSortedString(str);
            
            // Add current string to its anagram group
            anagramGroups.computeIfAbsent(sortedKey, k -> new ArrayList<>()).add(str);
        }
        
        // Return all groups as a list of lists
        return new ArrayList<>(anagramGroups.values());
    }
    
    /**
     * Helper method to create a canonical representation of a string
     * by sorting its characters alphabetically
     */
    private String getSortedString(String str) {
        char[] chars = str.toCharArray();
        Arrays.sort(chars);
        return new String(chars);
    }
}

This solution uses a hash map to group anagrams by their sorted character representation. For each input string, we create a "canonical" key by sorting its characters, then use that key to group all anagrams together. The time complexity is O(n * m log m) where n is the number of strings and m is the average string length, with space complexity O(n * m) for storing the results.

7. Complexity Analysis

Time Complexity: O(N × M × log M) where N is the number of strings and M is the average length of each string.

  • For each string, we need to sort its characters, which takes O(M × log M) time
  • We do this for all N strings, giving us the overall time complexity of O(N × M × log M)
  • The sorting dominates the linear traversal of each string

Space Complexity: O(N × M) where N is the number of strings and M is the average string length.

  • We store all input strings in our result map, requiring O(N × M) space
  • The auxiliary space for sorting each string is O(M), but this is dominated by the output storage
  • In the worst case, we might store all strings in the map, making space complexity O(N × M)

Why these bounds are correct:

  • The sorting operation is the primary cost driver, and it's performed N times
  • Space usage scales linearly with input size since we're essentially reorganizing the input into groups
  • The solution is optimal for the given constraints because we must examine each string at least once to determine its anagram group

Optimality note: While we can't avoid examining each string, we could potentially reduce the sorting overhead by using character frequency counting instead of sorting, which would bring us closer to O(N × M) time complexity. However, the current solution is clean, readable, and efficient enough for practical purposes in a FAANG interview setting.

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty input array: Should return empty list []
  • Single element: Array with one string should return list containing one group
  • Empty strings: ["", ""] should be grouped together as one anagram group

Duplicate Values and Repeated States

  • Duplicate strings: ["eat", "tea", "eat"] should group all "eat" and "tea" together
  • Identical strings: ["abc", "abc"] should form one group with two elements

Boundary Conditions

  • Single character strings: ["a", "b", "c"] should each form separate groups
  • Very long strings: Performance depends on string length handling (O(m) where m is average string length)
  • Large input size: With 10^4+ strings, the solution must scale efficiently

Common Implementation Mistakes

  • Incorrect hash key generation: Using string concatenation instead of sorted characters can create different keys for same anagrams
  • Not handling null inputs: Failing to check for null strings in input array
  • Modifying input: Mutating original strings or array elements during processing
  • Memory overhead: Creating unnecessary intermediate collections during grouping operations
  • Sorting complexity: Not realizing that sorting each string is O(m log m) where m is string length, and this dominates the overall complexity

Implementation Considerations

  • String immutability: Java strings are immutable, so sorting creates new objects - this is acceptable for the algorithm but worth noting
  • Integer overflow: When computing hash codes for very long strings, though unlikely with reasonable constraints
  • Character encoding: Assuming ASCII or Unicode consistency across all inputs (should be safe in Java)

9. Alternative Approaches

There are several alternative approaches to solving the group anagrams problem, each with different trade-offs:

Approach 1: Sorting Characters in Each String Instead of using prime products, we could sort the characters in each string and use the sorted string as a key. For example, "eat" becomes "aet", "tea" becomes "aet", and "ate" becomes "aet". All strings that are anagrams will have the same sorted representation.

public List<List<String>> groupAnagrams(String[] strs) {
    Map<String, List<String>> groups = new HashMap<>();
    
    for (String str : strs) {
        char[] chars = str.toCharArray();
        Arrays.sort(chars);
        String sorted = new String(chars);
        
        groups.computeIfAbsent(sorted, k -> new ArrayList<>()).add(str);
    }
    
    return new ArrayList<>(groups.values());
}

This approach has O(n * m log m) time complexity where n is the number of strings and m is the average string length. It's simpler to understand but less efficient than the prime product approach for large inputs.

Approach 2: Character Frequency Counting We could create a frequency count of each character in every string and use that as the key. For example, "eat" would have frequency {a:1, e:1, t:1}. This approach avoids sorting but requires more complex key generation.

public List<List<String>> groupAnagrams(String[] strs) {
    Map<String, List<String>> groups = new HashMap<>();
    
    for (String str : strs) {
        String key = getFrequencyKey(str);
        groups.computeIfAbsent(key, k -> new ArrayList<>()).add(str);
    }
    
    return new ArrayList<>(groups.values());
}

private String getFrequencyKey(String str) {
    int[] freq = new int[26];
    for (char c : str.toCharArray()) {
        freq[c - 'a']++;
    }
    
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 26; i++) {
        if (freq[i] > 0) {
            sb.append((char)('a' + i)).append(freq[i]);
        }
    }
    return sb.toString();
}

This approach has O(n * m) time complexity but uses more space for key generation and is more complex to implement correctly.

Trade-offs:

  • The prime product approach offers better time complexity O(n * m) but requires careful handling of integer overflow
  • The sorting approach is simpler but slower due to the O(m log m) sorting step
  • The frequency counting approach is more explicit about character counts but has more complex key generation logic

The prime product approach is generally preferred for its optimal time complexity and clean implementation when integer overflow is properly handled.

10. Test Cases

import java.util.*;

public class GroupAnagramsTestCases {
    public static void main(String[] args) {
        // Test case 1: Basic functionality
        String[] input1 = {"eat", "tea", "tan", "ate", "nat", "bat"};
        List<List<String>> expected1 = Arrays.asList(
            Arrays.asList("eat", "tea", "ate"),
            Arrays.asList("tan", "nat"),
            Arrays.asList("bat")
        );
        
        // Test case 2: Empty input
        String[] input2 = {};
        List<List<String>> expected2 = new ArrayList<>();
        
        // Test case 3: Single element
        String[] input3 = {"abc"};
        List<List<String>> expected3 = Arrays.asList(Arrays.asList("abc"));
        
        // Test case 4: All same anagrams
        String[] input4 = {"abc", "bca", "cab"};
        List<List<String>> expected4 = Arrays.asList(Arrays.asList("abc", "bca", "cab"));
        
        // Test case 5: Mixed case and empty strings
        String[] input5 = {"", "a", "ab", "ba"};
        List<List<String>> expected5 = Arrays.asList(
            Arrays.asList(""),
            Arrays.asList("a"),
            Arrays.asList("ab", "ba")
        );
        
        // Test case 6: Large input with duplicates
        String[] input6 = {"listen", "silent", "enlist", "hello", "world"};
        List<List<String>> expected6 = Arrays.asList(
            Arrays.asList("listen", "silent", "enlist"),
            Arrays.asList("hello"),
            Arrays.asList("world")
        );
    }
}

Sample test cases include:

  1. Basic functionality - Standard mixed anagrams to verify grouping works correctly
  2. Empty input - Edge case handling for empty arrays
  3. Single element - Minimal valid input to test boundary conditions
  4. All same anagrams - Verify grouping of identical anagram sets
  5. Mixed case and empty strings - Handle special characters and edge cases
  6. Large input with duplicates - Performance and correctness with larger datasets

The test cases cover both typical scenarios and edge conditions that could break a naive implementation. Each case tests different aspects of the grouping logic including empty inputs, single elements, duplicate handling, and various string formats.

11. Final Summary

The core idea is to group anagrams by using sorted characters as a canonical form for each string, enabling efficient classification through hash-based grouping. This solution works because anagrams share identical character frequencies, making their sorted representations identical. The approach is optimal with O(nmlog(m)) time complexity where n is the number of strings and m is the average string length, using O(n*m) space for the hash map storage. The solution's efficiency stems from avoiding expensive character counting operations by leveraging sorted string comparison, making it both clean and performant for production use.

Was this article helpful?

Topics in this article

More from DSA Problems

  • DSA Patterns

    Group Anagrams Problem Solution | LeetCode 49

    Complete Java solution for grouping anagrams with optimal time and space complexity analysis for FAANG interviews.

  • DSA Problems

    Find All Anagrams in a String Algorithm Solution

    Complete Java implementation for finding all anagrams of a pattern string within a target string using sliding window technique.

  • DSA Problems

    First Unique Character in a String Algorithm

    Find the first non-repeating character in a string and return its index, with optimal time and space complexity solutions.

  • DSA Problems

    Isomorphic Strings Java Solution with HashMap

    Complete Java implementation for determining if two strings are isomorphic, using character mapping with HashMap for optimal time and space complexity.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.