Find All Anagrams in a String (LeetCode 438)
You have two strings, s and p. Find every starting index in s where the substring of length len(p) beginning there is an anagram of p — i.e. uses exactly the same letters, rearranged. Return those indices, in any order.
Worked Examples
s = "cbaebabacd", p = "abc"[0, 6]s = "abab", p = "ab"[0, 1, 2]⚖️Formal Constraints & Bounds
1 <= s.length, p.length <= 3 * 104sandpconsist of lowercase English letters.
Core Insight
Fixed-size windows transform quadratic re-evaluations into constant-time delta shifts.
Real-World Scenario & Production Applications
DNA motif discovery, substring frequency matching, and token sliding window verifications.
Subproblems & Recurrence Decomposition3 Phases
Count character frequencies for pattern string p. Seed the rolling window with the first len(p) characters of s.
p_count = Counter(p)
s_count = Counter(s[:len(p)])
res = [0] if s_count == p_count else []Step-by-Step Execution Trace Table
| Step | Index i | Incoming s[i] | Outgoing s[i-len(p)] | Active Window | s_count == p_count? | Result Indices |
|---|---|---|---|---|---|---|
| Init | 0..2 | "cba" (seed) | — | s[0..2] = "cba" | True (Counts match) | [0] |
| 1 | 3 | 'e' | 'c' | s[1..3] = "bae" | False ('e' not in p) | [0] |
| 2 | 4 | 'b' | 'b' | s[2..4] = "aeb" | False ('e' not in p) | [0] |
| 3 | 5 | 'a' | 'a' | s[3..5] = "eba" | False ('e' not in p) | [0] |
| 4 | 6 | 'b' | 'e' | s[4..6] = "bab" | False ('b':2, 'a':1) | [0] |
| 5 | 7 | 'a' | 'b' | s[5..7] = "aba" | False ('a':2, 'b':1) | [0] |
| 6 | 8 | 'c' | 'a' | s[6..8] = "bac" | True (Counts match) | [0, 6] |
| 7 | 9 | 'd' | 'b' | s[7..9] = "acd" | False ('d' not in p) | [0, 6] |
| 1 | k = len(p) |
| 2 | if len(s) < k: |
| 3 | return [] |
| 4 | p_count, window_count = [0] * 26, [0] * 26 |
| 5 | for ch in p: |
| 6 | p_count[ord(ch) - ord('a')] += 1 |
| 7 | for ch in s[:k]: |
| 8 | window_count[ord(ch) - ord('a')] += 1 |
| 9 | result = [0] if window_count == p_count else [] |
| 10 | for i in range(k, len(s)): |
| 11 | window_count[ord(s[i]) - ord('a')] += 1 |
| 12 | window_count[ord(s[i - k]) - ord('a')] -= 1 |
| 13 | if window_count == p_count: |
| 14 | result.append(i - k + 1) |
| 15 | return result |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Fixed-size sliding windows are the rigid-boundary counterpart to dynamic windows. While dynamic windows (LC 209, LC 3, LC 76) grow and shrink adaptively based on constraint satisfaction, fixed-size windows enforce an immutable aperture width across the entire sequence.
🧭 Conceptual Foundation & Pattern Intuition
An anagram is any permutation of characters. The foundational insight is that two strings are anagrams if and only if their character frequency distributions are identical:
Because character arrangement inside the window is completely irrelevant, we never need to sort substrings or generate permutations. The algorithm only needs to verify whether the current -length window matches 's character histogram.
A naive approach tests every candidate starting index , taking a slice and counting or sorting its letters. Recomputing frequencies for characters takes work per position. Across all windows, the total runtime degrades quadratically to:
When and , this requires up to operations—triggering a guaranteed Time Limit Exceeded (TLE) failure in technical interviews.
📐 Fixed-Width Viewport Mental Model
A fixed window of length maintains the strict invariant . As the window shifts right by 1, only two characters change state: the exiting character on the left boundary is decremented, and the entering character on the right boundary is incremented.
String s: | c | b | a | e | b | a | b | a | c | d |Indices: 0 1 2 3 4 5 6 7 8 9Target p: "abc" (len = 3) -> requires {a: 1, b: 1, c: 1} Window 0: [ c b a ] -> freq: {a:1, b:1, c:1} == p -> MATCH! (Index 0) slide +1: drop s[0]='c', add s[3]='e'Window 1: [ b a e ] -> freq: {a:1, b:1, e:1} != p -> Miss slide +1: drop s[1]='b', add s[4]='b'Window 2: [ a e b ] -> freq: {a:1, b:1, e:1} != p -> Miss...Window 6: [ b a c ] -> freq: {a:1, b:1, c:1} == p -> MATCH! (Index 6) 🎞️ The Analogy: The Rigid Cardboard Stencil on a Conveyor Belt
Imagine an industrial conveyor belt carrying a continuous stream of letter tiles. Mounted directly above the belt is a stationary cardboard stencil with an open aperture window exactly tiles wide.
When the conveyor belt rolls forward by one position:
- The center tiles stay completely visible inside the aperture. Their counts do not change at all.
- Exactly one tile passes out of view on the left edge (
outgoing = s[i - k]). We subtract it from our tally:window_count[outgoing] -= 1. - Exactly one new tile rolls into view on the right edge (
incoming = s[i]). We add it to our tally:window_count[incoming] += 1.
Re-counting all tiles under the stencil on every step is redundant recomputation. By updating only the boundary delta (+1 incoming, -1 outgoing), state maintenance requires strictly constant operations per slide!
🔑 The Key Idea in the Code
There are two primary paradigms to manage and verify frequency equality:
Paradigm A: The Fixed 26-Element Array ( Comparison)
Because strings consist of lowercase English letters, we allocate two 26-element arrays ([0] * 26):
p_count, window_count = [0] * 26, [0] * 26for ch in p: p_count[ord(ch) - ord('a')] += 1for ch in s[:k]: window_count[ord(ch) - ord('a')] += 1 # First window: s[0..k-1] result = [0] if window_count == p_count else []for i in range(k, len(s)): window_count[ord(s[i]) - ord('a')] += 1 # Add incoming s[i] window_count[ord(s[i - k]) - ord('a')] -= 1 # Remove outgoing s[i - k] if window_count == p_count: # O(26) = O(1) array equality check result.append(i - k + 1) # Window s[i - k + 1..i] Because the alphabet size is constant, comparing the two 26-element arrays takes exactly 26 machine word comparisons—which is mathematically . Total time is strictly .
Paradigm B: The FAANG Scalar matches Counter (Zero-Scan Optimization)
Can we evaluate whether the window is valid without even doing 26 comparisons per step? Yes.
We maintain an integer register matches (from to ), indicating how many of the 26 characters currently have matching counts in both and .
- On each slide, only two characters alter their frequencies: the incoming character and the outgoing character.
- Before modifying a character's count, we check if it currently matches
p_count. If modifying it breaks a match, we decrementmatches -= 1. - After modifying its count, if it now equals
p_count, we incrementmatches += 1. - The entire window is an anagram if and only if
matches == 26! This evaluates window validity in strictly operations with zero array scanning, making it optimal even for large Unicode or ASCII alphabets.
⚖️ Brute Force vs. Fixed Sliding Window
| Dimension | Brute Force (Recompute Each Window) | Fixed Sliding Window (Rolling Delta) |
|---|---|---|
| Time Complexity | $O( | s |
| Work per Step | Re-scans entire -character window ( ops) | Ingests 1 char, evicts 1 char ( ops) |
| Auxiliary Space | substring copies | fixed 26-element counters |
| Boundary Invariant | Unmanaged | Rigid Aperture: |
💡 The 4-Step Fixed-Window Recipe for Interviews
Whenever an interview problem asks about a contiguous substring/subarray of fixed length :
- Guard Condition: Immediately return empty/default if .
- Seed State: Process the first elements into the window state accumulator (
s[0..k-1]). - Roll the Aperture: Iterate from to . At each step:
- Ingest
s[i]into state. - Evict
s[i - k]from state.
- Ingest
- Evaluate & Collect: Check state condition in and record the 0-indexed start position
i - k + 1.
Recomputing frequency counts from scratch: Re-hashing the entire window in
O(k)time degrades overall complexity to O(N * k). Maintain running counts withO(1)add and remove updates.String length smaller than target: Failing to guard
if len(s) < len(p): return []causes out-of-bounds slice errors on initialization.Comparing dictionary equality directly: In Python, comparing
dict1 == dict2takes O(26) =O(1)time, but in other languages maintain a scalarmatchescounter to avoid 26-key comparisons per step.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
Contiguous subarray/substring of fixed length K property verification.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: window contains exactly K elements at all times after the initial seed.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
When
s[i]enters,s[i - k]leaves: removings[i - k + 1]instead shifts every window, so the windows compared are the wrong ones.Compare
window_count == p_countonly after both the add and the remove: in between, the window holdsk + 1letters and never matches.if len(s) < k: return []before building the first window: no window fits.
The 60-Second Interview Pitch
Say this out loud before you type a single line
"I'd use a Fixed Window of size k, the length of p. I count p's letters into p_count and the first k letters of s into window_count. Then I slide one step at a time: s[i] enters and s[i - k] leaves, so I add one count and subtract one, and after both updates I compare window_count with p_count; if they're equal, the window starting at i - k + 1 is an anagram. The trap is the leaving index: it's s[i - k], and an off-by-one there shifts every window. Comparing two 26-entry lists is constant work, so it's
O(N)time andO(1)extra space."
Complexity & Mathematical Proof
O(N)
O(|s|): the slide loop runs once per letter of s after the first window, and each step does one add, one remove and one 26-entry comparison.
O(1) Auxiliary
Look at memory allocations: The algorithm executes iteratively with O(1) stack frames. Auxiliary heap memory is strictly bounded by two fixed 26-element integer frequency arrays (or hash maps bounded by alphabet size Σ = 26). Since the alphabet size is constant regardless of input length, auxiliary space is strictly O(1) Auxiliary.
Derivation Progression
O(|p|)
Constructs frequency counts for pattern p and the first |p| characters of s in O(|p|) time.
window_count[in] += 1; window_count[out] -= 1
Each slide adds the new incoming right character and subtracts the old left character in O(1) scalar operations.
Checks whether window character counts match pattern counts in constant time.
Overall execution time is strictly linear in string length: O(|s|).
Variable Definitions
Length of the target string s (up to 30,000)
Length of the pattern/anagram string p
Alphabet cardinality (26 lowercase English letters)
Memory Architecture & Bounds
O(1) Iterative
O(1) Two 26-element frequency arrays
O(|s|) Result start indices array
Boundary Best / Worst Cases
Pointer Invariant Transition Progression
Senior SWE Deconstruction & Hardware Caveats
Triggers: "find all start indices of p's anagrams in s", "both strings consist of lowercase English letters". Fixed-size sliding window () with rolling frequency state.
, lowercase English letters only. Recounting every window is , up to about steps (when p is half as long as s); sliding the counts costs one add, one remove and one 26-entry comparison per step, about .
At scale: with a large alphabet (Unicode text instead of 26 letters) the full comparison costs per step. There, keep a matches counter of letters whose counts agree, change it only when the entering or leaving letter's count reaches or leaves its target, and test matches == len(p_count). On a stream, the window must also keep its last k characters (a ring buffer), or it cannot know which one leaves.
Core Algorithmic State Invariants
- Counts Are All an Anagram Needs
An anagram is the same letters in any order, so the order inside the window never matters: two strings are anagrams exactly when their 26 letter counts are equal. That is why window_count == p_count is a complete test, with no sorting and no pairing of letters.
Find All Anagrams in a String (LeetCode 438)
You have two strings, s and p. Find every starting index in s where the substring of length len(p) beginning there is an anagram of p — i.e. uses exactly the same letters, rearranged. Return those indices, in any order.
Worked Examples
s = "cbaebabacd", p = "abc"[0, 6]s = "abab", p = "ab"[0, 1, 2]⚖️Formal Constraints & Bounds
1 <= s.length, p.length <= 3 * 104sandpconsist of lowercase English letters.
Core Insight
Fixed-size windows transform quadratic re-evaluations into constant-time delta shifts.
Real-World Scenario & Production Applications
DNA motif discovery, substring frequency matching, and token sliding window verifications.
Subproblems & Recurrence Decomposition3 Phases
Count character frequencies for pattern string p. Seed the rolling window with the first len(p) characters of s.
p_count = Counter(p)
s_count = Counter(s[:len(p)])
res = [0] if s_count == p_count else []Step-by-Step Execution Trace Table
| Step | Index i | Incoming s[i] | Outgoing s[i-len(p)] | Active Window | s_count == p_count? | Result Indices |
|---|---|---|---|---|---|---|
| Init | 0..2 | "cba" (seed) | — | s[0..2] = "cba" | True (Counts match) | [0] |
| 1 | 3 | 'e' | 'c' | s[1..3] = "bae" | False ('e' not in p) | [0] |
| 2 | 4 | 'b' | 'b' | s[2..4] = "aeb" | False ('e' not in p) | [0] |
| 3 | 5 | 'a' | 'a' | s[3..5] = "eba" | False ('e' not in p) | [0] |
| 4 | 6 | 'b' | 'e' | s[4..6] = "bab" | False ('b':2, 'a':1) | [0] |
| 5 | 7 | 'a' | 'b' | s[5..7] = "aba" | False ('a':2, 'b':1) | [0] |
| 6 | 8 | 'c' | 'a' | s[6..8] = "bac" | True (Counts match) | [0, 6] |
| 7 | 9 | 'd' | 'b' | s[7..9] = "acd" | False ('d' not in p) | [0, 6] |
| 1 | k = len(p) |
| 2 | if len(s) < k: |
| 3 | return [] |
| 4 | p_count, window_count = [0] * 26, [0] * 26 |
| 5 | for ch in p: |
| 6 | p_count[ord(ch) - ord('a')] += 1 |
| 7 | for ch in s[:k]: |
| 8 | window_count[ord(ch) - ord('a')] += 1 |
| 9 | result = [0] if window_count == p_count else [] |
| 10 | for i in range(k, len(s)): |
| 11 | window_count[ord(s[i]) - ord('a')] += 1 |
| 12 | window_count[ord(s[i - k]) - ord('a')] -= 1 |
| 13 | if window_count == p_count: |
| 14 | result.append(i - k + 1) |
| 15 | return result |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Fixed-size sliding windows are the rigid-boundary counterpart to dynamic windows. While dynamic windows (LC 209, LC 3, LC 76) grow and shrink adaptively based on constraint satisfaction, fixed-size windows enforce an immutable aperture width across the entire sequence.
🧭 Conceptual Foundation & Pattern Intuition
An anagram is any permutation of characters. The foundational insight is that two strings are anagrams if and only if their character frequency distributions are identical:
Because character arrangement inside the window is completely irrelevant, we never need to sort substrings or generate permutations. The algorithm only needs to verify whether the current -length window matches 's character histogram.
A naive approach tests every candidate starting index , taking a slice and counting or sorting its letters. Recomputing frequencies for characters takes work per position. Across all windows, the total runtime degrades quadratically to:
When and , this requires up to operations—triggering a guaranteed Time Limit Exceeded (TLE) failure in technical interviews.
📐 Fixed-Width Viewport Mental Model
A fixed window of length maintains the strict invariant . As the window shifts right by 1, only two characters change state: the exiting character on the left boundary is decremented, and the entering character on the right boundary is incremented.
String s: | c | b | a | e | b | a | b | a | c | d |Indices: 0 1 2 3 4 5 6 7 8 9Target p: "abc" (len = 3) -> requires {a: 1, b: 1, c: 1} Window 0: [ c b a ] -> freq: {a:1, b:1, c:1} == p -> MATCH! (Index 0) slide +1: drop s[0]='c', add s[3]='e'Window 1: [ b a e ] -> freq: {a:1, b:1, e:1} != p -> Miss slide +1: drop s[1]='b', add s[4]='b'Window 2: [ a e b ] -> freq: {a:1, b:1, e:1} != p -> Miss...Window 6: [ b a c ] -> freq: {a:1, b:1, c:1} == p -> MATCH! (Index 6) 🎞️ The Analogy: The Rigid Cardboard Stencil on a Conveyor Belt
Imagine an industrial conveyor belt carrying a continuous stream of letter tiles. Mounted directly above the belt is a stationary cardboard stencil with an open aperture window exactly tiles wide.
When the conveyor belt rolls forward by one position:
- The center tiles stay completely visible inside the aperture. Their counts do not change at all.
- Exactly one tile passes out of view on the left edge (
outgoing = s[i - k]). We subtract it from our tally:window_count[outgoing] -= 1. - Exactly one new tile rolls into view on the right edge (
incoming = s[i]). We add it to our tally:window_count[incoming] += 1.
Re-counting all tiles under the stencil on every step is redundant recomputation. By updating only the boundary delta (+1 incoming, -1 outgoing), state maintenance requires strictly constant operations per slide!
🔑 The Key Idea in the Code
There are two primary paradigms to manage and verify frequency equality:
Paradigm A: The Fixed 26-Element Array ( Comparison)
Because strings consist of lowercase English letters, we allocate two 26-element arrays ([0] * 26):
p_count, window_count = [0] * 26, [0] * 26for ch in p: p_count[ord(ch) - ord('a')] += 1for ch in s[:k]: window_count[ord(ch) - ord('a')] += 1 # First window: s[0..k-1] result = [0] if window_count == p_count else []for i in range(k, len(s)): window_count[ord(s[i]) - ord('a')] += 1 # Add incoming s[i] window_count[ord(s[i - k]) - ord('a')] -= 1 # Remove outgoing s[i - k] if window_count == p_count: # O(26) = O(1) array equality check result.append(i - k + 1) # Window s[i - k + 1..i] Because the alphabet size is constant, comparing the two 26-element arrays takes exactly 26 machine word comparisons—which is mathematically . Total time is strictly .
Paradigm B: The FAANG Scalar matches Counter (Zero-Scan Optimization)
Can we evaluate whether the window is valid without even doing 26 comparisons per step? Yes.
We maintain an integer register matches (from to ), indicating how many of the 26 characters currently have matching counts in both and .
- On each slide, only two characters alter their frequencies: the incoming character and the outgoing character.
- Before modifying a character's count, we check if it currently matches
p_count. If modifying it breaks a match, we decrementmatches -= 1. - After modifying its count, if it now equals
p_count, we incrementmatches += 1. - The entire window is an anagram if and only if
matches == 26! This evaluates window validity in strictly operations with zero array scanning, making it optimal even for large Unicode or ASCII alphabets.
⚖️ Brute Force vs. Fixed Sliding Window
| Dimension | Brute Force (Recompute Each Window) | Fixed Sliding Window (Rolling Delta) |
|---|---|---|
| Time Complexity | $O( | s |
| Work per Step | Re-scans entire -character window ( ops) | Ingests 1 char, evicts 1 char ( ops) |
| Auxiliary Space | substring copies | fixed 26-element counters |
| Boundary Invariant | Unmanaged | Rigid Aperture: |
💡 The 4-Step Fixed-Window Recipe for Interviews
Whenever an interview problem asks about a contiguous substring/subarray of fixed length :
- Guard Condition: Immediately return empty/default if .
- Seed State: Process the first elements into the window state accumulator (
s[0..k-1]). - Roll the Aperture: Iterate from to . At each step:
- Ingest
s[i]into state. - Evict
s[i - k]from state.
- Ingest
- Evaluate & Collect: Check state condition in and record the 0-indexed start position
i - k + 1.
Recomputing frequency counts from scratch: Re-hashing the entire window in
O(k)time degrades overall complexity to O(N * k). Maintain running counts withO(1)add and remove updates.String length smaller than target: Failing to guard
if len(s) < len(p): return []causes out-of-bounds slice errors on initialization.Comparing dictionary equality directly: In Python, comparing
dict1 == dict2takes O(26) =O(1)time, but in other languages maintain a scalarmatchescounter to avoid 26-key comparisons per step.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
Contiguous subarray/substring of fixed length K property verification.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: window contains exactly K elements at all times after the initial seed.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
When
s[i]enters,s[i - k]leaves: removings[i - k + 1]instead shifts every window, so the windows compared are the wrong ones.Compare
window_count == p_countonly after both the add and the remove: in between, the window holdsk + 1letters and never matches.if len(s) < k: return []before building the first window: no window fits.
The 60-Second Interview Pitch
Say this out loud before you type a single line
"I'd use a Fixed Window of size k, the length of p. I count p's letters into p_count and the first k letters of s into window_count. Then I slide one step at a time: s[i] enters and s[i - k] leaves, so I add one count and subtract one, and after both updates I compare window_count with p_count; if they're equal, the window starting at i - k + 1 is an anagram. The trap is the leaving index: it's s[i - k], and an off-by-one there shifts every window. Comparing two 26-entry lists is constant work, so it's
O(N)time andO(1)extra space."
Complexity & Mathematical Proof
O(N)
O(|s|): the slide loop runs once per letter of s after the first window, and each step does one add, one remove and one 26-entry comparison.
O(1) Auxiliary
Look at memory allocations: The algorithm executes iteratively with O(1) stack frames. Auxiliary heap memory is strictly bounded by two fixed 26-element integer frequency arrays (or hash maps bounded by alphabet size Σ = 26). Since the alphabet size is constant regardless of input length, auxiliary space is strictly O(1) Auxiliary.
Derivation Progression
O(|p|)
Constructs frequency counts for pattern p and the first |p| characters of s in O(|p|) time.
window_count[in] += 1; window_count[out] -= 1
Each slide adds the new incoming right character and subtracts the old left character in O(1) scalar operations.
Checks whether window character counts match pattern counts in constant time.
Overall execution time is strictly linear in string length: O(|s|).
Variable Definitions
Length of the target string s (up to 30,000)
Length of the pattern/anagram string p
Alphabet cardinality (26 lowercase English letters)
Memory Architecture & Bounds
O(1) Iterative
O(1) Two 26-element frequency arrays
O(|s|) Result start indices array
Boundary Best / Worst Cases
Pointer Invariant Transition Progression
Senior SWE Deconstruction & Hardware Caveats
Triggers: "find all start indices of p's anagrams in s", "both strings consist of lowercase English letters". Fixed-size sliding window () with rolling frequency state.
, lowercase English letters only. Recounting every window is , up to about steps (when p is half as long as s); sliding the counts costs one add, one remove and one 26-entry comparison per step, about .
At scale: with a large alphabet (Unicode text instead of 26 letters) the full comparison costs per step. There, keep a matches counter of letters whose counts agree, change it only when the entering or leaving letter's count reaches or leaves its target, and test matches == len(p_count). On a stream, the window must also keep its last k characters (a ring buffer), or it cannot know which one leaves.
Core Algorithmic State Invariants
- Counts Are All an Anagram Needs
An anagram is the same letters in any order, so the order inside the window never matters: two strings are anagrams exactly when their 26 letter counts are equal. That is why window_count == p_count is a complete test, with no sorting and no pairing of letters.