015. Group Anagrams
Problem
Group strings that have the same character multiset. Preserve input order within each group and order groups by the first occurrence of their signature. Duplicate words remain duplicated. Characters are case-sensitive; return [] for an empty word list.
Examples
Example 1
group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"])
=> [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]Group and member order follow first encounters.
Example 2
group_anagrams([])
=> []There are no groups.
Example 3
group_anagrams(["", "", "a"])
=> [['', ''], ['a']]Empty words share the empty signature.
Reveal the worked solution and reasoning
Why this mechanism works
Convert an equivalence relation into a canonical key. Sorting a word's characters yields the same tuple exactly when its multiplicities match another word's. A dictionary then gathers each equivalence class without comparing every pair of words.
State and boundaries
groups maps a sorted-character tuple to the words seen with that signature, in encounter order.
Protocol
- Start an empty dictionary.
- Sort each word's characters into a tuple.
- Create its group if absent.
- Append the original word.
- Return dictionary groups in insertion order.
Visualization
eat, tea, ate -> ('a','e','t')
tan, nat -> ('a','n','t')
bat -> ('a','b','t')Python implementation
def group_anagrams(words):
groups = {}
for word in words:
key = tuple(sorted(word))
groups.setdefault(key, []).append(word)
return list(groups.values())
Worked execution
group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"])
L2 group_anagrams: groups = {}
words=['eat', 'tea', 'tan', 'ate', 'nat', 'bat']; groups={}
L3 group_anagrams: for word in words:
words=['eat', 'tea', 'tan', 'ate', 'nat', 'bat']; groups={}; word='eat'
L4 group_anagrams: key = tuple(sorted(word))
words=['eat', 'tea', 'tan', 'ate', 'nat', 'bat']; groups={}; word='eat'; key=('a', 'e', 't')
L5 group_anagrams: groups.setdefault(key, []).append(word)
words=['eat', 'tea', 'tan', 'ate', 'nat', 'bat']; groups={('a', 'e', 't'):['eat']}; word='eat'; key=('a', 'e', 't')
L3 group_anagrams: for word in words:
words=['eat', 'tea', 'tan', 'ate', 'nat', 'bat']; groups={('a', 'e', 't'):['eat']}; word='tea'; key=('a', 'e', 't')
L5 group_anagrams: groups.setdefault(key, []).append(word)
words=['eat', 'tea', 'tan', 'ate', 'nat', 'bat']; groups={('a', 'e', 't'):['eat', 'tea']}; word='tea'; key=('a', 'e', 't')
L3 group_anagrams: for word in words:
words=['eat', 'tea', 'tan', 'ate', 'nat', 'bat']; groups={('a', 'e', 't'):['eat', 'tea']}; word='tan'; key=('a', 'e', 't')
L4 group_anagrams: key = tuple(sorted(word))
words=['eat', 'tea', 'tan', 'ate', 'nat', 'bat']; groups={('a', 'e', 't'):['eat', 'tea']}; word='tan'; key=('a', 'n', 't')
... 6 intermediate state changes omitted ...
L5 group_anagrams: groups.setdefault(key, []).append(word)
words=['eat', 'tea', 'tan', 'ate', 'nat', 'bat']; groups={('a', 'e', 't'):['eat', 'tea', 'ate'], ('a', 'n', 't'):['tan', 'nat']}; word='nat'; key=('a', 'n', 't')
L3 group_anagrams: for word in words:
words=['eat', 'tea', 'tan', 'ate', 'nat', 'bat']; groups={('a', 'e', 't'):['eat', 'tea', 'ate'], ('a', 'n', 't'):['tan', 'nat']}; word='bat'; key=('a', 'n', 't')
L4 group_anagrams: key = tuple(sorted(word))
words=['eat', 'tea', 'tan', 'ate', 'nat', 'bat']; groups={('a', 'e', 't'):['eat', 'tea', 'ate'], ('a', 'n', 't'):['tan', 'nat']}; word='bat'; key=('a', 'b', 't')
L5 group_anagrams: groups.setdefault(key, []).append(word)
words=['eat', 'tea', 'tan', 'ate', 'nat', 'bat']; groups={('a', 'e', 't'):['eat', 'tea', 'ate'], ('a', 'n', 't'):['tan', 'nat'], ('a', 'b', 't'):['ba...; word='bat'; key=('a', 'b', 't')Result: [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]. Group and member order follow first encounters.
group_anagrams([])
L2 group_anagrams: groups = {}
words=[]; groups={}Result: []. There are no groups.
Why the solution is correct
Equal keys imply equal sorted sequences and therefore equal character counts. Anagrams necessarily produce equal keys. Each word is appended once to its unique correct group, and dictionary insertion order enforces the requested group ordering.
Time and space
O(sum(L_i log L_i)) sorting time, plus O(sum L_i) key/output-reference storage in the worst case.
Common mistakes
A list cannot be a dictionary key. Using a set signature incorrectly groups ab with aab.
Reasoning card
State: groups maps a sorted-character tuple to the words seen with that signature, in encounter order.
Invariant: Words sharing a key are equivalent under the chosen signature.
Decision to rehearse: Create a canonical hashable signature.
Finish: Return dictionary groups in insertion order
Transfer exercise
For lowercase English letters only, replace sorting with a 26-count tuple and derive the new cost.