Sliding Window Technique — Deep Dive
Advanced sliding window patterns
The basic sliding window handles sums and simple conditions. Advanced problems require more sophisticated window state: hash maps for character counts, deques for monotonic structures, and multiple windows for complex constraints.
Pattern 1: Window with character frequency map
Minimum Window Substring
Given strings s and t, find the minimum window in s that contains all characters of t.
from collections import Counter
def min_window(s, t):
if not t or not s:
return ""
target = Counter(t)
required = len(target) # unique chars needed
formed = 0 # unique chars satisfied
window_counts = {}
left = 0
best = (float('inf'), 0, 0) # (length, left, right)
for right, char in enumerate(s):
window_counts[char] = window_counts.get(char, 0) + 1
if char in target and window_counts[char] == target[char]:
formed += 1
while formed == required:
length = right - left + 1
if length < best[0]:
best = (length, left, right)
# Shrink from left
left_char = s[left]
window_counts[left_char] -= 1
if left_char in target and window_counts[left_char] < target[left_char]:
formed -= 1
left += 1
return "" if best[0] == float('inf') else s[best[1]:best[2] + 1]
print(min_window("ADOBECODEBANC", "ABC")) # "BANC"
Time: O(|s| + |t|). Space: O(|s| + |t|).
The formed counter is the critical optimization. Instead of comparing full frequency maps each iteration (O(26) or O(128)), we track how many unique characters have reached their required count. This makes the inner check O(1).
Longest Substring with At Most K Distinct Characters
def longest_k_distinct(s, k):
if k == 0:
return 0
counts = {}
left = 0
best = 0
for right, char in enumerate(s):
counts[char] = counts.get(char, 0) + 1
while len(counts) > k:
left_char = s[left]
counts[left_char] -= 1
if counts[left_char] == 0:
del counts[left_char]
left += 1
best = max(best, right - left + 1)
return best
print(longest_k_distinct("eceba", 2)) # 3 ("ece")
print(longest_k_distinct("araaci", 2)) # 4 ("araa")
Pattern 2: Sliding window maximum with deque
Finding the maximum in every window of size k seems like it requires O(k) per window (O(nk) total). A monotonic deque achieves O(n).
from collections import deque
def max_sliding_window(nums, k):
dq = deque() # stores indices, front is always max
result = []
for i, num in enumerate(nums):
# Remove elements outside window
while dq and dq[0] < i - k + 1:
dq.popleft()
# Remove smaller elements (they can never be the max)
while dq and nums[dq[-1]] < num:
dq.pop()
dq.append(i)
# Window is fully formed
if i >= k - 1:
result.append(nums[dq[0]])
return result
print(max_sliding_window([1, 3, -1, -3, 5, 3, 6, 7], 3))
# [3, 3, 5, 5, 6, 7]
Time: O(n) — each element enters and leaves the deque at most once. Space: O(k) for the deque.
Why monotonic deque works: The deque maintains indices in decreasing order of their values. When a new element arrives that is larger than the back of the deque, the smaller elements are removed — they can never be the maximum of any future window because the new element is both larger and newer.
Sliding window minimum
Same structure, just reverse the comparison:
def min_sliding_window(nums, k):
dq = deque()
result = []
for i, num in enumerate(nums):
while dq and dq[0] < i - k + 1:
dq.popleft()
while dq and nums[dq[-1]] > num: # changed > to maintain ascending
dq.pop()
dq.append(i)
if i >= k - 1:
result.append(nums[dq[0]])
return result
Pattern 3: String permutation / anagram matching
Check if s2 contains a permutation of s1
from collections import Counter
def check_inclusion(s1, s2):
if len(s1) > len(s2):
return False
target = Counter(s1)
window = Counter(s2[:len(s1)])
if window == target:
return True
for i in range(len(s1), len(s2)):
# Add right character
window[s2[i]] += 1
# Remove left character
left_char = s2[i - len(s1)]
window[left_char] -= 1
if window[left_char] == 0:
del window[left_char]
if window == target:
return True
return False
print(check_inclusion("ab", "eidbaooo")) # True ("ba" is permutation)
Optimization: Comparing Counter objects is O(26) for lowercase ASCII. For tighter performance, track the number of matching characters:
def check_inclusion_optimized(s1, s2):
if len(s1) > len(s2):
return False
target = [0] * 26
window = [0] * 26
for c in s1:
target[ord(c) - ord('a')] += 1
for c in s2[:len(s1)]:
window[ord(c) - ord('a')] += 1
matches = sum(1 for i in range(26) if target[i] == window[i])
for i in range(len(s1), len(s2)):
if matches == 26:
return True
# Add right
idx = ord(s2[i]) - ord('a')
window[idx] += 1
if window[idx] == target[idx]:
matches += 1
elif window[idx] == target[idx] + 1:
matches -= 1
# Remove left
idx = ord(s2[i - len(s1)]) - ord('a')
window[idx] -= 1
if window[idx] == target[idx]:
matches += 1
elif window[idx] == target[idx] - 1:
matches -= 1
return matches == 26
print(check_inclusion_optimized("ab", "eidbaooo")) # True
This makes each step truly O(1) — no counter comparison needed.
Pattern 4: Counting subarrays with a condition
Subarrays with at most K distinct elements
To count subarrays with exactly K distinct elements, use the identity: exactly_k = at_most_k - at_most_(k-1).
def subarrays_with_k_distinct(nums, k):
def at_most(k):
counts = {}
left = 0
result = 0
for right, num in enumerate(nums):
counts[num] = counts.get(num, 0) + 1
while len(counts) > k:
left_num = nums[left]
counts[left_num] -= 1
if counts[left_num] == 0:
del counts[left_num]
left += 1
result += right - left + 1 # all subarrays ending at right
return result
return at_most(k) - at_most(k - 1)
print(subarrays_with_k_distinct([1, 2, 1, 2, 3], 2)) # 7
Key insight: right - left + 1 counts all valid subarrays ending at right. The window [left, right] is valid, and so are [left+1, right], [left+2, right], etc. — all subarrays starting between left and right that end at right.
Pattern 5: Multiple conditions with two maps
Longest Repeating Character Replacement
Given a string, replace at most k characters to get the longest substring of identical characters.
def character_replacement(s, k):
counts = {}
left = 0
max_freq = 0
best = 0
for right, char in enumerate(s):
counts[char] = counts.get(char, 0) + 1
max_freq = max(max_freq, counts[char])
# Window is invalid if replacements needed > k
window_len = right - left + 1
if window_len - max_freq > k:
counts[s[left]] -= 1
left += 1
best = max(best, right - left + 1)
return best
print(character_replacement("AABABBA", 1)) # 4 ("AABA"→"AAAA")
Subtle optimization: max_freq is never decreased. This is correct because we only care about max_freq increasing (which could produce a longer valid window). If max_freq stays the same or decreases, the window cannot grow, so we just slide it forward without shrinking.
Performance comparison
Benchmarking sliding window vs brute force for “maximum sum subarray of size k”:
import timeit
def brute_force(nums, k):
return max(sum(nums[i:i+k]) for i in range(len(nums) - k + 1))
def sliding_window(nums, k):
window = sum(nums[:k])
best = window
for i in range(k, len(nums)):
window += nums[i] - nums[i - k]
best = max(best, window)
return best
import random
nums = [random.randint(1, 100) for _ in range(100_000)]
k = 1000
# Brute force: O(n*k) ≈ 10^8 operations
# Sliding window: O(n) ≈ 10^5 operations
For n=100,000 and k=1,000: brute force takes ~3 seconds, sliding window takes ~15 milliseconds. That is a 200× speedup that grows with k.
Edge cases to handle
- Empty input: Return 0 or empty string.
- Window larger than array: Return the full array or handle gracefully.
- All elements identical: Window expands to full length.
- Single element: Window of size 1.
- Negative numbers: Sliding window for max sum still works; for min sum, adjust the comparison.
def safe_sliding_window(nums, k):
if not nums or k <= 0 or k > len(nums):
return 0
# ... normal implementation
One thing to remember: Every sliding window problem boils down to: what enters the window, what leaves the window, and how do you maintain the window state efficiently. The deque handles order-dependent queries (max/min). The hash map handles frequency-dependent queries (distinct count, anagram). Choose the right state structure and the rest follows.
See Also
- Python Backtracking Algorithms How computers solve puzzles by trying every path, backing up when stuck, and never giving up until they find the answer.
- Python Big O Complexity Analysis Why some programs finish in a blink while others take forever — explained with pizza delivery and toy cleanup.
- Python Binary Search Implementation Find anything in a sorted list insanely fast using the same trick you already use with dictionaries and phone books.
- Python Dynamic Programming The clever trick of remembering answers you already figured out so you never solve the same puzzle twice.
- Python Graph Algorithms How computers navigate maps, friendships, and connections using dots and lines — explained without any math.