Start learning free

LESSON 204 OF 348 · SORTING

Group Anagrams in TypeScript: 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 10
    Ctrl + Enter to check

    TYPESCRIPT · SORTING

    Group Anagrams in TypeScript, explained

    Problem overview

    The method groupAnagrams receives an array of strings and returns them grouped, so that each group holds words made of the same letters. The groups can come in any order.

    In the example, ["eat", "tea", "tan", "ate", "nat", "bat"] splits into 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. An anagram uses every letter of the other word exactly once, so two words belong together when their letters, written in alphabetical order, are the same. Sorting eat, tea and ate all gives aet.

    The statement sets a budget of O(n * log n * L * log L) time and O(n * L) extra space, where n is the number of strings and L their length. That is a hint at the plan: sort the array of strings, where every comparison sorts letters.

    Approach: sort the words by their sorted letters

    Call the sorted letters of a word its key. Anagrams have equal keys. If the array of words is sorted by key, all words with the same key end up next to each other, because equal keys compare as equal. Then one pass over the array can cut it into groups: start a new group whenever the key changes.

    The cost matches the statement. Sorting the array takes O(n log n) comparisons. Each comparison computes two keys, and building a key sorts L letters, O(L log L). Comparing two keys takes O(L), which is smaller. Together that is O(n log n * L log L).

    Step by step

    The lesson first assumes a helper sorted(s) that returns the letters of s in alphabetical order, and writes it last.

    1. Sort strings with a comparator that compares this.sorted(a) and this.sorted(b) using localeCompare. It returns a negative number, zero or a positive number, which is what a comparator must return. Anagrams now sit side by side.
    2. Create result as an empty string[][], declared with const, because the code only pushes into it.
    3. Create currentGroup as an empty string[], declared with let, because it will be reassigned.
    4. Loop over the indices of strings.
    5. Push strings[i] into currentGroup. In sorted order, it always belongs to the group being built.
    6. Detect the end of a group. A group ends at the last index, or when the key of strings[i] differs from the key of strings[i + 1]. The last-index test comes first: || skips its right side when the left side is true, so strings[i + 1] is never needed past the end.
    7. Store the group and start a new one: push currentGroup into result, then assign a fresh empty array to currentGroup.
    8. Return result.
    9. Write the helper sorted. Place it before groupAnagrams in the class.
    10. Build the key: s.split('') turns the string into an array of characters, sort() orders them, and join('') glues them back into a string.

    The solution in TypeScript

    class Solution {
        sorted(s: string): string {
            return s.split('').sort().join('');
        }
    
        groupAnagrams(strings: string[]): string[][] {
            strings.sort((a, b) => this.sorted(a).localeCompare(this.sorted(b)));
            const result: string[][] = [];
            let currentGroup: string[] = [];
            for (let i = 0; i < strings.length; i++) {
                currentGroup.push(strings[i]);
                if (i == strings.length - 1 || this.sorted(strings[i]) != this.sorted(strings[i + 1])) {
                    result.push(currentGroup);
                    currentGroup = [];
                }
            }
            return result;
        }
    }
    

    On the example, this returns [["bat"],["eat","tea","ate"],["tan","nat"]]. The groups come out in the alphabetical order of their keys abt, aet and ant. The statement accepts any order.

    Details worth knowing:

    • Arrow functions and this. An arrow function keeps the this of the method around it, so this.sorted works inside the comparator. A function expression there would get its own this and fail.
    • Strings have no sort method, but arrays do. That is why the helper goes through split and join.
    • The default sort() on characters. With no comparator, characters are ordered by their character codes, which is alphabetical for lowercase letters. Uppercase letters come before lowercase ones, and case matters for grouping: "Ab" and "ab" get the different keys Ab and ab, so they are not grouped together.
    • Reassign, don't clear. currentGroup = [] creates a new array. Writing currentGroup.length = 0 instead would also empty the group already stored in result, because both names point to the same array.
    • The input changes. sort reorders the caller's array of strings.

    Complexity

    Time: O(n * log n * L * log L). The sort makes O(n log n) comparisons, and each one builds two keys at O(L log L) apiece. The grouping pass computes two keys per index, which is O(n * L log L), and doesn't add to the bound.

    Space: O(n * L). The groups hold the original strings, and the keys built during comparisons are temporary, O(L) at a time. The keys are recomputed on every comparison instead of being stored, which saves memory and costs time.

    Edge cases and common mistakes

    • An empty array. The loop doesn't run, so the result is [].
    • The empty string. [""] gives [[""]], a single group.
    • Equal words. ["a", "a"] gives [["a", "a"]].
    • Words with different letters but the same length. ["ab", "ba", "cc"] gives [["ab", "ba"], ["cc"]]: the comparison is on sorted letters, not on length.
    • Reading strings[i + 1] on the last index. Reading past the end gives undefined in TypeScript, and calling sorted on it would then fail at run time. The last-index check prevents that.
    • Forgetting to push the last group. The condition has to treat the final index as the end of a group, because no next string exists to differ from it.

    Testing

    • ["eat", "tea", "tan", "ate", "nat", "bat"] gives [["bat"], ["eat", "tea", "ate"], ["tan", "nat"]].
    • [] gives [].
    • [""] gives [[""]].
    • ["ab", "ba", "cc"] gives [["ab", "ba"], ["cc"]].
    Keep going with TypeScriptAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free