Skip to content

How to Check Anagrams Using Counter in Python

Learn how to efficiently detect anagrams in Python using collections.Counter for O(n) time complexity, including handling case sensitivity, spaces, and punctuation.

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"))       # False

How 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"))          # True

Finding 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.7x

Early 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.
Navigation