The sliding window is a scanning technique. A fixed-size frame moves across your data — one element enters on the right, one leaves on the left. You never recompute from scratch.
Watch the sliding window process an array to find the maximum sum of any contiguous subarray of size k = 3.
Every sliding window problem asks you to define four things. Once you know these four, you can solve any variant — the skeleton stays the same, only the definitions change.
This is the template every problem in this series will follow. The four functions are the only things that change.
def solve(arr, k): left = 0 state = init() # ← Q1: what does your window hold? for right in range(len(arr)): # ─── STEP 1: new element enters on the right ─── add(arr[right]) # ─── STEP 2: window is now full ──────────────── if right >= k - 1: # ─── STEP 3: use the window ──────────────────── use_window() # ─── STEP 4: oldest element leaves on the left ─ remove(arr[left]) left += 1
⚡ The one rule: Only read the window when it is fully formed — when right >= k - 1. Reading before that is the single most common bug in sliding window code.
The window is just two indices — left and right — scanning through the same array. They never go backwards.
indices: 0 1 2 3 4 5 6 7 array: [ 3, 1, 4, 1, 5, 9, 2, 6 ] step 1: L ← right=0, size 1, growing R step 2: L───R ← right=1, size 2, growing step 3: L───────R ← right=2, FULL. use(). then slide. step 4: L───────R ← right=3, FULL. use(). slide. step 5: L───────R ← right=4, FULL. use(). slide. Once full: left and right move in lockstep. Size never changes. Only position does.
🔍 What "remove" actually means: There is no array deletion. remove(arr[left]) simply undoes the effect that element had on your state variable. The array stays intact. Only your bookkeeping moves forward.
All sliding window problems fall into one of two families. The problems in this series all use fixed-size windows.
| Type | What stays constant? | When does left move? | Example |
|---|---|---|---|
| Fixed 🔒 | Window size = k (always) | Every step, in lockstep with right | LC 643, 1343, 1456, 1100, 1652 |
| Variable 🔓 | A constraint (validity condition) | Only when the constraint is violated | LC 3, 209, 76 — covered next |
LC 219 in this series is a small hybrid — it uses a set to check membership within distance k, so removal is index-triggered. You'll see exactly how that plays out.
The skeleton never changes. Read each problem by asking: what is different from the one before it? That question is the whole lesson.
What it asks: Given an integer array and a window size k, find the contiguous subarray of length k with the maximum average. Return that average.
The window holds a running sum. Every slide is a two-number transaction: subtract the element that just left, add the element that just arrived. Everything else in the window is unaffected. At the end, divide by k exactly once.
💡 Track max_sum, not max_avg. Division is expensive and floaty. A bigger sum means a bigger average, so compare sums. Divide once at the very end.
def findMaxAverage(nums, k): total = 0 max_sum = float('-inf') left = 0 for right in range(len(nums)): total += nums[right] # ADD: new element enters if right >= k - 1: # window is full max_sum = max(max_sum, total) # USE: record best total -= nums[left] # REMOVE: old element leaves left += 1 return max_sum / k # divide ONCE at the end
max_avg (a float) instead of max_sum (an integer). Float comparison accumulates rounding error.What it asks: Count the number of subarrays of length k whose average is greater than or equal to a given threshold.
💡 Eliminate the division entirely. avg ≥ threshold is equivalent to sum ≥ k × threshold. Multiply once at the start, compare integers forever. No floats, no division inside the loop.
def numOfSubarrays(arr, k, threshold): target = k * threshold # multiply once, avoid floats forever total = 0 count = 0 left = 0 for right in range(len(arr)): total += arr[right] if right >= k - 1: if total >= target: # USE: does this window qualify? count += 1 # ← this is the only changed part total -= arr[left] left += 1 return count
> vs >= — the problem says "greater than or equal to". Re-read the condition before submitting.1. A consonant is a 0. The sum is the vowel count.What it asks: Given a string and window size k, find the maximum number of vowels in any substring of length k.
This is LC 643 in disguise. Replace "value of the element" with "is it a vowel (1) or not (0)". The running sum IS the vowel count. The add() and remove() steps just become conditional.
def maxVowels(s, k): vowels = set('aeiou') cnt = 0 max_cnt = 0 left = 0 for right in range(len(s)): if s[right] in vowels: # ADD: only count vowels cnt += 1 if right >= k - 1: max_cnt = max(max_cnt, cnt) if s[left] in vowels: # REMOVE: only undo vowels cnt -= 1 left += 1 return max_cnt
cnt for the leaving element — you only decrement if it was a vowel. The if guard is essential.s[right] == 'a' or == 'e' ... — just use a set lookup. Cleaner and O(1).What it asks: Given a string and window size k, count the number of substrings of length k where all characters are unique.
distinct: int — characters with freq > 0The key insight: if k characters fit into exactly k slots and distinct == k, there can be no repeats — each slot must hold a different character. You don't need to check every pair; just track how many distinct characters are in the window.
🔑 The distinct counter trick: Only update distinct when a frequency crosses the threshold — entering at 1 means a new character appeared; leaving at 0 means it's completely gone. Don't increment/decrement on every change, only on these crossings.
from collections import defaultdict def countKLengthSubstrings(s, k): if k > 26: # only 26 lowercase letters — impossible return 0 freq = defaultdict(int) # ← state upgraded to a frequency map distinct = 0 # chars currently in window with freq > 0 count = 0 left = 0 for right in range(len(s)): c = s[right] freq[c] += 1 if freq[c] == 1: # first time c appears in window distinct += 1 if right >= k - 1: if distinct == k: # all characters are unique count += 1 d = s[left] freq[d] -= 1 if freq[d] == 0: # c is fully gone from window distinct -= 1 left += 1 return count
distinct when freq[d] hits exactly 0. If you decrement unconditionally, distinct goes negative for duplicates.What it asks: Return True if any two equal values in the array exist at indices no more than k apart.
This problem doesn't say "substrings of size exactly k". It says "any two equal values within distance k". The window holds the last k values seen. If the current value is already in there, you found a pair. Check membership before inserting.
⚠ Order matters here. The use() step (the duplicate check) happens before add(). You're checking whether the incoming value collides with what's already in the window, before you actually add it.
def containsNearbyDuplicate(nums, k): seen = set() # ← state is a set, not a map for right in range(len(nums)): if nums[right] in seen: # USE first — before adding return True seen.add(nums[right]) # ADD if right >= k: # REMOVE when window exceeds k seen.remove(nums[right - k]) return False
| Structure | Answers the question | Use when |
|---|---|---|
| freq map | "How many of each are in the window?" | LC 1100 — you need counts |
| set | "Is this value anywhere in the window?" | LC 219 — you only need presence |
k previous elements, so remove at right >= k (not k - 1).nums[right - k] is the element that entered exactly k steps ago. Off by one here causes a silent wrong answer.% n.What it asks: Given a circular code array, replace each element code[i] with the sum of the next k elements (if k > 0) or the previous |k| elements (if k < 0). If k = 0, all values are zero.
% nThe "circular" part only lives in the indexing. Build the first window once — compute its sum directly. Then for each of the n positions: record the sum, subtract the element that just left, add the element that just arrived — both using % n to wrap around.
🔑 Build first, then slide. Computing the first window takes O(|k|). After that, every slide is O(1) — one subtraction, one addition. Don't recompute from scratch each time.
def decrypt(code, k): n = len(code) result = [0] * n if k == 0: return result # all zeros # Set window direction based on sign of k if k > 0: start, size = 1, k # next k elements else: start, size = n - abs(k), abs(k) # previous |k| elements # Build the first window once total = sum(code[(start + j) % n] for j in range(size)) for i in range(n): result[i] = total # USE: record answer for this position total -= code[(start + i) % n] # REMOVE: left element exits total += code[(start + i + size) % n] # ADD: right element enters return result
(idx % n + n) % n to be safe in C++/Java.Every problem differs in exactly two places: what the window holds, and what the use() step does.
| Problem | Window holds | State type | use() triggers when | Output |
|---|---|---|---|---|
| LC 643 | Sum of elements | int | right ≥ k−1 → track max sum | max / k |
| LC 1343 | Sum of elements | int | right ≥ k−1 and sum ≥ k×t | count |
| LC 1456 | Vowel count | int | right ≥ k−1 → track max count | max vowels |
| LC 1100 | Frequency map + distinct counter | dict + int | right ≥ k−1 and distinct == k | count |
| LC 219 | Set of last k values | set | val in seen (before add) | bool |
| LC 1652 | Circular sum (mod n) | int | every position i | array |
When your solution is "almost right" but fails test cases, go through this list in order.
right >= k - 1, not right >= k. Test with k=1 mentally.% n.