Last Updated: April 2, 2026
We need to find every position in s where a contiguous substring of length p.length() is an anagram of p. Two strings are anagrams if they contain exactly the same characters with the same frequencies, just in a different order.
The key insight is that we are always looking at substrings of a fixed length (the length of p). This means the window size never changes, which makes this a perfect candidate for a fixed-size sliding window. The challenge is figuring out how to efficiently check whether the current window is an anagram of p without doing expensive work at every position.
1 <= s.length, p.length <= 3 * 10^4 → With n up to 30,000, an O(n k) approach (where k is the length of p) is fine, but O(n k log k) sorting at every position is borderline. An O(n) solution is ideal.s and p consist of lowercase English letters → Only 26 possible characters. This means we can use a fixed-size array of 26 instead of a hash map, keeping space constant.The simplest way to check if two strings are anagrams is to sort them both and see if the sorted versions are identical. So for every possible substring of s with length equal to p, we can extract it, sort it, and compare it with the sorted version of p.
This is the approach most people think of first. It works, but sorting each substring is expensive.
p to get sortedP.i from 0 to s.length() - p.length():s[i..i+p.length()).sortedP, add i to the result list.s and k is the length of p. We check n - k + 1 substrings, and sorting each one takes O(k log k).The bottleneck here is sorting every substring from scratch. Each window overlaps with the previous one by k - 1 characters, but we throw all that information away and re-sort. What if we could update the window's character information incrementally instead of recomputing it?
Instead of sorting, we can represent each string by its character frequency count. Two strings are anagrams if and only if their frequency counts are identical. We build a frequency map for p once, then slide a fixed-size window across s, maintaining a frequency map for the current window. When the window slides right by one position, we add the new character and remove the old one, then compare the two maps.
The comparison itself takes O(26) = O(1) time since there are only 26 lowercase letters. This is already much better than sorting.
pCount of size 26 for p.windowCount of size 26 for the first window s[0..p.length()).windowCount equals pCount, add index 0 to the result.s.length() - p.length():windowCount with pCount. If they match, add the current start index.s. We slide the window across s once. At each step, we do O(1) work to update the window and O(26) = O(1) work to compare the two frequency arrays.This approach is already O(n), but at every window position we compare all 26 entries of the two frequency arrays. What if we tracked how many characters already match so we could skip the full comparison and just update the match count incrementally?
Building on the frequency map idea, we can avoid comparing all 26 entries at every step. The trick is to maintain a matches counter that tracks how many of the 26 characters currently have the same frequency in both the window and p.
When the window slides, at most two character frequencies change (the one entering and the one leaving). So we only need to check those two characters against their target frequency and update matches accordingly. When matches reaches 26, every character frequency lines up and we have an anagram.
This is a classic optimization pattern: instead of recomputing a global property from scratch, maintain it incrementally by handling only what changed.
The matches counter is the key insight. After incrementing or decrementing a character's count in the window, there are only two interesting cases:
matches++.matches--.We detect the "lost a match" case by checking if the new count is exactly one more (for adds) or one less (for removes) than the target. If the count was already far from the target, neither condition triggers and matches stays the same.
When matches is 26, all characters (including those not in p, which both have count 0) have matching frequencies, meaning the window is an anagram.
pCount and windowCount for p and the first window.matches by counting how many indices (0 to 25) have pCount[i] == windowCount[i].c_in. Increment windowCount[c_in]. If this made the counts equal, increment matches. If this broke a previous match, decrement matches.c_out. Decrement windowCount[c_out]. Apply the same match logic.matches == 26, add the current start index to the result.s. We iterate through s once. At each step, we do O(1) work: two array updates and at most four comparisons.