DSA sheet · Strings · Sliding Window pattern (fixed size + frequency map)
Find All Anagrams in a String
This is the first sliding-window question of the strings section, and the teacher spends two videos on it. In Part 1 she explains what an anagram is, reads the constraints, and writes a brute force: cut out every substring of length len(p), count its letters, and compare the counts with p's counts. She also compares "sort and compare" against "count and compare", and does a careful time-complexity analysis with different sizes. In Part 2 she says why this is a sliding-window problem and solves it in O(n) two ways: first with two frequency maps (easier to understand), then with one map (less space, the one interviewers like).
Why it matters: "find every window of s that is a rearrangement of p" is the parent of a whole family: Permutation in String (problem 13) is the same code returning True/False, and Minimum Window Substring (problem 14) uses the same "count of still-needed characters" idea with a variable window.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · Sliding window from scratch + frequency maps + what an anagram is
- Part A · Brute force: count every substring of length k (video 1)
- Part B · Sliding window with two maps (video 2)
- Part C · Sliding window with one map (video 2)
- Part D · Revision page
Part 0 · Before starting
Substring / subarray = a contiguous piece
A substring (for strings) or subarray (for arrays) is a piece whose characters sit next to each other. You pick a start index and an end index and take everything between them. You may not skip a character in the middle.
The brute force over all windows, and why it repeats work
Many questions say "find the best / count all substrings (or subarrays) that satisfy some rule". The simplest answer is two loops: the outer loop picks the start, the inner loop walks the end forward and re-checks the piece. That is about n² pieces, so O(n²) (or worse, if each check is itself a loop).
The waste: two neighbouring pieces share almost everything. "cba" and "bae" share "ba". The brute force throws away what it learned about "ba" and counts it again from zero. Sliding window keeps the shared middle and only updates the two ends.
The window [left..right]
A window is the current substring, described by two indexes: left (its first character) and right (its last character). Its length is right − left + 1. Two moves change it:
- Expand: move
rightone step forward. One new character enters, so we add it to whatever we are tracking (a sum, a count, a map). - Shrink: move
leftone step forward. One old character leaves, so we undo exactly what we did when it entered.
The teacher's short version: right is responsible for expanding, left is responsible for shrinking, and left always undoes what right did. Both pointers only move forward, never back. Each one walks the string at most once, so the whole thing is about 2n steps = O(n).
Fixed-size vs variable-size windows
| Fixed size (size k) | Variable size | |
|---|---|---|
| when | the question tells you the length (here: exactly len(p)) | the question asks for the longest / shortest / number of windows that obey a rule |
| moves | expand by 1; if the length becomes k+1, shrink by exactly 1 | expand by 1; then while the window is invalid (or, for "shortest", while it is still valid) shrink |
| shrink uses | if (one step is always enough) | while (may need many steps) |
| this page | yes, every anagram has length len(p) | problems 3–8, 11, 12, 14 |
def fixed_window(arr, k):
out = []
left = 0
for right in range(len(arr)):
# 1. add arr[right] to the window's data
if right - left + 1 > k: # window became k+1 long
# 2. remove arr[left] from the window's data
left += 1 # exactly one step, so `if` not `while`
if right - left + 1 == k:
out.append((left, right)) # 3. use the full window
return outLongest valid, shortest valid, count of valid (for the variable kind)
- Longest valid: grow right; while the window breaks the rule, shrink left; then the window is valid →
best = max(best, right − left + 1). - Shortest valid: grow right; while the window still satisfies the rule, record its length and shrink left to try to make it shorter.
- Count of valid: after shrinking, every window that ends at
rightand starts anywhere fromlefttorightis valid, socount += right − left + 1. - Exactly K: "exactly K" is hard to slide directly, so use
exactly(K) = atMost(K) − atMost(K − 1)(used in problems 8 and 12).
Why sliding window needs a "monotonic" rule
Shrinking is only safe if removing a character can only move the window towards valid (and adding can only move it away, or the other way round, but always the same way). With positive numbers and a sum rule this holds: add → bigger, remove → smaller. With negative numbers it breaks (removing a negative makes the sum bigger), and sliding window gives wrong answers. For this problem the window length is fixed, so we never have to decide "should I shrink?" based on the data at all, which is why it is the easiest kind.
(One more tool you'll meet in problem 9, Sliding Window Maximum: a monotonic deque that keeps window elements in decreasing order so the max is always at the front. Not needed here.)
Frequency map (Counter)
A frequency map stores character → how many times it appears. In Python a dict works, and collections.Counter builds one in a single line.
from collections import Counter
freq = {}
for ch in "abca":
freq[ch] = freq.get(ch, 0) + 1 # {'a': 2, 'b': 1, 'c': 1}
same = Counter("abca") # Counter({'a': 2, 'b': 1, 'c': 1})
arr = [0] * 26 # only lowercase letters
for ch in "abca":
arr[ord(ch) - ord('a')] += 1 # index 0 = 'a', 1 = 'b', ... 25 = 'z'freq.get(ch, 0) means "the count of ch, or 0 if ch isn't in the map yet" (Java's getOrDefault). The 26-slot list works because the question says only lowercase English letters appear: ord(ch) − ord('a') turns 'a' into 0, 'b' into 1, … 'z' into 25.
What is an anagram?
An anagram of a word is any rearrangement (permutation) of its letters, using every letter exactly as many times as in the original. For "abc" the anagrams are: abc, acb, bac, bca, cab, cba (3! = 6 of them). Two strings are anagrams of each other exactly when they have the same length and the same frequency map. The order of letters doesn't matter, only the counts.
Part A · Brute force: count every substring of length k
LeetCode 438 · from video 1
1The question in simple words
You get two strings, s (long) and p (short). Find every place in s where a substring is an anagram of p, and return the list of starting indexes of those substrings.
"cba" starts at index 0 and is a rearrangement of "abc". "bac" starts at index 6 and is too. No other length-3 piece uses exactly one a, one b and one c. So the answer is [0, 6]. The question wants the start index of each anagram, not the substrings themselves.
2What the constraints tell us
- 1 ≤ len(s), len(p) ≤ 3·10⁴. The teacher's rule of thumb: up to about 10⁸ simple operations is safe; 2–5·10⁸ sometimes passes if the logic is very light; beyond that (7·10⁸, 8·10⁸, 9·10⁸…) you get TLE. An O(n²) solution here could reach (3·10⁴)² = 9·10⁸, so even simple O(n²) code is in danger. We will need to optimise, but she writes the brute force first.
- Only lowercase English letters. So there are only 26 possible characters, and a fixed 26-slot count array is enough. Its size never grows with n, so it counts as O(1) space.
- p can be longer than s. Then no substring of s can have p's length, and the answer is an empty list.
3Intuition: cut out every piece of length k and check it
Let k = len(p). An anagram of p must have length k, so we only care about substrings of length exactly k. Line them up: "cba", "bae", "aeb", "eba", "bab", "aba", "bac", "acd". For each one, ask "is this a rearrangement of p?" If yes, remember where it started.
4Building the logic from examples
How do we check "is this piece an anagram of p"? Two ways
- Sort both and compare: sorted("cba") = "abc" = sorted("abc") ✓. No extra array needed, but every check costs a sort.
- Count both and compare: make a 26-slot count array for p (
p_count) and one for the piece (s_count). For p = "abc": a → 1, b → 1, c → 1, all other slots 0. For the piece "cba": c → 1, b → 1, a → 1. The two arrays are equal → anagram ✓.
→ The teacher picks counting. Sorting takes no extra space, but each sort of a k-long piece costs k·log k, and we sort about n pieces, so it adds an extra log k factor of time. Counting a piece costs only k steps (plus a 26-step compare). We still have to fill and compare the array, so counting isn't free, but it removes that log factor. Time matters more here than 26 slots of memory.
Where can a piece start?
Call the start index i. The piece is s[i], s[i+1], …, s[i+k−1]. The last legal start is the one where the piece ends exactly at the last index n−1, so i + k − 1 = n − 1, which means i = n − k. So i runs from 0 to n − k, both included, which is n − k + 1 pieces. For s = "cbaebabacd" (n = 10), k = 3: i goes 0…7, 8 pieces. If you start at index 8, only two letters ("cd") are left, so it can't hold a 3-letter anagram.
i + k or i + k − 1?→ Both are the same idea. The piece's last index is i + k − 1 (included). In a loop that stops before its end value, you write the end as i + k. In Python:
for j in range(i, i + k) visits i … i+k−1.→ The start, which is
i. That's exactly what the question asks for. So the outer pointer i doubles as "the answer if this piece works".→ In the video she says i goes "till n − k". Make sure the last start n − k is actually tried: in Python that's
range(n − k + 1). With range(n − k) you would miss an anagram at the very end, e.g. s = "xab", p = "ab" (answer [1]). The tests check this.5Approach steps
- Make
result = [], and let n = len(s), k = len(p). - Build
p_count: 26 zeros, then +1 for every letter of p. - For every start i from 0 to n − k: make a fresh
s_countof 26 zeros and count s[i … i+k−1] into it. - If
s_count == p_count, append i to result. - Return result.
6Code (Python)
class Solution:
def findAnagrams(self, s, p):
result = []
n, k = len(s), len(p)
p_count = [0] * 26
for ch in p:
p_count[ord(ch) - ord('a')] += 1
for i in range(n - k + 1): # every legal start (empty if k > n)
s_count = [0] * 26 # fresh counts for this piece
for j in range(i, i + k): # s[i .. i+k-1]
s_count[ord(s[j]) - ord('a')] += 1
if s_count == p_count: # same letters, same counts
result.append(i) # store the START index
return resultclass Solution:
def findAnagrams(self, s, p):
k = len(p)
target = sorted(p)
return [i for i in range(len(s) - k + 1)
if sorted(s[i:i + k]) == target]7Code line by line
| line | what it means |
|---|---|
| result = [] | The list of start indexes we will return. |
| n, k = len(s), len(p) | k is the length every anagram must have. |
| p_count = [0] * 26 for ch in p: ... += 1 | p's frequency array. Slot 0 is 'a', slot 25 is 'z'. Built once. |
| for i in range(n - k + 1): | Try every start from 0 to n−k. If k > n this range is empty, so we return [] automatically. |
| s_count = [0] * 26 | A new count array for each piece. If you reused the old one, letters from the previous piece would leak in. |
| for j in range(i, i + k): | Walk the k letters of this piece. |
| if s_count == p_count: | Python compares the two lists slot by slot (26 checks). |
| result.append(i) | i is the start of the anagram. |
8Dry run (hand table)
s = "cbaebabacd", p = "abc", so p_count has a:1, b:1, c:1. Only non-zero slots are shown.
| i | piece s[i..i+2] | s_count (non-zero) | equal to p_count? | result |
|---|---|---|---|---|
| 0 | cba | a:1 b:1 c:1 | yes | [0] |
| 1 | bae | a:1 b:1 e:1 | no (e, no c) | [0] |
| 2 | aeb | a:1 b:1 e:1 | no | [0] |
| 3 | eba | a:1 b:1 e:1 | no | [0] |
| 4 | bab | a:1 b:2 | no (two b) | [0] |
| 5 | aba | a:2 b:1 | no (two a) | [0] |
| 6 | bac | a:1 b:1 c:1 | yes | [0, 6] |
| 7 | acd | a:1 c:1 d:1 | no | [0, 6] |
Final answer: [0, 6] ✓. Notice how rows 1, 2, 3 count "a", "b", "e" over and over. That repetition is what Part B removes.
9Complexity & remember
The teacher works out the time by trying different sizes, to show that "plug in the biggest k" is not automatically the worst case.
- Building p_count costs k steps. The outer loop runs n − k + 1 times; each time the inner loop runs k times (plus 26 for the compare). Total ≈ (n − k + 1) · k.
- k almost equal to n (say both 10⁴): the outer loop runs only a handful of times, the inner one about n → roughly 2n steps. That's a good case, even though k is huge.
- n = 10⁴, k = 10³: outer ≈ 9,000, inner = 1,000 → 9·10⁶ steps. That's already "near n²" behaviour.
- My addition: the product (n − k)·k is biggest when k ≈ n/2, giving about n²/4. With n = 3·10⁴ that's ≈ 2.25·10⁸, right at the danger line. So call it O(n·k), which is O(n²) in the worst case.
- Space O(1): two arrays of 26, fixed size.
- Sorting version: (n − k + 1) · k log k time. Slower by the log factor.
She submitted the counting version: accepted, but slow. So she looks for a better pattern.
Part B · Sliding window with two maps
LeetCode 438 · from video 2
1The question again, with the new goal
Same question (all start indexes of anagrams of p in s), same example: s = "cbaebabacd", p = "abc" → [0, 6]. The goal now is one pass over s, O(n), with no inner loop.
2What the constraints tell us now
- n up to 3·10⁴ → we want O(n). O(n) is only 3·10⁴ steps.
- Lowercase letters only → each map holds at most 26 keys, so the maps are O(1) space in the strict sense (the teacher still calls this "two maps" vs "one map" when comparing space).
- If len(s) < len(p) → return [] straight away (her base case).
3Intuition: why sliding window, and the "count" idea
Why sliding window? The teacher gives two reasons:
- Strings have two main patterns. Two pointers is for comparing or computing with exactly two positions (a left character and a right character). Sliding window is for questions about a substring/subarray (everything between left and right). We want substrings → sliding window.
- The brute force repeats work: when we moved from "cba" to "bae" we re-counted "ba". Sliding window keeps the middle and only changes the ends, turning two loops into one.
The picture: keep a window of length k sliding over s. Keep two maps:
p_map: what we need, letter → count in p. Built once, never changes.s_map: what the window has, letter → count inside the window.
Comparing two maps every step would be slow-ish, so she adds one number: count = how many required characters the window is still missing. It starts at len(p) (we're missing all 3). Every time a useful character enters, count goes down by 1. When count == 0 and the window has length k, the window holds exactly the letters of p → anagram.
4Building the logic from examples
Example 1: "cba…" – expanding and reducing count
count = 3. right reads c: is c in p_map with a count > 0? Yes → it's needed. Put it in s_map (c:1) and count → 2. right reads b: needed → s_map b:1, count → 1. right reads a: needed → a:1, count → 0. Count 0 means every required character has been seen in the window, and the window length is 3 = k → valid. Store the start, which is left = 0 (not right!).
Note that every character right reads goes into s_map, needed or not. Only count is touched just for needed ones.
Example 2: "eabc" – why we sometimes have to shrink
Imagine s = "eabc", p = "abc". right reads e (not needed, count stays 3), then a (2), b (1), c (0). count is 0, but the window is "eabc", length 4. The answer is the start of "abc" (index 1), not the start of "eabc". So count 0 is not enough on its own; the length must also be exactly k. When the window is longer than k, we shrink from the left. In "cba" the length was already 3, so no shrinking was needed there.
Shrinking: left undoes what right did
Back to "cbaebabacd". After storing 0, right reads e → window "cbae", length 4 > 3 → shrink. The letter leaving is c. Before it leaves, the window had everything; after it leaves, "bae" is missing a c. So count must go back up to 1 ("I need one more character").
In code: if the left character is in p_map and it was a required copy (s_map[ch] ≤ p_map[ch]), then count += 1. In both cases, s_map[ch] −= 1 (undo right's +1), and left += 1.
The tricky part: duplicates. "Present in p" is not enough
Now window is "bae" (count 1, missing c). right reads b → s_map b becomes 2. If our rule were only "b is in p_map", we would lower count to 0 and wrongly call "aeb"(+b) valid. But p needs only one b, and the window already had one. The second b is extra.
So the rule when a character enters is: after adding it, if s_map[ch] ≤ p_map[ch], it was needed → count −= 1; if s_map[ch] > p_map[ch], it's extra → leave count alone. The teacher acts it out: s_map says "I've seen two b's", p_map says "I only need one. If you're equal to me or below, I'll take it. You're above me, so this b is not useful."
The same rule protects shrinking: if the leaving b has s_map[b] = 2 while p_map[b] = 1, there is still another b inside, so losing this one doesn't hurt → count unchanged. Only when s_map[ch] ≤ p_map[ch] (we're about to drop below what's needed) does count go up.
→ Before removing, s_map[ch] is the count including this copy. If it's ≤ p_map[ch], every copy in the window is needed, so this one is needed too: losing it makes us miss one → count += 1. If it's > p_map[ch], we have a spare. Then we subtract. Checking after the subtraction would need "<" instead. Same idea, different moment.
if, not while?→ The window only ever grows by one character per step. If it was length k, it becomes k+1, and one shrink brings it back to k. It can never be k+2. So one step is always enough. This is the fixed-size window from Part 0.
→ After the shrink, the length is at most k. count == 0 means k needed characters are inside, so the length is at least k. Together: exactly k. That's why her code checks only count. Early on (right < k−1) the window is short, but then count can't be 0 yet.
→ No. Her spoken walk-through around index 4–8 lets the window grow (b goes up to 3, a to 3) and then drops several letters on the left in a row. The code she writes shrinks once per step, so the window stays at length 3 and s_map[b] never goes above 2. Both reach the same answer (6). The dry run below follows the code. The duplicate rule (s_map ≤ p_map) is exactly the same in both.
5Approach steps
- If len(s) < len(p): return [].
- Build
p_mapfrom p.s_map = {},left = 0,count = len(p). - For right from 0 to n−1: ch = s[right]; s_map[ch] += 1.
- If ch is in p_map and s_map[ch] ≤ p_map[ch] → count −= 1 (a needed character came in).
- If the window length right−left+1 > k: let lc = s[left]. If lc is in p_map and s_map[lc] ≤ p_map[lc] → count += 1. Then s_map[lc] −= 1, left += 1.
- If count == 0 → append left.
- Return the result.
6Code (Python)
from collections import Counter
class Solution:
def findAnagrams(self, s, p):
result = []
if len(s) < len(p): # base case: no room for an anagram
return result
p_map = Counter(p) # what we NEED
s_map = {} # what the window HAS
left = 0
count = len(p) # needed characters still missing
k = len(p)
for right in range(len(s)):
ch = s[right]
s_map[ch] = s_map.get(ch, 0) + 1 # every char enters the map
if ch in p_map and s_map[ch] <= p_map[ch]:
count -= 1 # a needed copy arrived
if right - left + 1 > k: # window too long: shrink once
lc = s[left]
if lc in p_map and s_map[lc] <= p_map[lc]:
count += 1 # losing a needed copy
s_map[lc] -= 1 # undo what right did
left += 1
if count == 0: # length is k and nothing missing
result.append(left)
return result7Code line by line
| line | what it means |
|---|---|
| if len(s) < len(p): return result | Her base case. A shorter s can't contain a k-letter anagram. |
| p_map = Counter(p) | Letter → how many we need. For "abc": a:1, b:1, c:1. |
| s_map = {} | Letter → how many are inside the current window. |
| count = len(p) | At the start we're missing all k needed characters. |
| for right in range(len(s)): | right walks once from 0 to n−1 (expanding). |
| s_map[ch] = s_map.get(ch, 0) + 1 | The new letter is now in the window, whether it's useful or not. |
| if ch in p_map and s_map[ch] <= p_map[ch]: count -= 1 | Only a needed copy reduces count. A letter not in p, or an extra copy of a letter in p, leaves count alone. |
| if right - left + 1 > k: | The window is k+1 long, so we must drop the leftmost letter. |
| if lc in p_map and s_map[lc] <= p_map[lc]: count += 1 | Checked before the −1: if this copy was needed, after it leaves we'll be missing one. |
| s_map[lc] -= 1 left += 1 | Undo right's +1 and move the window's start forward. |
| if count == 0: result.append(left) | Window length is k and nothing is missing → anagram starting at left. |
8Dry run (hand table, s_map after every slide)
s = "cbaebabacd", p = "abc", k = 3, p_map = {a:1, b:1, c:1}. Start: s_map = {}, left = 0, count = 3. "needed" means s_map[ch] ≤ p_map[ch] after the +1.
| step | right (char added) | window [l..r] before shrink | s_map after adding | count after adding | shrink? (what leaves, why) | window after shrink | s_map after shrink | count | answer so far |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 0 (c) needed | [0..0] c | c:1 | 2 | no (len 1) | c | c:1 | 2 | [] |
| 2 | 1 (b) needed | [0..1] cb | c:1 b:1 | 1 | no (len 2) | cb | c:1 b:1 | 1 | [] |
| 3 | 2 (a) needed | [0..2] cba | c:1 b:1 a:1 | 0 | no (len 3) | [0..2] cba | c:1 b:1 a:1 | 0 | [0] |
| 4 | 3 (e) extra (not in p) | [0..3] cbae | c:1 b:1 a:1 e:1 | 0 | yes, len 4: c leaves; s_map[c]=1 ≤ 1 → needed → count+1 | [1..3] bae | c:0 b:1 a:1 e:1 | 1 | [0] |
| 5 | 4 (b) extra (b:2 > 1) | [1..4] baeb | c:0 b:2 a:1 e:1 | 1 | yes: b leaves; s_map[b]=2 > 1 → spare, count same | [2..4] aeb | c:0 b:1 a:1 e:1 | 1 | [0] |
| 6 | 5 (a) extra (a:2 > 1) | [2..5] aeba | c:0 b:1 a:2 e:1 | 1 | yes: a leaves; a=2 > 1 → spare | [3..5] eba | c:0 b:1 a:1 e:1 | 1 | [0] |
| 7 | 6 (b) extra (b:2) | [3..6] ebab | c:0 b:2 a:1 e:1 | 1 | yes: e leaves; not in p | [4..6] bab | c:0 b:2 a:1 e:0 | 1 | [0] |
| 8 | 7 (a) extra (a:2) | [4..7] baba | c:0 b:2 a:2 e:0 | 1 | yes: b leaves; b=2 > 1 → spare | [5..7] aba | c:0 b:1 a:2 e:0 | 1 | [0] |
| 9 | 8 (c) needed (c:1 ≤ 1) | [5..8] abac | c:1 b:1 a:2 e:0 | 0 | yes: a leaves; a=2 > 1 → spare, count stays 0 | [6..8] bac | c:1 b:1 a:1 e:0 | 0 | [0, 6] |
| 10 | 9 (d) extra | [6..9] bacd | c:1 b:1 a:1 e:0 d:1 | 0 | yes: b leaves; b=1 ≤ 1 → needed → count+1 | [7..9] acd | c:1 b:0 a:1 e:0 d:1 | 1 | [0, 6] |
right goes past the end → loop ends → return [0, 6] ✓.
The string at three key moments (yellow = window, grey = already left behind):
9Complexity & remember
- Time O(n): building p_map is O(k); right moves 0 → n−1 once, left moves forward at most n times; neither pointer ever goes back. About 2n steps, so linear. (In the video she says "O of one" once by slip; she means one pass, O(n).) Her Java submission was fast.
- Space: two maps. With only 26 letters each, that's O(1) in big-O terms, but it's twice the memory of Part C.
Part C · Sliding window with one map
LeetCode 438 · from video 2 · same window, same count, half the maps
1What stays the same
Same question, same constraints, same fixed-size window, same count of missing characters, same "shrink once with if", same "store left when count == 0". Time is the same O(n). Only the bookkeeping changes: we drop s_map and let one map, need, mean "how many more of this letter the window still needs". The teacher's view: one-map is not harder, it just makes more sense after you've seen two maps. Interviewers usually prefer this one because it uses less space.
2Intuition: one map that counts down
- Start with
need= p's counts: a:1, b:1, c:1. - When right brings in a letter, decrease need[letter]. Going down towards 0 means "one fewer still needed". (Increasing would make no sense: we'd never be able to say "all found".)
- A letter that isn't in p at all (like e) gets added to the map starting at 0 and then decreased to −1.
- Negative = "the window has more of this letter than p wants". It's extra, or as she puts it, it was added to the substring for nothing.
- When left lets a letter go, increase need[letter] (left undoes right).
3Building the conditions
When right adds a letter: did it reduce count?
It's needed if need[ch] was positive before we decrease it. Two equal ways to write it:
- Check first, then decrease:
if need[ch] > 0: count −= 1, thenneed[ch] −= 1. - Decrease first, then check:
need[ch] −= 1, thenif need[ch] >= 0: count −= 1. After decreasing, a needed letter lands on 0 or more; an extra one lands below 0.
→ The teacher stresses this. It's the same test at two different moments. Before the −1, "needed" means at least 1 left (> 0). After the −1, that same letter shows ≥ 0. Mix them up and you get wrong counts. Pick one order and match the sign to it.
→ So that a letter p doesn't have looks exactly like an extra copy: negative. Then one rule covers both. When e later leaves, it goes back up to 0, still not ≥ 1, and count isn't touched. In Python,
need.get(ch, 0) gives that starting 0.When left removes a letter: did it increase count?
Look at need[lc] before increasing it. If it's ≥ 0, this copy was a needed one: when right brought it in, need was positive and went down to this value. Losing it means we're missing one → count += 1. If it's negative, the window has a spare copy, so count stays. Either way, need[lc] += 1, then left += 1.
Her example: window "cbae" with need = {a:0, b:0, c:0, e:−1}. c leaves: need[c] = 0, which is ≥ 0 → it was needed → count 0 → 1, and need[c] becomes 1 ("we need one c again").
→ 0 means the window has exactly as many of this letter as p wants, no spare. Remove one and we'd be short. Only a negative value says "there's a spare, you can lose one".
4Approach steps
- If len(s) < len(p): return [].
need= counts of p;left = 0;count = len(p).- For each right: ch = s[right]. If need.get(ch, 0) > 0 → count −= 1. Then need[ch] = need.get(ch, 0) − 1.
- If the window is longer than k: lc = s[left]; if need[lc] ≥ 0 → count += 1; need[lc] += 1; left += 1.
- If count == 0 → append left.
- Return result.
5Code (Python)
class Solution:
def findAnagrams(self, s, p):
result = []
if len(s) < len(p):
return result
need = {} # letter -> how many still needed
for ch in p:
need[ch] = need.get(ch, 0) + 1
left = 0
count = len(p)
k = len(p)
for right in range(len(s)):
ch = s[right]
if need.get(ch, 0) > 0: # check first ...
count -= 1 # ... it was needed
need[ch] = need.get(ch, 0) - 1 # then decrease (may go negative)
if right - left + 1 > k:
lc = s[left]
if need[lc] >= 0: # it was a needed copy
count += 1
need[lc] += 1 # undo what right did
left += 1
if count == 0:
result.append(left)
return resultclass Solution:
def findAnagrams(self, s, p):
result = []
if len(s) < len(p):
return result
need = {}
for ch in p:
need[ch] = need.get(ch, 0) + 1
left, count, k = 0, len(p), len(p)
for right in range(len(s)):
ch = s[right]
need[ch] = need.get(ch, 0) - 1 # decrease first ...
if need[ch] >= 0: # ... so the test is >= 0
count -= 1
if right - left + 1 > k:
lc = s[left]
if need[lc] >= 0:
count += 1
need[lc] += 1
left += 1
if count == 0:
result.append(left)
return result6Code line by line (only the lines that differ from Part B)
| line | what it means |
|---|---|
| need = {} ... need[ch] + 1 | Only one map. At the start it equals p's counts. |
| if need.get(ch, 0) > 0: count -= 1 | Still needed some of this letter? Then this copy is useful. A letter not in p reads as 0 → not useful. |
| need[ch] = need.get(ch, 0) - 1 | Every letter that enters lowers its need, even if it goes negative (= extra). |
| if need[lc] >= 0: count += 1 | The leaving copy was not a spare, so after it leaves we're missing one. |
| need[lc] += 1 | Left undoes right's −1. |
7Dry run (hand table, the one map after every slide)
s = "cbaebabacd", p = "abc". Start: need = {a:1, b:1, c:1}, left = 0, count = 3. "need before" is the value right sees before it decreases.
| step | right (char, need before) | window before shrink | need after adding | count after adding | shrink? (what leaves, why) | window after shrink | need after shrink | count | answer so far |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 0 c (1 > 0 → needed) | c | a:1 b:1 c:0 | 2 | no | c | a:1 b:1 c:0 | 2 | [] |
| 2 | 1 b (1 → needed) | cb | a:1 b:0 c:0 | 1 | no | cb | a:1 b:0 c:0 | 1 | [] |
| 3 | 2 a (1 → needed) | cba | a:0 b:0 c:0 | 0 | no (len 3) | [0..2] cba | a:0 b:0 c:0 | 0 | [0] |
| 4 | 3 e (0 → extra) | cbae | a:0 b:0 c:0 e:−1 | 0 | yes: c leaves, need[c]=0 ≥ 0 → count+1 | [1..3] bae | a:0 b:0 c:1 e:−1 | 1 | [0] |
| 5 | 4 b (0 → extra) | baeb | a:0 b:−1 c:1 e:−1 | 1 | yes: b leaves, need[b]=−1 → spare | [2..4] aeb | a:0 b:0 c:1 e:−1 | 1 | [0] |
| 6 | 5 a (0 → extra) | aeba | a:−1 b:0 c:1 e:−1 | 1 | yes: a leaves, need[a]=−1 → spare | [3..5] eba | a:0 b:0 c:1 e:−1 | 1 | [0] |
| 7 | 6 b (0 → extra) | ebab | a:0 b:−1 c:1 e:−1 | 1 | yes: e leaves, need[e]=−1 → spare | [4..6] bab | a:0 b:−1 c:1 e:0 | 1 | [0] |
| 8 | 7 a (0 → extra) | baba | a:−1 b:−1 c:1 e:0 | 1 | yes: b leaves, need[b]=−1 → spare | [5..7] aba | a:−1 b:0 c:1 e:0 | 1 | [0] |
| 9 | 8 c (1 → needed) | abac | a:−1 b:0 c:0 e:0 | 0 | yes: a leaves, need[a]=−1 → spare | [6..8] bac | a:0 b:0 c:0 e:0 | 0 | [0, 6] |
| 10 | 9 d (0 → extra) | bacd | a:0 b:0 c:0 e:0 d:−1 | 0 | yes: b leaves, need[b]=0 ≥ 0 → count+1 | [7..9] acd | a:0 b:1 c:0 e:0 d:−1 | 1 | [0, 6] |
return [0, 6] ✓. Notice: at every valid window (steps 3 and 9) all of p's letters show exactly 0, and nothing is negative.
8The "eabc" example with one map
s = "eabc", p = "abc": e → need e:−1 (count 3); a → a:0 (2); b → b:0 (1); c → c:0 (0), but length 4 → shrink: e leaves with need −1 (spare) → count stays 0, e back to 0, left = 1 → count 0 → store 1. Answer [1] ✓, the start of "abc", not of "eabc".
9Complexity & remember
- Time O(n), same as two maps: each pointer moves forward at most n times.
- Space: one map instead of two (at most 26 + a few keys). Less space doesn't change the speed, but it's the version interviewers usually expect.
Part D · Revision page
| Brute force | Two maps | One map | |
|---|---|---|---|
| idea | count every k-piece from scratch | window keeps s_map; compare with p_map through count | one map "need" counting down; negative = extra |
| needed on enter | — | s_map[ch] ≤ p_map[ch] (after +1) | need[ch] > 0 (before −1) or ≥ 0 (after −1) |
| needed on leave | — | s_map[lc] ≤ p_map[lc] (before −1) | need[lc] ≥ 0 (before +1) |
| valid when | s_count == p_count | count == 0 | count == 0 |
| store | i | left | left |
| time | O((n−k+1)·k), O(n²) worst | O(n) | O(n) |
| space | O(1) (26 slots) | two maps (≤ 26 keys each) | one map |
2. Fixed window: expand right by 1; if length > k, shrink left by 1 (if, not while).
3.
count = needed letters still missing; starts at k; anagram when it hits 0.4. Only a needed copy changes count. Extra copies (or letters not in p) never do.
5. Left undoes right. Store left, never right.
✗ mixing "> 0" with "decrease first" (must be "≥ 0" then)
✗ checking the leaving letter after changing its count
✗ storing right (or right − k) instead of left
✗ outer brute-force loop that stops at n − k − 1 (misses the last window)
✗ reusing the brute-force count array between pieces
✗ forgetting
len(s) < len(p) → []sol = Solution()
print(sol.findAnagrams("cbaebabacd", "abc")) # [0, 6]
print(sol.findAnagrams("abab", "ab")) # [0, 1, 2]
print(sol.findAnagrams("eabc", "abc")) # [1]
print(sol.findAnagrams("xab", "ab")) # [1]
print(sol.findAnagrams("a", "ab")) # []
print(sol.findAnagrams("aaaa", "aa")) # [0, 1, 2]Based on these videos: Find All Anagrams in a String · Part 1 (brute force) · Find All Anagrams in a String · Part 2 (sliding window: two maps & one map)