πͺ Sliding Window
Contiguous subarray/substring + a rule: grow right, shrink left, each index enters and leaves once. O(n).
Recognize the pattern
Every window of exactly k consecutive items (max sum, average, count) β fixed window with a running total
Longest substring/subarray such that <rule> β variable window, shrink while invalid, record after
Shortest subarray such that <rule> β variable window, record and shrink while valid
Rule is about letter counts (anagram, cover, at most k distinct) β Counter or 26-slot list for the window
Count subarrays with exactly k of something β at_most(k) β at_most(k β 1)
Max/min of every window of size k β monotonic deque of indices
Subsequence, or sums with negative numbers β not a window: think DP or prefix sums
Fixed-size window
One loop: add the entering item, remove the one k behind, record once full.
def fixed_window(nums: list[int], k: int) -> int:
window = 0
best = float("-inf")
for right in range(len(nums)):
window += nums[right] # enters
if right >= k:
window -= nums[right - k] # leaves
if right >= k - 1: # window full
best = max(best, window)
return best
Longest valid window
Write down "the window is valid whenβ¦" first. Record after shrinking.
from collections import Counter
def longest(s: str, k: int) -> int:
count = Counter()
left = best = 0
for right, ch in enumerate(s):
count[ch] += 1 # expand
while len(count) > k: # invalid
count[s[left]] -= 1
if count[s[left]] == 0:
del count[s[left]]
left += 1 # shrink
best = max(best, right - left + 1)
return best
Shortest valid window
Needs monotonicity (e.g. all numbers β₯ 0). Record inside the shrink loop.
def shortest(nums: list[int], target: int) -> int:
left = window = 0
best = float("inf")
for right, x in enumerate(nums):
window += x # expand
while window >= target: # valid
best = min(best, right - left + 1)
window -= nums[left] # shrink
left += 1
return 0 if best == float("inf") else best
Cover a target (need / have / formed)
Valid when every needed char is present often enough. formed changes only when a count crosses its requirement.
from collections import Counter
def min_cover(s: str, t: str) -> str:
need = Counter(t)
have = Counter()
formed, left = 0, 0
best = (float("inf"), 0, 0)
for right, ch in enumerate(s):
have[ch] += 1
if have[ch] == need[ch]:
formed += 1
while formed == len(need):
if right - left + 1 < best[0]:
best = (right - left + 1, left, right)
out = s[left]
have[out] -= 1
if have[out] < need[out]:
formed -= 1
left += 1
size, lo, hi = best
return "" if size == float("inf") else s[lo:hi + 1]
Exactly k = at most k β at most (k β 1)
Counting windows: every right adds right - left + 1 subarrays ending there.
def at_most(nums: list[int], k: int) -> int:
count: dict[int, int] = {}
left = total = 0
for right, x in enumerate(nums):
count[x] = count.get(x, 0) + 1
while len(count) > k:
y = nums[left]
count[y] -= 1
if count[y] == 0:
del count[y]
left += 1
total += right - left + 1
return total
def exactly(nums: list[int], k: int) -> int:
return at_most(nums, k) - at_most(nums, k - 1)
Window maximum (monotonic deque)
Deque holds indices with decreasing values; the front is the max.
from collections import deque
def window_max(nums: list[int], k: int) -> list[int]:
dq: deque[int] = deque()
out = []
for i, x in enumerate(nums):
while dq and nums[dq[-1]] <= x:
dq.pop()
dq.append(i)
if dq[0] <= i - k:
dq.popleft()
if i >= k - 1:
out.append(nums[dq[0]])
return out
Operation costs
| Operation | Time | Space |
|---|---|---|
| Fixed window over n items | O(n) | O(1) |
| Variable window (each index enters/leaves once) | O(n) | O(1) or O(alphabet) |
| Counter update / delete on slide | O(1) | O(distinct) |
| Compare two 26-slot count lists | O(26) = O(1) | β |
| Window max via monotonic deque | O(n) total | O(k) |
| Brute force: recompute every window | O(nΒ·k) or O(nΒ²) | β |
Watch for
Window length is
right - left + 1; the window of size k ending atrightstarts atright - k + 1.Longest: record after the shrink loop. Shortest: record inside it. Swapping them gives wrong answers.
When
len(counter)means "distinct in window", delete keys whose count drops to 0.Sum-based shrinking needs non-negative numbers; with negatives use prefix sums or a deque.
Last-seen index jumps: use
left = max(left, last[ch] + 1)soleftnever moves backwards ("abba").Empty input, k > len(nums), and "no valid window" (return 0 / "" not infinity).