Learn/DSA Patterns
DSA PatternsStringseasy10 min read

Anagram Pattern

Two strings are anagrams if they contain the same characters in the same quantities — regardless of order. Checking anagram equality costs O(n); finding all anagram windows in a longer string costs O(n) using a sliding frequency map. This one idea powers valid-anagram, find-all-anagrams, and grouped-anagrams problems.

#anagram#frequency#sliding-window#hashmap#strings#beginner#interview
Table of contents

Before we start

"Anagram" sounds like a word puzzle, but it is really a disguised frequency-comparison problem. By the end of this chapter you will be able to:

  • Check if two strings are anagrams in three ways (sorted, Counter, array) and know which to choose when.
  • Find all anagram windows inside a longer string in O(n) — the sliding window variant.
  • Connect this to Chapter 3.2's sliding window: anagram detection is just the fixed-size window with a frequency equality check.

Picture this first (no code yet)

A real-life story

A postal worker is sorting letters. She doesn't care what order the letters on an address label are written — she only cares that the total stock of letters matches her template. "LISTEN" and "SILENT" use the exact same six letters, one of each. Same stock, different arrangement — anagrams.

She checks by reading her template stock (1 L, 1 I, 2 Es, 1 T, 1 N... wait, "LISTEN": L, I, S, T, E, N — one of each) and then tallying the incoming label (S, I, L, E, N, T — also one of each). Tallies match — anagram confirmed.

Her tally sheet is the frequency map. Comparing two tally sheets is the algorithm.


The actual problem

Valid Anagram (LC #242):

Given two strings s and t, return True if t is an anagram of s.

"anagram" and "nagaram"True   (same letters, rearranged)
"rat"     and "car"False  ('r','a','t' vs 'c','a','r' — different stock)

Simple to state. Three ways to solve it — and each teaches something different.


First, the slow way (so you feel the pain)

Generate every permutation of s and check if any matches t. For a string of length 10, there are 10! = 3 628 800 permutations. For length 20: 2.4 quintillion. The universe will end before you finish checking n = 20. We can do this in O(n log n) with sorting, or O(n) with frequency counting.


The turning point

Pause & think

You want to check if s = "eat" and t = "tea" are anagrams. Instead of comparing characters directly, what if you described each string as a "recipe" — a list of ingredients and quantities? What does the recipe for "eat" look like? What about "tea"? Are the recipes the same?

Answer

"eat": e×1, a×1, t×1. "tea": t×1, e×1, a×1. Same recipe — just written in a different order. Anagram. The "recipe" is exactly the frequency map. Two strings are anagrams if and only if their frequency maps are identical.


The one idea to remember

The entire pattern in one sentence

Two strings are anagrams if and only if their character frequency maps are equal — so sort both (O(n log n)) or compare frequency arrays (O(n)) to check; for sliding-window anagram finding, maintain a frequency difference and slide in O(n) total.


Three ways to check: side-by-side

Method 1 — Sort both strings

def isAnagram_sort(s: str, t: str) -> bool:
    return sorted(s) == sorted(t)   # O(n log n)

Simplest code. But O(n log n) and allocates new sorted lists.

Method 2 — Counter comparison

from collections import Counter

def isAnagram_counter(s: str, t: str) -> bool:
    return Counter(s) == Counter(t)   # O(n)

O(n) time and space. Pythonic. Counter(s) == Counter(t) compares dictionaries — O(k) where k is distinct characters.

Method 3 — 26-element frequency array

def isAnagram_array(s: str, t: str) -> bool:
    if len(s) != len(t):
        return False
    freq = [0] * 26
    for c in s: freq[ord(c) - ord('a')] += 1
    for c in t: freq[ord(c) - ord('a')] -= 1
    return all(x == 0 for x in freq)

O(n) time, O(1) space (26-element array is constant). Best choice when you need minimum memory, or in languages without built-in Counter.

When to choose which:

ConstraintMethod
Unicode / arbitrary charactersCounter (handles any hashable)
Only lowercase letters, min space26-array
Simplest code, performance acceptablesorted(s) == sorted(t)
Interview defaultCounter or 26-array

Watch it happen, frame by frame (frequency array method)

s = "anagram", t = "nagaram"

Build freq from s:
  a: +1  3 times total
  n: +1
  g: +1
  r: +1
  m: +1
freq after s = [3,0,0,0,0,0,1,0,0,0,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0]
               (a=3, g=1, m=1, n=1, r=1)

Subtract freq from t:
  n: -1, a: -1, g: -1, a: -1, r: -1, a: -1, m: -1
freq after t = [0,0,0,0,0,0,0,...]  all zeros

all(x == 0)  True  

The sliding window variant: Find All Anagrams (LC #438)

Now the problem is: given a long string s and a short pattern p, find all starting indices in s where a substring of length len(p) is an anagram of p.

s = "cbaebabacd",  p = "abc"
Output: [0, 6]
s[0:3] = "cba" — anagram of "abc" ✅
s[6:9] = "bac" — anagram of "abc" ✅

Brute force: for each of n - m + 1 windows, check anagram in O(m) — total O(n × m). For n=100 000 and m=100, that is 10 million checks. We can do O(n) with a sliding frequency map.

The O(n) sliding approach

from collections import Counter

def findAnagrams(s: str, p: str) -> list[int]:
    k      = len(p)
    if k > len(s):
        return []

    need   = Counter(p)          # required frequency for each char in p
    have   = Counter(s[:k])      # frequency of first window
    result = []

    if have == need:
        result.append(0)

    for i in range(k, len(s)):
        # add incoming character on the right
        have[s[i]] += 1

        # remove outgoing character on the left
        old = s[i - k]
        have[old] -= 1
        if have[old] == 0:
            del have[old]          # keep dict clean for correct == comparison

        if have == need:
            result.append(i - k + 1)

    return result

Frame-by-frame: s = "cbaebabacd", p = "abc" (k=3)

need = {a:1, b:1, c:1}

First window s[0:3] = "cba":
have = {c:1, b:1, a:1} == need → result = [0]

i=3, add 'e', remove s[0]='c':
have = {b:1, a:1, e:1}  ≠  need

i=4, add 'b', remove s[1]='b':
have = {a:1, e:1, b:1}  ≠  need

i=5, add 'a', remove s[2]='a':
have = {e:1, b:1, a:1}  ≠  need

i=6, add 'b', remove s[3]='e':
have = {b:2, a:1}  ≠  need

i=7, add 'a', remove s[4]='b':
have = {b:1, a:2}  ≠  need

i=8, add 'c', remove s[5]='a':
have = {b:1, a:1, c:1} == need → result = [0, 6]

i=9, add 'd', remove s[6]='b':
have = {a:1, c:1, d:1}  ≠  need

Answer: [0, 6]  ✅

One pass through s. Each character is added once and removed once — O(n) total.

Pause & think

Why do we del have[old] when have[old] == 0 instead of leaving it as {old: 0}? What breaks if we don't?

Answer

Python's Counter({'a': 1}) == Counter({'a': 1, 'b': 0}) returns False — dictionaries with different key sets are not equal, even if the extra values are 0. If we leave zero-count keys in have, every comparison have == need could return False even when the window is a valid anagram. Deleting the key keeps have clean so the equality check is meaningful.


Variant: Group Anagrams (LC #49)

Covered in Chapter 2.7 (Custom Hash Design). Key technique: canonical key = "".join(sorted(word)). All anagrams map to the same sorted string → same bucket in the dictionary.


Where to spot this pattern

Trigger words:

  • "anagram" or "permutation of"
  • "rearrangement" where order doesn't matter
  • "contains all characters of" — if order doesn't matter, it's anagram; if order matters, it's subsequence (Chapter 3.1)
  • "find all occurrences of any permutation of p in s" — always sliding anagram window

5 disguises:

  1. Valid Anagram (LC #242): direct frequency comparison.
  2. Find All Anagrams in a String (LC #438): sliding fixed window + Counter equality.
  3. Permutation in String (LC #567): "does s2 contain a permutation of s1?" — same sliding window, return True on first match.
  4. Group Anagrams (LC #49): canonical key (sorted string or freq tuple) → group into buckets.
  5. Minimum Window Substring (LC #76): looks like anagram but the window is variable size and must contain all chars of t (not equal count) — different pattern (Chapter 3.2).

Common traps

Watch out for these

  • Not deleting zero-count entries. have[old] -= 1 then if have[old] == 0: del have[old] is mandatory for correct == comparison with need. Omitting the delete is the single most common bug in this pattern.
  • Confusing anagram with subsequence. Anagram: same characters, same counts, any order. Subsequence: characters in order, can skip. "abc" is a subsequence of "aXbYc" but not an anagram of it.
  • Forgetting len(s) != len(t) early return in isAnagram. Two strings of different lengths cannot be anagrams. This early exit avoids building two Counters unnecessarily.
  • Using Counter(s) - Counter(t) for validity check. This gives the "excess" count — if the result is empty, they're equal. It works but is less readable than direct == comparison.

Complexity

OperationTimeSpace
isAnagram (sorted)O(n log n)O(n)
isAnagram (Counter)O(n)O(k)
isAnagram (26-array)O(n)O(1)
Find All AnagramsO(n + m)O(m)
Group AnagramsO(N × k log k)O(N × k)

N = number of words, k = average word length.


Remember this forever

Anagram Pattern — 3 checks

# O(n log n): sorted(s) == sorted(t)
# O(n):       Counter(s) == Counter(t)
# O(n), O(1): 26-array — add for s, subtract for t, check all zeros

Sliding anagram window:

need = Counter(p);  have = Counter(s[:k])
if have == need: result.append(0)
for i in range(k, len(s)):
    have[s[i]] += 1
    old = s[i-k]; have[old] -= 1
    if have[old] == 0: del have[old]   # ← critical!
    if have == need: result.append(i-k+1)

Key trap: always delete zero-count keys — Counter != dict with zeros.


Check yourself

Why is the sliding anagram window O(n) and not O(n × m)?

The naive approach checks anagram equality from scratch for every window — O(m) per window, O(n × m) total. The sliding approach maintains a running frequency map have. Adding one character and removing one character takes O(1) per step (hash map insert/delete). The equality check have == need takes O(k) where k is the number of distinct characters in p (at most 26 for lowercase). So each of the n steps costs O(1) amortised — total O(n).

Can two strings of different lengths be anagrams? What is the fastest way to handle this?

No. If len(s) != len(t), they cannot possibly have identical frequency maps (the total character count differs). The fastest check is if len(s) != len(t): return False at the very top of isAnagram, before building any data structure. This is O(1) — avoids two O(n) Counter builds for the obvious impossible case.


Practice problems

ProblemDifficultyWhat to noticeLink
Valid AnagramEasyThree methods; 26-array for O(1) spaceLC #242
Find All Anagrams in a StringMediumSliding fixed window; delete zero keysLC #438
Permutation in StringMediumSame sliding window; return True on first hitLC #567
Group AnagramsMediumCanonical key; Chapter 2.7 for detailLC #49

Next up: Trie-based String Problems — the prefix tree data structure that answers "does any word in a dictionary start with this prefix?" in O(L) time, and how it powers autocomplete, word search, and longest common prefix.