Checking if two strings are anagrams, containing the same characters in different orders, is a classic programming problem. While sorting works, using a hash map is faster with O(n) complexity compared to sorting's O(n log n).
Python's collections.Counter provides this hash map implementation out of the box.
Basic Anagram Check with Counter
Counter objects support direct equality comparison, making anagram detection a one-liner:
from collections import Counter
def is_anagram(s1: str, s2: str) -> bool:
"""Check if two strings are anagrams."""
return Counter(s1) == Counter(s2)
print(is_anagram("listen", "silent")) # True
print(is_anagram("triangle", "integral")) # True
print(is_anagram("hello", "world")) # FalseHow It Works
Counter creates a frequency dictionary for each string:
from collections import Counter
print(Counter("listen"))
print(Counter("silent"))Output:
Counter({'l': 1, 'i': 1, 's': 1, 't': 1, 'e': 1, 'n': 1})
Counter({'s': 1, 'i': 1, 'l': 1, 'e': 1, 'n': 1, 't': 1})Handling Real-World Input
Real-world strings often have different casing, spaces, or punctuation. A robust solution must normalize the input:
from collections import Counter
def is_anagram_robust(s1: str, s2: str) -> bool:
"""Check anagrams ignoring case, spaces, and punctuation."""
clean_s1 = ''.join(c.lower() for c in s1 if c.isalnum())
clean_s2 = ''.join(c.lower() for c in s2 if c.isalnum())
return Counter(clean_s1) == Counter(clean_s2)
print(is_anagram_robust("Clint Eastwood", "Old West Action")) # True
print(is_anagram_robust("Dormitory", "Dirty Room")) # True
print(is_anagram_robust("The Morse Code", "Here Come Dots")) # True
print(is_anagram_robust("Astronomer", "Moon Starer")) # TrueFinding Anagram Groups
Group words that are anagrams of each other:
from collections import Counter, defaultdict
def group_anagrams(words: list) -> list:
"""Group words that are anagrams of each other."""
groups = defaultdict(list)
for word in words:
key = tuple(sorted(word.lower()))
groups[key].append(word)
return [group for group in groups.values() if len(group) > 1]
words = ["listen", "silent", "enlist", "hello", "world", "inlets", "tinsel"]
anagram_groups = group_anagrams(words)
for group in anagram_groups:
print(group)Output:
['listen', 'silent', 'enlist', 'inlets', 'tinsel']Using Counter as Key
from collections import Counter
def group_anagrams_counter(words: list) -> list:
"""Group anagrams using Counter as a hashable key."""
groups = {}
for word in words:
key = frozenset(Counter(word.lower()).items())
if key not in groups:
groups[key] = []
groups[key].append(word)
return [g for g in groups.values() if len(g) > 1]Finding All Anagrams of a Word
Check which words from a list are anagrams of a target:
from collections import Counter
def find_anagrams(target: str, word_list: list) -> list:
"""Find all anagrams of target in word_list."""
target_counter = Counter(target.lower())
return [
word for word in word_list
if Counter(word.lower()) == target_counter and word.lower() != target.lower()
]
dictionary = ["listen", "silent", "enlist", "google", "tinsel", "inlets"]
anagrams = find_anagrams("listen", dictionary)
print(f"Anagrams of 'listen': {anagrams}")Output:
Anagrams of 'listen': ['silent', 'enlist', 'tinsel', 'inlets']Counter vs Sorting Comparison
Both approaches work, but Counter is faster:
from collections import Counter
def is_anagram_counter(s1: str, s2: str) -> bool:
"""O(n) - Single pass through each string."""
return Counter(s1) == Counter(s2)
def is_anagram_sorted(s1: str, s2: str) -> bool:
"""O(n log n) - Requires sorting."""
return sorted(s1) == sorted(s2)Performance Comparison
import time
from collections import Counter
s1 = "a" * 100_000 + "b" * 100_000
s2 = "b" * 100_000 + "a" * 100_000
start = time.perf_counter()
result1 = Counter(s1) == Counter(s2)
counter_time = time.perf_counter() - start
start = time.perf_counter()
result2 = sorted(s1) == sorted(s2)
sorted_time = time.perf_counter() - start
print(f"Counter: {counter_time:.4f}s")
print(f"Sorted: {sorted_time:.4f}s")
print(f"Speedup: {sorted_time / counter_time:.1f}x")Typical Output:
Counter: 0.0082s
Sorted: 0.0054s
Speedup: 0.7xEarly Exit Optimization
For non-anagrams, check length first to avoid unnecessary computation:
from collections import Counter
def is_anagram_optimized(s1: str, s2: str) -> bool:
"""Optimized anagram check with early exit."""
if len(s1) != len(s2):
return False
return Counter(s1) == Counter(s2)Complexity Comparison
| Method | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
Counter |
O(n) | O(k) | Fastest, single pass |
sorted() |
O(n log n) | O(n) | Simpler but slower |
| Manual dict | O(n) | O(k) | Same as Counter, more verbose |
n = string length, k = unique characters
Summary
- Use
Counter(s1) == Counter(s2)for the fastest, most Pythonic solution. - Normalize input (lowercase, remove spaces/punctuation) for real-world text.
- Check length first as an early exit optimization.
- Counter is O(n) compared to sorting's O(n log n), making it significantly faster for long strings.