Start learning free

LESSON 203 OF 347 · SORTING

Group Anagrams in C#: Sorting by Sorted Letters

Practice O(n * log n * L * log L) Read the full explanation

Given an array of strings, group the anagrams together. You can return the answer in any order.

An anagram is a word or phrase formed by rearranging the letters of a different word or phrase, using all the original letters exactly once.

Example:

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

Output:  [["bat"],["nat","tan"],["ate","eat","tea"]]

Note:

For this lesson, your algorithm should run in O(n * log n * L * log L) time and use O(n * L) extra space.
(There are faster solutions which we will discuss in future lessons)


STEP 1 OF 12
    Ctrl + Enter to check

    C# · SORTING

    Group Anagrams in C#, explained

    Problem overview

    The method groupAnagrams receives a string[] called strings and returns a List<List<string>>. Each inner list collects words that are anagrams of each other, meaning one can be rearranged into the other using every letter exactly once. The groups can come back in any order.

    For ["eat", "tea", "tan", "ate", "nat", "bat"] there are three groups: eat, tea and ate share the letters a, e and t; tan and nat share a, n and t; and bat stands alone. Running the reference solution on this input returns [bat], [eat, tea, ate] and [tan, nat]. The groups and their order differ from the statement's listing, which is fine, because any order is accepted.

    Approach: sort the words by their sorted letters

    Two words are anagrams exactly when their letters, sorted alphabetically, give the same string. eat, tea and ate all become aet. That string works as a signature for the group.

    The lesson's idea is to sort the whole array of words by this signature. Words with equal signatures end up next to each other, so one pass can cut the array into groups: whenever the next word's signature differs from the current one, the current group is complete.

    The statement allows O(n * log n * L * log L) time, where n is the number of words and L the longest word. Sorting the array makes O(n log n) comparisons, and each comparison sorts two words' letters at O(L log L) each. Extra space is O(n * L) for the result and the temporary character arrays.

    Step by step

    1. Assume a helper Sorted(s) that returns the letters of s in alphabetical order. It is written last.
    2. Sort strings with a comparison. Array.Sort accepts a lambda (a, b) => ... that returns a negative number, zero or a positive number. The lambda replaces a and b with their sorted forms and compares those with CompareTo. Reassigning the parameters changes only the values being compared; the array keeps the original words.
    3. Create result, an empty List<List<string>> for the finished groups.
    4. Create currentGroup, an empty List<string> for the group being built.
    5. Loop over every index i from 0 while i < strings.Length.
    6. Add the current word to currentGroup. In sorted order, it always belongs to the group under construction.
    7. Detect the end of a group. A group ends when i is the last index or when the next word's signature differs from this word's. The last-index test goes first, and || skips the right side when the left side is true, so strings[i + 1] is never read past the end.
    8. Store the group and start a new one. Add currentGroup to result, then assign a fresh list with new().
    9. Return result after the loop.
    10. Write the helper Sorted at the top of the class.
    11. Copy the word into a char[] with s.ToCharArray(). C# strings are immutable, so their characters can't be sorted in place.
    12. Sort the character array with Array.Sort.
    13. Build a string from it with new string(temp) and return it.

    The solution in C#

    class Solution {
        public string Sorted(string s) {
            char[] temp = s.ToCharArray();
            Array.Sort(temp);
            return new string(temp);
        }
    
        public List<List<string>> groupAnagrams(string[] strings) {
            Array.Sort(strings, (a, b) => {
                    a = Sorted(a);
                    b = Sorted(b);
                    return a.ToLower().CompareTo(b.ToLower());
            });
            List<List<string>> result = new List<List<string>>();
            List<string> currentGroup = new List<string>();
            for (int i = 0; i < strings.Length; i++) {
                currentGroup.Add(strings[i]);
                if (i == strings.Length - 1 || Sorted(strings[i]).CompareTo(Sorted(strings[i + 1])) != 0) {
                    result.Add(currentGroup);
                    currentGroup = new();
                }
            }
            return result;
        }
    }
    

    The C# details to notice:

    • The method name. This lesson names the method groupAnagrams with a lowercase first letter, while C# convention would be GroupAnagrams. C# is case-sensitive, so use the name exactly as the editor has it.
    • Immutable strings. There is no way to sort a string's characters where they are. ToCharArray() makes a mutable copy, and new string(char[]) turns it back. Calling temp.ToString() on a char[] would return the type name, not the letters.
    • A lambda as a comparison. Array.Sort(array, comparison) takes a Comparison<T>, a delegate that receives two elements and returns an int. Lambda parameters are ordinary local variables, which is why assigning to a and b is allowed and doesn't touch the array.
    • CompareTo returns an ordering, not a bool. It gives a negative number, 0 or a positive number, and 0 means the strings are equal. That is why the group boundary tests != 0. Note that string.CompareTo compares using the current culture. For lowercase ASCII letters that gives the usual alphabetical order.
    • Why not Clear(). result.Add(currentGroup) stores a reference to the same list object, not a copy. Calling currentGroup.Clear() would empty the group already stored. Assigning a new list leaves the stored one alone.
    • new() is a target-typed expression. The compiler takes the type from the variable, here List<string>.
    • Order inside a group. Array.Sort is not stable, so words with the same signature may appear in any order within their group.

    Complexity

    Time: O(n log n * L log L). The sort makes O(n log n) comparisons, and every comparison builds and sorts two signatures at O(L log L) each. The grouping pass computes two signatures per index, which is O(n * L log L), below the sort's cost.

    Space: O(n * L). The result holds every word once, and the temporary signatures at any moment take O(L).

    Edge cases and common mistakes

    • No anagrams at all. ["a", "b"] gives two groups of one word each.
    • A single word. The loop runs once, the last-index test is true, and the result is one group with that word.
    • An empty array. The loop never runs and the result is an empty list.
    • Empty strings. Their signature is the empty string, so all empty strings land in one group.
    • Reading past the end. Putting strings[i + 1] before the i == strings.Length - 1 test would throw IndexOutOfRangeException on the last word.
    • Reusing one list. Adding the same currentGroup object to result repeatedly and clearing it leaves empty lists in the answer.
    • Forgetting the last group. A group is only stored when a boundary is detected, and the final group has no word after it. That is the job of the last-index test.

    Testing

    • ["eat", "tea", "tan", "ate", "nat", "bat"] gives three groups, as described above.
    • ["a"] gives [[a]].
    • [] gives an empty list.
    • ["", "b", ""] gives two groups: the two empty strings together, and b alone.
    Keep going with C#About 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free