Sliding Window: Pattern Intuition Guide¶
"The window is a moving lens of attention β it forgets the past to focus on what matters now."
The Situation That Calls for a Window¶
Imagine you're walking through a long corridor, and you can only see through a rectangular frame you carry with you. As you move forward, new things enter your view on the right, and old things disappear on the left.
This is the essence of Sliding Window.
You encounter this pattern whenever: - You're scanning through a sequence (string, array, stream) - You care about a contiguous portion of that sequence - The answer depends on properties of that portion - Those properties can be updated incrementally as the portion shifts
The key insight: You don't need to remember everything β only what's currently in view.
The Two Forces at Play¶
Every sliding window algorithm is a dance between two opposing forces:
The Explorer (Right Boundary) \(R\)¶
- Always moves forward, never backward
- Discovers new territory
- Adds new elements to consideration
- Asks: "What happens if I include this?"
The Gatekeeper (Left Boundary) \(L\)¶
- Follows behind, cleaning up
- Removes elements that no longer serve the goal
- Enforces the rules of what's allowed
- Asks: "Must I let go of something to stay valid?"
The Explorer is eager and expansive. The Gatekeeper is disciplined and selective. Together, they maintain a window of validity that slides through the sequence.
The Invariant: The Window's Promise¶
At every moment, the window makes a promise β an invariant that must always be true:
| Problem Type | The Promise |
|---|---|
| Longest unique substring | "Every character in my view appears exactly once" |
| At most K distinct | "I contain no more than K different characters" |
| Minimum covering substring | "I contain everything required" |
| Sum at least target | "My total meets or exceeds the goal" |
This promise is sacred. The moment it's broken, the Gatekeeper must act β shrinking the window until the promise is restored.
The Irreversible Truth¶
Here's what makes sliding window work: the Explorer never retreats.
Once the right boundary passes an element, that element has been "seen." We may include it or exclude it from our current window, but we never go back to re-examine it as a potential starting point... unless the Gatekeeper releases it.
This one-directional march is what gives us O(n) time complexity. Each element enters the window at most once and exits at most once. No element is visited more than twice across the entire algorithm.
The irreversibility creates efficiency: past decisions don't haunt us.
The Two Modes of Seeking¶
Depending on what you're optimizing, the dance changes:
Mode 1: Maximize the Window¶
"How large can my view become while staying valid?"
Process:
1. Explorer advances, adding new element
2. If promise breaks β Gatekeeper advances until promise restored
3. Record the current window size (this is a candidate answer)
4. Repeat
The window EXPANDS freely, CONTRACTS only when forced.
Mental image: Stretching a rubber band until it's about to snap, then easing off just enough.
Flowchart: Maximize Window¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β Example: Longest Substring Without Repeating Characters β β Sequence: [ a b c a b ] Promise: "All chars unique" β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β β β βββββββ β β βSTARTβ β β ββββ¬βββ β β βΌ β β ββββββββββββββββββββββββ β β β R < length? ββββNoββββΊ βββββββββββ β β ββββββββ¬ββββββββββββββββ β DONE β β β Yes β βββββββββββ β β βΌ β β ββββββββββββββββββββββββββββββββ β β β π’ R advances (Explorer) ββββββββββββββββββββββββββββββββββ β β β Add element to state β β β β βββββββββββββββββ€βββββββββββββββ β β β βΌ β β β βββββββββββββββββββββββ β β β β Promise broken? β β β β β (duplicate found?) β β β β ββββββββββ¬βββββββββββββ β β β Yes β No β β β ββββββββββββ΄βββββββββββ β β β βΌ βΌ β β β βββββββββββββββββββ ββββββββββββββββββββββββββββββββββ β β β β While promise β β β Update answer: β β β β β is broken: β β ans = max(ans, R-L+1) β β β β β β ββββββββββββββββ¬ββββββββββββββββββ β β β β βββββββββββββββ β β β β β β β π΄ L++ β β β β β β β β Remove L-1 β β β β β β β βββββββββββββββ β β β β β ββββββββββ¬βββββββββ β β β β β β β β β ββββββββββββββββββββββββββββ β β β β β β β β β β β β β β β ββββββββββββββββββββββββββββββββββββββββββββββ β β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Visual Trace: βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Sequence: a b c a b [0] [1] [2] [3] [4]
Step 1: π’Rβ [ a ] max = 1 L,R
Step 2: π’Rβ [ a b ] max = 2 L R
Step 3: π’Rβ [ a b c ] max = 3 L R
Step 4: π’Rβ [ a b c a ] β 'a' duplicate! L R β βΌ π΄Lβ π΄L [ b c a ] max = 3 (restored) L R
Step 5: π’Rβ [ b c a b ] β 'b' duplicate! L R β βΌ π΄Lβ π΄L [ c a b ] max = 3 (final) L R
Legend: π’ = R expands (green) π΄ = L contracts (red) β = promise broken
Process: 1. Explorer advances until promise becomes TRUE 2. While promise holds β Gatekeeper advances, shrinking window 3. Record the window size just before promise breaks 4. RepeatThe window EXPANDS until valid, then CONTRACTS aggressively.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β Example: Minimum Size Subarray Sum β₯ 7 β β Sequence: [ 2 3 1 2 4 3 ] Promise: "Sum β₯ target" β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β β β βββββββ β β βSTARTβ β β ββββ¬βββ β β βΌ β β ββββββββββββββββββββββββββββββββ β β β π’ R advances (Explorer) β ββββββββββββββββββββββββββ β β β Add to sum β β β β βββββββββββββββββ€βββββββββββββββ β β β βΌ β β β βββββββββββββββββββββββ β β β β Promise satisfied? β β β β β (sum β₯ target?) β β β β ββββββββββ¬βββββββββββββ β β β No β Yes β β β ββββββββββββ΄βββββββββββββββββββββββ β β β β βΌ β β β β ββββββββββββββββββββββββββββββββββββββββββββββββββ β β β β β WHILE promise still holds: β β β β β β ββββββββββββββββββββββββββββββββββββββββββββ β β β β β β β β Update answer: min(ans, R-L+1) β β β β β β β β π΄ L advances (Gatekeeper) β β β β β β β β Subtract from sum β β β β β β β ββββββββββββββββββββββββββββββββββββββββββββ β β β β β ββββββββββββββββββββββββββββββββββββββββββββββββββ β β β β β β β β β βΌ β β β β ββββββββββββββββββββββββββββββββ β β β ββββββββββΊβ More elements? β β β β βββββββββββββββ¬βββββββββββββββββ β β β Yes β No β β β βββββββββββ΄ββββββββββ β β β β βΌ β β β β βββββββββββ β β β β β DONE β β β β β βββββββββββ β β β βββββββββββββββββββββββββββββββββββββββββββ β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββVisual Trace: βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Sequence: 2 3 1 2 4 3 target = 7 [0] [1] [2] [3] [4] [5]
Step 1: π’Rβ [ 2 ] sum=2 < 7 ans = β L,R (keep expanding)
Step 2: π’Rβ [ 2 3 ] sum=5 < 7 ans = β L R (keep expanding)
Step 3: π’Rβ [ 2 3 1 ] sum=6 < 7 ans = β L R (keep expanding)
Step 4: π’Rβ [ 2 3 1 2 ] sum=8 β₯ 7 β VALID! ans = min(β, 3 - 0 + 1) = 4 L R β βΌ π΄Lβ π΄L [ 3 1 2 ] sum=8-2 < 7 (stop contracting) L R
Step 5: π’Rβ [ 3 1 2 4 ] sum=10 β₯ 7 β VALID! ans = min(4, 4) = 4 L R β βΌ π΄Lβ π΄L [ 1 2 4 ] sum=7 β₯ 7 β VALID! ans = min(4, 3) = 3 L R β βΌ π΄Lβ π΄L [ 2 4 ] sum=6 < 7 (stop) L R
Step 6: π’Rβ [ 2 4 3 ] sum=9 β₯ 7 β VALID! ans = min(4, 3) = 3 L R β βΌ π΄Lβ π΄L [ 4 3 ] sum=7 β₯ 7 β VALID! ans = min(4, 2) = 2 L R β βΌ π΄Lβ π΄L [ 3 ] sum=3 < 7 (stop) L,R
Legend: π’ = R expands π΄ = L contracts β = promise satisfied
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 1. Am I looking for a CONTIGUOUS subarray or substring? β β βββ No? β Not sliding window β β β β 2. Can I describe a PROPERTY that makes a window valid? β β βββ No? β Probably not sliding window β β β β 3. Can I UPDATE that property in O(1) when I add/remove β β a single element? β β βββ No? β Sliding window won't give O(n) β β β β 4. Is the answer about OPTIMIZING that window β β (longest, shortest, exists)? β β βββ Yes to all? β SLIDING WINDOW. β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ---
## The Three Window Shapes
### Shape 1: Variable Window, Maximize
**Story**: *"I want the biggest room that still follows the rules."*
- Invariant: Some constraint must not be violated
- Strategy: Grow greedily, shrink reluctantly
- Answer: Largest valid window seen
**Classic problems**: Longest substring without repeating characters, longest with at most K distinct
### Shape 2: Variable Window, Minimize
**Story**: *"I want the smallest container that still holds everything I need."*
- Invariant: Some requirement must be satisfied
- Strategy: Grow until valid, shrink aggressively
- Answer: Smallest valid window seen
**Classic problems**: Minimum window substring, minimum size subarray sum
### Shape 3: Fixed Window
**Story**: *"I'm looking through a frame of exact size β does it ever show what I'm looking for?"*
- Invariant: Window size exactly K
- Strategy: Add one, remove one, check condition
- Answer: Whether/where condition is met
**Classic problems**: Find all anagrams, check permutation inclusion
#### Fixed Window Example Trace (K=3)
ββββββββ¬ββββββββ¬ββββββββββ¬βββββββββββββββββββ¬ββββββββββββββββ¬βββββββββββββββββ β Step β Rβ β sum β L action β Window [L,R] β Max Sum β ββββββββΌββββββββΌββββββββββΌβββββββββββββββββββΌββββββββββββββββΌβββββββββββββββββ€ β 0 β 1 β 1 β β β [1] β (building...) β β 1 β 4 β 5 β β β [1,4] β (building...) β β 2 β 2 β 7 β β β [1,4,2] β 7 β¨ β β 3 β 10 β 7+10=17 β π΄ remove 1 β 16 β [4,2,10] β 16 β β 4 β 2 β 16+2=18 β π΄ remove 4 β 14 β [2,10,2] β 16 β β 5 β 3 β 14+3=17 β π΄ remove 2 β 15 β [10,2,3] β 16 β β 6 β 1 β 15+1=16 β π΄ remove 10β 6 β [2,3,1] β 16 β β 7 β 0 β 6+0=6 β π΄ remove 2 β 4 β [3,1,0] β 16 β β 8 β 20 β 4+20=24 β π΄ remove 3 β 21 β [1,0,20] β 21 β¨ β ββββββββ΄ββββββββ΄ββββββββββ΄βββββββββββββββββββ΄ββββββββββββββββ΄βββββββββββββββββ
Key insight: Once R reaches index 2 (K-1), every subsequent step: 1. π’ R advances β add new element 2. π΄ L advances β remove oldest element (exactly K steps behind) 3. Window size stays constant at K=3
Answer: Maximum sum = 21 (subarray [1, 0, 20])
---
## The State: What the Window Remembers
The window isn't just boundaries β it carries **state** about its contents:
| What You're Tracking | State Structure | Update Cost |
|---------------------|-----------------|-------------|
| Character uniqueness | Last-seen index map | O(1) |
| Character frequencies | Count map | O(1) |
| Distinct count | Map + size | O(1) |
| Running sum | Single integer | O(1) |
| Requirement satisfaction | "Have" vs "Need" counters | O(1) |
The magic of sliding window is that these states are **incrementally maintainable**. Adding an element updates the state. Removing an element reverses that update. No full recomputation needed.
---
## Visualizing the Dance
**Problem**: Longest substring without repeating characters
**Input**: `"abcabcbb"` β Find the longest window where all characters are unique.
| Step | $R$ (char) | State: `last_seen` | $L$ move? | Window `[L, R]` | Max Length |
|:----:|:----------:|:-------------------|:---------:|:---------------:|:----------:|
| 0 | `a` | `{a:0}` | β | `[0,0]` = "a" | 1 |
| 1 | `b` | `{a:0, b:1}` | β | `[0,1]` = "ab" | 2 |
| 2 | `c` | `{a:0, b:1, c:2}` | β | `[0,2]` = "abc" | 3 |
| 3 | `a` | `{a:3, b:1, c:2}` | π΄ `Lβ1` (skip past old 'a') | `[1,3]` = "bca" | 3 |
| 4 | `b` | `{a:3, b:4, c:2}` | π΄ `Lβ2` (skip past old 'b') | `[2,4]` = "cab" | 3 |
| 5 | `c` | `{a:3, b:4, c:5}` | π΄ `Lβ3` (skip past old 'c') | `[3,5]` = "abc" | 3 |
| 6 | `b` | `{a:3, b:6, c:5}` | π΄ `Lβ5` (skip past old 'b') | `[5,6]` = "cb" | 3 |
| 7 | `b` | `{a:3, b:7, c:5}` | π΄ `Lβ7` (skip past old 'b') | `[7,7]` = "b" | 3 |
**Answer**: 3 (substring `"abc"`)
**Key observations**:
- $R$ (Explorer) advances every single step β never skips, never retreats
- $L$ (Gatekeeper) only moves when a duplicate is found in the current window
- The jump optimization: $L$ jumps directly to `last_seen[char] + 1` instead of incrementing one by one
- Window length = `R - L + 1`
---
## The Moment of Recognition
You're reading a problem. You see phrases like:
- *"contiguous subarray"*
- *"substring"*
- *"longest/shortest"*
- *"at most K"*
- *"containing all of"*
And you feel it: *This is about maintaining something over a moving portion.*
That's your cue. The Explorer and Gatekeeper are ready. The window wants to slide.
---
## From Intuition to Implementation
Only now β after the dance is clear β does code become useful.
The template is always the same skeleton:
```python
def sliding_window(sequence):
state = initial_state()
left = 0
answer = initial_answer()
## 1. Explorer (R) always advances
for right, element in enumerate(sequence):
# Explorer: include new element
update_state_add(state, element)
## 2. Gatekeeper (L) acts to restore invariant
while promise_is_broken(state):
update_state_remove(state, sequence[left])
left += 1
# Record: this window is valid
answer = consider(answer, left, right)
return answer
The variations come from: 1. What is the promise? (determines the while condition) 2. What state do we track? (determines the data structure) 3. What are we optimizing? (determines how we update the answer)
Quick Reference: Problem β Pattern Mapping¶
| When You See... | Think... | Window Type |
|---|---|---|
| "Longest substring with unique chars" | Uniqueness promise | Maximize |
| "Longest with at most K distinct" | Count limit promise | Maximize |
| "Minimum window containing all of T" | Coverage promise | Minimize |
| "Subarray sum β₯ target" | Threshold promise | Minimize |
| "Contains permutation" | Exact match promise | Fixed |
| "Find all anagrams" | Exact match, collect all | Fixed |
Complete Implementations: From Intuition to Code¶
Now we translate intuition into production-ready code. Each solution demonstrates the Explorer-Gatekeeper dance with explicit state management and complexity guarantees.
Problem 1: Longest Substring Without Repeating Characters (LeetCode 3)¶
The Promise: "Every character in my view appears exactly once."
Why This Problem Matters: This is the canonical sliding window problem. Master it, and you understand the pattern.
def length_of_longest_substring(s: str) -> int:
"""
Find the length of the longest substring without repeating characters.
Intuition:
We maintain a window [left, right] where all characters are unique.
The Explorer (right pointer) advances one character at a time.
When a duplicate is detected, the Gatekeeper (left pointer) jumps
directly past the previous occurrence β no incremental crawling needed.
The Jump Optimization:
Instead of shrinking one position at a time (while loop), we record
each character's last-seen index. When we see a duplicate, we can
teleport the left boundary to skip all characters up to and including
the previous occurrence. This eliminates redundant comparisons.
Time Complexity: O(n)
- The right pointer visits each character exactly once: O(n)
- The left pointer only moves forward (never backward): amortized O(n)
- Dictionary operations (get, set): O(1) per operation
- Total: O(n) where n = len(s)
Space Complexity: O(min(n, Ο))
- Ο = size of character set (26 for lowercase, 128 for ASCII, etc.)
- In practice, O(1) for fixed alphabets, O(n) for arbitrary Unicode
Args:
s: Input string to search
Returns:
Length of the longest substring with all unique characters
Examples:
>>> length_of_longest_substring("abcabcbb")
3 # "abc"
>>> length_of_longest_substring("bbbbb")
1 # "b"
>>> length_of_longest_substring("pwwkew")
3 # "wke"
"""
# State: Maps each character to its most recent index
# This enables O(1) duplicate detection and O(1) jump calculation
last_seen_at: dict[str, int] = {}
# Window boundaries: [left, right] inclusive
left = 0
max_length = 0
# Explorer advances through every position
for right, char in enumerate(s):
# Duplicate detection: Is this char already in our current window?
# Key insight: We only care if the previous occurrence is at or after 'left'
# Characters before 'left' are outside our window β they don't count
if char in last_seen_at and last_seen_at[char] >= left:
# Gatekeeper acts: Jump past the previous occurrence
# The +1 ensures we exclude the duplicate itself
left = last_seen_at[char] + 1
# Record this character's position for future duplicate detection
last_seen_at[char] = right
# The window [left, right] is now guaranteed unique
# Update our answer if this window is the largest seen
current_length = right - left + 1
max_length = max(max_length, current_length)
return max_length
# Verification
if __name__ == "__main__":
test_cases = [
("abcabcbb", 3),
("bbbbb", 1),
("pwwkew", 3),
("", 0),
("au", 2),
("dvdf", 3),
]
for s, expected in test_cases:
result = length_of_longest_substring(s)
status = "β" if result == expected else "β"
print(f"{status} Input: {s!r:15} β {result} (expected {expected})")
Complexity Deep Dive:
| Operation | Count | Cost | Total |
|---|---|---|---|
| Right pointer advances | n | O(1) | O(n) |
| Left pointer advances | β€ n (total) | O(1) | O(n) |
| Dictionary lookup/update | n | O(1) average | O(n) |
| Total | O(n) |
The left pointer never retreats. Each character index is visited by left at most once across the entire algorithm, giving us the O(n) guarantee.
Problem 2: Longest Substring with At Most K Distinct Characters (LeetCode 340)¶
The Promise: "I contain no more than K different characters."
The Difference from Problem 1: We can't jump β we must shrink incrementally because removing one character might still leave us with too many distinct characters.
def length_of_longest_substring_k_distinct(s: str, k: int) -> int:
"""
Find the length of the longest substring with at most K distinct characters.
Intuition:
The window [left, right] maintains at most K distinct characters.
When adding a character causes distinct count to exceed K, we shrink
from the left until we're back to K or fewer distinct characters.
Why We Can't Jump:
Unlike the unique-character problem, removing one character doesn't
guarantee we restore validity. We might need to remove several
characters before the distinct count drops. Hence, we use a while-loop.
State Design:
We use a frequency map rather than a last-seen-index map because:
1. We need to know when a character's count drops to zero (to decrement distinct count)
2. The len(frequency_map) tells us the distinct count directly
Time Complexity: O(n)
- Right pointer: n iterations, O(1) per iteration
- Left pointer: moves at most n times total (amortized)
- Each character enters and exits the window at most once
Space Complexity: O(K)
- The frequency map contains at most K+1 entries at any time
- Before we shrink, we might briefly have K+1 entries
Args:
s: Input string
k: Maximum number of distinct characters allowed
Returns:
Length of the longest valid substring
Examples:
>>> length_of_longest_substring_k_distinct("eceba", 2)
3 # "ece"
>>> length_of_longest_substring_k_distinct("aa", 1)
2 # "aa"
"""
if k == 0:
return 0
# State: Frequency count of each character in current window
# The length of this dict = number of distinct characters
char_count: dict[str, int] = {}
left = 0
max_length = 0
for right, char in enumerate(s):
# Explorer adds new character to window
char_count[char] = char_count.get(char, 0) + 1
# Gatekeeper shrinks window while we have too many distinct characters
# This is a while-loop, not an if β we may need multiple shrinks
while len(char_count) > k:
left_char = s[left]
char_count[left_char] -= 1
# Critical: Remove from dict when count reaches zero
# This keeps len(char_count) accurate for distinct count
if char_count[left_char] == 0:
del char_count[left_char]
left += 1
# Window is now valid: at most K distinct characters
max_length = max(max_length, right - left + 1)
return max_length
# Verification
if __name__ == "__main__":
test_cases = [
(("eceba", 2), 3),
(("aa", 1), 2),
(("a", 0), 0),
(("aabbcc", 2), 4),
]
for (s, k), expected in test_cases:
result = length_of_longest_substring_k_distinct(s, k)
status = "β" if result == expected else "β"
print(f"{status} Input: {s!r}, k={k} β {result} (expected {expected})")
Engineering Note: The deletion del char_count[left_char] is essential. Without it, len(char_count) would count characters with zero frequency, breaking our invariant check.
Problem 3: Minimum Window Substring (LeetCode 76)¶
The Promise: "I contain all required characters with sufficient frequency."
The Paradigm Shift: Now we're minimizing, not maximizing. We expand until valid, then shrink aggressively while staying valid.
def min_window(s: str, t: str) -> str:
"""
Find the minimum window in s that contains all characters of t.
Intuition:
Phase 1 (Expand): Explorer advances until window contains all of t.
Phase 2 (Contract): Gatekeeper shrinks window while it remains valid.
Record the smallest valid window, then continue exploring.
The Satisfied Counter Optimization:
Naively checking "do we have all characters?" requires O(|t|) per step.
Instead, we track how many unique characters have met their quota.
When `chars_satisfied == chars_required`, the window is valid.
This reduces per-step cost from O(|t|) to O(1).
Time Complexity: O(|s| + |t|)
- Building need_count: O(|t|)
- Main loop: O(|s|) β each character enters and exits once
- All dictionary operations: O(1) each
Space Complexity: O(|t|)
- need_count: O(unique chars in t)
- have_count: O(unique chars in t) β we only track needed chars
Args:
s: Source string to search in
t: Target string containing required characters
Returns:
Minimum window substring, or "" if no valid window exists
Examples:
>>> min_window("ADOBECODEBANC", "ABC")
"BANC"
>>> min_window("a", "a")
"a"
>>> min_window("a", "aa")
""
"""
if not t or not s:
return ""
# Build the requirement: what characters do we need, and how many of each?
need_count: dict[str, int] = {}
for char in t:
need_count[char] = need_count.get(char, 0) + 1
# State: what characters do we have in current window?
have_count: dict[str, int] = {}
# Optimization: Track satisfaction at character level
# chars_satisfied = count of unique characters meeting their quota
chars_satisfied = 0
chars_required = len(need_count) # number of unique characters in t
# Answer tracking
min_length = float('inf')
result_start = 0
left = 0
for right, char in enumerate(s):
# Explorer: Add character to window
have_count[char] = have_count.get(char, 0) + 1
# Did adding this character satisfy a requirement?
# We check for exact equality to avoid double-counting
if char in need_count and have_count[char] == need_count[char]:
chars_satisfied += 1
# Gatekeeper: Try to shrink while window remains valid
while chars_satisfied == chars_required:
# Current window is valid β record if it's the smallest
window_length = right - left + 1
if window_length < min_length:
min_length = window_length
result_start = left
# Remove leftmost character
left_char = s[left]
have_count[left_char] -= 1
# Did removing break a requirement?
if left_char in need_count and have_count[left_char] < need_count[left_char]:
chars_satisfied -= 1
left += 1
if min_length == float('inf'):
return ""
return s[result_start : result_start + min_length]
# Verification
if __name__ == "__main__":
test_cases = [
(("ADOBECODEBANC", "ABC"), "BANC"),
(("a", "a"), "a"),
(("a", "aa"), ""),
(("aa", "aa"), "aa"),
]
for (s, t), expected in test_cases:
result = min_window(s, t)
status = "β" if result == expected else "β"
print(f"{status} s={s!r}, t={t!r} β {result!r} (expected {expected!r})")
Complexity Breakdown:
| Phase | Operations | Complexity |
|---|---|---|
Build need_count | Iterate over t | O(|t|) |
| Expand (right pointer) | Each char enters once | O(|s|) |
| Contract (left pointer) | Each char exits at most once | O(|s|) |
| Total | O(|s| + |t|) |
Problem 4: Find All Anagrams in a String (LeetCode 438)¶
The Promise: "I contain exactly the same character frequencies as the pattern."
Fixed Window Property: Since anagrams have the same length, we maintain a window of exactly len(p).
def find_anagrams(s: str, p: str) -> list[int]:
"""
Find all starting indices of p's anagrams in s.
Intuition:
An anagram has the exact same character frequencies as the pattern.
Since length must match, we use a fixed-size window of len(p).
At each position, check if window frequencies match pattern frequencies.
The Match Counter Optimization:
Instead of comparing two frequency maps (O(26) for lowercase),
we track how many characters have matching frequencies.
When all match, we've found an anagram.
Careful State Transitions:
When adding a character:
- If it now matches the pattern frequency: matches++
- If it just exceeded the pattern frequency: matches--
When removing a character:
- If it was matching and now isn't: matches--
- If it was exceeding and now matches: matches++
Time Complexity: O(|s| + |p|)
- Build pattern frequency: O(|p|)
- Slide window over s: O(|s|) with O(1) per step
Space Complexity: O(1)
- Two frequency maps bounded by alphabet size (26 for lowercase)
Args:
s: Source string to search in
p: Pattern string (looking for its anagrams)
Returns:
List of starting indices where anagrams of p begin
Examples:
>>> find_anagrams("cbaebabacd", "abc")
[0, 6]
>>> find_anagrams("abab", "ab")
[0, 1, 2]
"""
result: list[int] = []
pattern_len = len(p)
source_len = len(s)
if pattern_len > source_len:
return result
# Build pattern frequency map
pattern_freq: dict[str, int] = {}
for char in p:
pattern_freq[char] = pattern_freq.get(char, 0) + 1
# Window frequency map
window_freq: dict[str, int] = {}
# Track how many characters have matching frequencies
chars_matched = 0
chars_to_match = len(pattern_freq)
for right in range(source_len):
# Add character at right edge
right_char = s[right]
window_freq[right_char] = window_freq.get(right_char, 0) + 1
# Update match count for added character
if right_char in pattern_freq:
if window_freq[right_char] == pattern_freq[right_char]:
chars_matched += 1
elif window_freq[right_char] == pattern_freq[right_char] + 1:
# We just went from matching to exceeding
chars_matched -= 1
# Remove character at left edge when window exceeds pattern length
left = right - pattern_len
if left >= 0:
left_char = s[left]
# Update match count for removed character BEFORE decrementing
if left_char in pattern_freq:
if window_freq[left_char] == pattern_freq[left_char]:
# We're about to break this match
chars_matched -= 1
elif window_freq[left_char] == pattern_freq[left_char] + 1:
# Removing brings us from exceeding to matching
chars_matched += 1
window_freq[left_char] -= 1
if window_freq[left_char] == 0:
del window_freq[left_char]
# Check for anagram when window size equals pattern size
if right >= pattern_len - 1 and chars_matched == chars_to_match:
result.append(right - pattern_len + 1)
return result
# Verification
if __name__ == "__main__":
test_cases = [
(("cbaebabacd", "abc"), [0, 6]),
(("abab", "ab"), [0, 1, 2]),
(("aaaaaaaaaa", "aaaaaaaaaaaaa"), []),
]
for (s, p), expected in test_cases:
result = find_anagrams(s, p)
status = "β" if result == expected else "β"
print(f"{status} s={s!r}, p={p!r} β {result} (expected {expected})")
Problem 5: Minimum Size Subarray Sum (LeetCode 209)¶
The Promise: "My sum is at least the target."
Numeric Sliding Window: Instead of tracking character frequencies, we track a running sum. The principle remains identical.
def min_subarray_len(target: int, nums: list[int]) -> int:
"""
Find the minimal length of a subarray whose sum is >= target.
Intuition:
This is the numeric equivalent of minimum window substring.
Expand until sum >= target, then shrink while sum stays >= target.
Track the smallest window that ever achieves the target.
Why Positive Numbers Matter:
This algorithm works because all elements are positive.
Adding an element always increases the sum.
Removing an element always decreases the sum.
This monotonicity is what makes sliding window viable.
Caution for Interviews:
If the array can contain negatives, sliding window doesn't work!
You'd need a different approach (prefix sums + monotonic deque).
Time Complexity: O(n)
- Right pointer: visits each element once
- Left pointer: moves forward only, at most n times total
- Each element enters and exits the window at most once
Space Complexity: O(1)
- Only a few integer variables (sum, pointers, min_length)
Args:
target: The minimum sum we need to achieve
nums: Array of positive integers
Returns:
Minimum length of valid subarray, or 0 if impossible
Examples:
>>> min_subarray_len(7, [2, 3, 1, 2, 4, 3])
2 # [4, 3]
>>> min_subarray_len(11, [1, 1, 1, 1, 1, 1, 1, 1])
0 # Impossible
"""
n = len(nums)
if n == 0:
return 0
# State: Running sum of current window [left, right]
window_sum = 0
left = 0
min_length = float('inf')
for right, num in enumerate(nums):
# Explorer: Expand window by including nums[right]
window_sum += num
# Gatekeeper: Shrink while sum meets target
# We want the SMALLEST valid window, so shrink aggressively
while window_sum >= target:
# Current window is valid β record its size
current_length = right - left + 1
min_length = min(min_length, current_length)
# Remove leftmost element
window_sum -= nums[left]
left += 1
return min_length if min_length != float('inf') else 0
# Verification
if __name__ == "__main__":
test_cases = [
((7, [2, 3, 1, 2, 4, 3]), 2),
((4, [1, 4, 4]), 1),
((11, [1, 1, 1, 1, 1, 1, 1, 1]), 0),
((15, [1, 2, 3, 4, 5]), 5),
]
for (target, nums), expected in test_cases:
result = min_subarray_len(target, nums)
status = "β" if result == expected else "β"
print(f"{status} target={target}, nums={nums} β {result} (expected {expected})")
The O(n) Guarantee Visualized:
Element: [2] [3] [1] [2] [4] [3]
β β β β β β
Right visits: β β β β β β = 6 times
Left visits: β β β β β β = 6 times (at most)
βββββββββ
Total operations: β€ 2n = O(n)
Time Complexity: The Definitive Analysis¶
Every sliding window algorithm achieves O(n) through the two-pointer invariant:
| Algorithm | Per-Element Cost | Total Visits | Complexity |
|---|---|---|---|
| Right pointer advance | O(1) | n | O(n) |
| Left pointer advance | O(1) | β€ n | O(n) |
| State update (add/remove) | O(1) | 2n | O(n) |
| Combined | O(n) |
The Amortized Argument: While the inner while loop might run multiple times for a single right advance, the total number of left advances across the entire algorithm is bounded by n. This is because left starts at 0 and can only increase, never decrease, and can never exceed n.
The Pattern in One Sentence¶
Sliding Window is the art of maintaining a valid contiguous view by advancing eagerly and retreating only when necessary.
When you see a problem about optimizing over contiguous sequences with incrementally checkable properties β you've found your window.
Let it slide.
For additional variations and template reference, see templates.md.