Skip to content
← All posts

Find All Anagrams in a String: Sliding Window Frequency Match

6 min read
leetcodeproblemmediumstringssliding-windowhash-map

LeetCode's Find All Anagrams in a String (LeetCode 438) asks you to find every position in a string s where a substring of the same length as p is an anagram of p. You return a list of starting indices.

For example, given s = "cbaebabacd" and p = "abc", the answer is [0, 6] because "cba" starting at index 0 and "bac" starting at index 6 are both anagrams of "abc".

If you have worked through Minimum Window Substring or Longest Substring Without Repeating Characters, you already know the sliding window skeleton. This problem is actually simpler because the window size is fixed: it is always len(p). That constraint makes the slide predictable and the logic cleaner.

s = "cbaebabacd", p = "abc"c0b1a2e3b4a5b6a7c8d9start=0start=6Anagram windows found at indices [0, 6]"cba" and "bac" are both anagrams of "abc"
Two windows of length 3 match the character frequencies of p = "abc". Both "cba" (index 0) and "bac" (index 6) are anagrams.

Why this problem matters

This is one of the best problems for learning how to combine two fundamental patterns: the sliding window and the frequency map. You will see both of these building blocks again and again in string problems, and this particular combination shows up in Minimum Window Substring, Permutation in String, and Longest Substring with At Most K Distinct Characters.

The fixed-size window variant here is the gentlest introduction to frequency-based sliding window. Once you internalize this approach, the variable-size versions feel like natural extensions.

The key insight

A naive approach would sort every window of length len(p) and compare it to the sorted version of p. That works, but sorting each window costs O(k log k) where k is the length of p, giving you O(n * k log k) overall.

The insight is that two strings are anagrams if and only if they have the same character frequencies. Instead of sorting, maintain a frequency count for the current window and compare it to the frequency count for p. When you slide the window one position to the right, you remove one character on the left and add one character on the right, doing O(1) work per slide.

The second optimization is the "match count" trick. Instead of comparing the full frequency maps on every slide (which costs O(26) per step), track how many characters currently have matching counts between the window and p. When the match count equals the number of unique characters in p, you have found an anagram. Updating the match count on each slide is O(1).

The solution

def find_anagrams(s, p):
    if len(p) > len(s):
        return []

    from collections import Counter

    p_count = Counter(p)
    window_count = Counter()
    result = []

    required = len(p_count)  # unique chars in p
    formed = 0               # chars with matching counts

    for i in range(len(s)):
        # Add the new character on the right
        ch = s[i]
        window_count[ch] += 1

        # Check if this character's count now matches p
        if ch in p_count and window_count[ch] == p_count[ch]:
            formed += 1

        # Remove the character sliding out on the left
        if i >= len(p):
            left_ch = s[i - len(p)]
            if left_ch in p_count and window_count[left_ch] == p_count[left_ch]:
                formed -= 1
            window_count[left_ch] -= 1

        # If all character counts match, record this index
        if formed == required:
            result.append(i - len(p) + 1)

    return result

Let's walk through the important parts:

  1. p_count is a Counter that tells you how many of each character you need. For p = "abc", that is {a: 1, b: 1, c: 1}.
  2. required is the number of unique characters in p. We need formed to reach this number.
  3. On each step, we add s[i] to the window. If i >= len(p), we also remove the character that just slid out the left side.
  4. The formed counter goes up when a character's window count reaches its required count, and goes down when a character's window count drops below its required count.
  5. When formed == required, every character in the window matches the pattern frequencies, so we record the starting index.

The match count approach (formed == required) is faster than comparing the full frequency maps on every step. Comparing two Counter objects costs O(26) per comparison. The match count approach does O(1) work per slide because you only check the one character that changed. This same trick appears in Minimum Window Substring and Permutation in String.

Visual walkthrough

Step 1: Build frequency map for pattern "abc". Need a:1, b:1, c:1.

cbaebabacda:0/1 b:0/1 c:0/1matches = 0 / 3

Pattern frequency: a:1, b:1, c:1. We need all 3 characters matched to find an anagram.

Step 2: Initialize window [0..2] = "cba". Compare frequencies.

cbaebabacdleftrighta:1/1 b:1/1 c:1/1matches = 3 / 3

All 3 characters match! "cba" is an anagram. Record index 0. Result: [0]

Step 3: Slide window right. Remove s[0]="c", add s[3]="e". Window = "bae".

cbaebabacdleftrighta:1/1 b:1/1 c:0/1matches = 2 / 3

Lost c, gained e. Only a and b match. Not an anagram.

Step 4: Slide again. Remove s[1]="b", add s[4]="b". Window = "aeb".

cbaebabacdleftrighta:1/1 b:1/1 c:0/1matches = 2 / 3

Swapped one b for another, still have e instead of c. Not an anagram.

Step 5: Slide. Remove s[2]="a", add s[5]="a". Window = "eba".

cbaebabacdleftrighta:1/1 b:1/1 c:0/1matches = 2 / 3

Swapped a for a. Still have e, missing c. Not an anagram.

Step 6: Slide. Remove s[3]="e", add s[6]="b". Window = "bab".

cbaebabacdleftrighta:1/1 b:2/1 c:0/1matches = 1 / 3

b count is 2 (need 1), c missing. Only a matches. Not an anagram.

Step 7: Slide. Remove s[4]="b", add s[7]="a". Window = "aba".

cbaebabacdleftrighta:2/1 b:1/1 c:0/1matches = 1 / 3

a count is 2 (need 1), c missing. Only b matches. Not an anagram.

Step 8: Slide. Remove s[5]="a", add s[8]="c". Window = "bac". All match!

cbaebabacdleftrighta:1/1 b:1/1 c:1/1matches = 3 / 3

All 3 characters match! "bac" is an anagram. Record index 6. Result: [0, 6]

Step 9: Slide. Remove s[6]="b", add s[9]="d". Window = "acd". Done.

cbaebabacdleftrighta:1/1 b:0/1 c:1/1matches = 2 / 3

Lost b, gained d. End of string reached. Final result: [0, 6]

Notice how each step does exactly two things: remove one character from the left side of the window and add one character on the right. The frequency map updates incrementally, never rebuilt from scratch. The match count tells you instantly whether the window is a valid anagram.

Complexity analysis

ApproachTimeSpace
Sort each windowO(n * k log k)O(k)
Compare full frequency maps each stepO(26n)O(1)
Sliding window with match countO(n)O(1)

Time: O(n) where n is the length of s. Each character is added to the window once and removed once. Each addition and removal does O(1) work to update the frequency count and match counter.

Space: O(1). The frequency maps hold at most 26 entries (lowercase English letters). This is constant regardless of input size.

The O(26n) approach is technically O(n) too, but the match count optimization removes that constant factor of 26 per step, which matters in practice on large inputs.

Edge cases

Before submitting, verify your solution handles:

  • p longer than s: return []. No window of length len(p) can fit.
  • p and s are identical: return [0]. The entire string is one anagram.
  • Single character: s = "a", p = "a". Return [0].
  • No anagrams exist: s = "abcdef", p = "zz". Return [].
  • Overlapping anagrams: s = "aaa", p = "aa". Return [0, 1]. Two overlapping windows both match.
  • All characters the same: s = "aaaa", p = "aa". Return [0, 1, 2]. Every window of length 2 is an anagram.

The building blocks

This problem decomposes into two reusable pieces that CodeBricks drills independently:

1. Fixed-size sliding window

The mechanics of sliding a window of constant width across a string. On each step you add one character on the right and remove one on the left. Both pointers advance together, so there is no shrink-while-valid or expand-while-invalid logic. This is the simplest variant of sliding window and the one you should learn first.

for i in range(len(s)):
    # add s[i] to window
    if i >= window_size:
        # remove s[i - window_size] from window
    # check window

This same fixed-size pattern appears in Maximum Sum Subarray of Size K and Sliding Window Maximum.

2. Frequency map with match counting

The technique of maintaining character counts for the window and tracking how many characters have matching frequencies. Instead of comparing entire maps, you update a single counter whenever a character's count crosses the threshold. This is O(1) per update and tells you immediately whether the window satisfies the condition.

# When adding a character
if ch in target and window[ch] == target[ch]:
    formed += 1

# When removing a character
if ch in target and window[ch] == target[ch]:
    formed -= 1
window[ch] -= 1

This same building block powers Minimum Window Substring and Permutation in String. The only difference is the window size: fixed here, variable there.

From understanding to recall

Reading through this solution is the easy part. Reproducing it from memory under time pressure is where most people struggle. The formed counter trick, the order of operations when removing a character (check before decrementing), and the fixed-window index math are all details that fade without practice. Spaced repetition locks them in by having you write the code at increasing intervals until the pattern is automatic.

Related posts