LeetCode #438 Medium

Find All Anagrams in a String

Given two strings s and p, return the start indices of all occurrences of p's anagrams in s.

Constraints
  • 1 <= s.length, p.length <= 3 * 10⁴
  • s and p consist of lowercase English letters.
stringsliding-windowhash-table
Open on LeetCode ↗
02

Intuition

Find all anagrams in a string returns every starting index where a substring of s is an anagram of p. Since anagrams have equal length, only windows of length p.length can qualify — which immediately suggests a fixed-size sliding window. Checking each window by sorting or building a fresh count is O(n × m). The improvement comes from reusing work between adjacent windows: - Consecutive windows differ by exactly two characters — one leaves the left, one enters the right — so update the counts rather than recomputing them. So each window costs O(1) to maintain instead of O(m). Comparing frequency maps at every position still costs O(26) per window, which is acceptable but avoidable. A match counter removes even that: track how many of the 26 letters currently have exactly the right count, and a window is an anagram when that counter reaches 26. Updating the counter correctly is where care is needed. When a character's count changes, check whether it just became correct or just stopped being correct, comparing against the target before and after the change. Testing only one side leaves the counter permanently wrong. The simpler comparison version is easier to get right and fast enough for the constraints — worth preferring unless the counter version is written carefully. The first window must be built before the loop begins, then slid one position at a time. Anagrams need not be contiguous in p, only equal in multiset, which is exactly what the counts capture.

How to spot this pattern

The same fixed-width window as Permutation in String, collecting every match instead of returning on the first. The starting index is r - k + 1, since r is the window's right edge.

03

Approach

Try it first

Before reading on: price up what counting everything costs here, then ask what makes a window invalid, and which pointer should move when it is. Aim for O(n) time and O(1) space.

1

Fix the window size

Anagrams have equal length, so only windows of length p.length can match. The window size never changes, which is what makes the sliding update possible.

2

Build the target counts

Count each character's frequency in p. A window matches when its own counts equal these exactly — order within the window is irrelevant.

3

Slide by updating, not recomputing

Adjacent windows differ by two characters. Decrement the outgoing character and increment the incoming one, making each step O(1) instead of O(m).

4

Compare counts per window

With a 26-letter alphabet, comparing the two count arrays is O(26) — constant. This version is simple and fast enough for the constraints.

5

Or track a match counter

Count how many letters have exactly the right frequency. Check whether each change just made a letter correct or just broke it — testing only one side leaves the counter permanently wrong.

6

Record the starting index

When a window matches, append its left index. The problem asks for start positions, not the substrings themselves.

7

Cost of the scan

Each character enters and leaves the window once, giving O(n) time with the counter, or O(26n) with array comparison. Space is O(1) for the fixed alphabet.

04

Solution & live demo

▶1class Solution:
▶2 def findAnagrams(self, s, p):
▶3 from collections import Counter
▶4 k = len(p)
▶5 if k > len(s):
▶6 return []
▶7 need = Counter(p)
▶8 have = Counter()
▶9 matches = 0
▶10 need_keys = len(need)
▶11 res = []
▶12 for r in range(len(s)):
▶13 ch = s[r]
▶14 have[ch] += 1
▶15 if have[ch] == need.get(ch, 0):
▶16 matches += 1
▶17 if r >= k:
▶18 left_ch = s[r - k]
▶19 if have[left_ch] == need.get(left_ch, 0):
▶20 matches -= 1
▶21 have[left_ch] -= 1
▶22 if r >= k - 1 and matches == need_keys:
▶23 res.append(r - k + 1)
▶24 return res
05

Common pitfalls

Recording the right index instead of the left

✗ Wrong
res.append(r)
✓ Right
res.append(r - k + 1)

The answer is the anagram's starting position. Reporting the right edge shifts every index by k - 1, which looks plausible until compared against expected output.

Rebuilding the counter for each window

✗ Wrong
for i in range(len(s) - k + 1):
    if Counter(s[i:i+k]) == need: res.append(i)
✓ Right
have[ch] += 1
...
have[left_ch] -= 1

Slicing and counting per position is O(nk). The sliding window adds one character and removes one, so each step is constant regardless of k.

Not guarding against p being longer than s

✗ Wrong
need = Counter(p)
for r in range(len(s)):
✓ Right
if k > len(s):
    return []

The window can never reach full width, so r >= k - 1 is never true and the loop runs pointlessly — and any index arithmetic on r - k would be negative. Returning early is both correct and clearer.

06

Edge cases

p longer than s

No window of that width exists, so the results list stays empty; the loop condition naturally prevents any comparison from firing.

Every window in s is an anagram of p (e.g. s='abab', p='ab')

Overlapping windows at consecutive start indices are all recorded since the scan never skips ahead after a match.

p has repeated characters

The need map's counts greater than 1 are handled the same way as unique characters -- matches is keyed on distinct letters reaching their required count, not on total character count.

No anagram of p appears in s

matches never reaches the needed key count for any window, and the function returns an empty list.

07

Complexity

Time
O(n)
Space
O(1)
n is len(s); each character enters and leaves the window once, count maps bounded by the 26-letter alphabet.