Sliding Window — Fixed Size
How to answer 'what is the best K-element stretch of this array?' in O(n) instead of O(n·K). We build the idea from a train window, prove why reuse beats recompute, and see the skeleton in four different problems.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- Watch it happen, frame by frame
- Now, the code — line by line
- Why is it correct?
- Why is it so fast?
- When should I reach for this? (the trigger list)
- The same trick in four disguises
- Disguise 1 — Average of Subarrays of Size K
- Disguise 2 — Maximum of All Subarrays of Size K (LC #239)
- Disguise 3 — First Negative Number in Each Window of Size K
- Disguise 4 — Count Anagrams (LC #438 — Fixed Char Window)
- Traps that catch beginners
- Say it like a pro (interview one-liner)
- Check yourself
- Practice problems
Before we start
The first three chapters used two or three pointers to scan individual elements. This chapter introduces a new idea: treating a contiguous block of K elements as a single moving object — a window — that slides across the array. By the end you will be able to:
- See the sliding window as a train compartment gliding along a track.
- Explain why reusing the previous window's result makes this O(n) instead of O(n·K).
- Recognise the fixed-window skeleton in problems about sums, averages, and maximums over K-length subarrays.
Stop at every Pause & Think box before reading on.
Picture this first (no code yet)
A real-life story
You are riding a long train. The train has hundreds of cars. You are sitting in a compartment that shows exactly three cars through your window at any time.
A friend challenges you: "Which three consecutive cars have the highest combined weight?" The weight of each car is written on its roof.
The naïve approach: For every possible group of three, lean out, add up the three weights, record the total, lean back in. Do this for every possible starting position.
What you actually do: You note the total of the first three cars. Then the train moves one car forward — your window shifts by one. Here's the insight: you don't re-add all three cars. You simply subtract the car that left the window on the left, and add the car that entered the window on the right. One subtraction, one addition — and you have the new total.
That's it. No matter how many cars the train has, each window shift costs exactly two operations.
The train is the array. The window is a block of K elements. Every shift costs O(1), not O(K).
The actual problem
Given an array of integers and a window size K, find the maximum sum of any K consecutive elements.
Example:
array = [2, 1, 5, 1, 3, 2]
K = 3
windows: [2,1,5]=8 [1,5,1]=7 [5,1,3]=9 [1,3,2]=6
answer = 9
First, the slow way (so you feel the pain)
The obvious approach: for every starting position, add up K elements.
def max_sum_slow(arr, k):
n = len(arr)
best = float('-inf')
for i in range(n - k + 1): # every starting position
total = 0
for j in range(i, i + k): # add K elements
total += arr[j]
best = max(best, total)
return best
For each of the n - k + 1 windows you do k additions. Total work:
n = 10, k = 3 → about 24 operations
n = 10,000, k = 1,000 → about 9,000,000 operations
n = 100,000, k = 50,000 → about 2,500,000,000 operations
The second approach is the train trick: reuse what you computed last time.
The turning point
Look at two adjacent windows:
Window 1: arr[0] + arr[1] + arr[2] = 2 + 1 + 5 = 8
Window 2: arr[1] + arr[2] + arr[3] = 1 + 5 + 1 = 7
Notice how much they share: arr[1] and arr[2] appear in both. We computed them twice.
Pause & think
If you already know window_sum = arr[0] + arr[1] + arr[2] = 8, what is the fastest way to compute arr[1] + arr[2] + arr[3]? You should need only two numbers, not three additions.
The answer: new_sum = old_sum - arr[0] + arr[3].
Subtract the element that left the window on the left. Add the element that entered the window on the right. One subtraction, one addition — the window "slides" forward in O(1) regardless of K.
The one idea to remember
The entire pattern in one sentence
Compute the first window's result from scratch, then for every subsequent window subtract the outgoing element and add the incoming element — reuse beats recompute.
Watch it happen, frame by frame
Array: [2, 1, 5, 1, 3, 2], K = 3.
Step 1 — Build the first window (positions 0, 1, 2):
window_sum = 2 + 1 + 5 = 8
best = 8
Step 2 — Slide: remove arr[0]=2, add arr[3]=1
window_sum = 8 - 2 + 1 = 7
best = max(8, 7) = 8
Step 3 — Slide: remove arr[1]=1, add arr[4]=3
window_sum = 7 - 1 + 3 = 9
best = max(8, 9) = 9
Step 4 — Slide: remove arr[2]=5, add arr[5]=2
window_sum = 9 - 5 + 2 = 6
best = max(9, 6) = 9
No more positions. Answer = 9. ✅
The outgoing element is always at index i - 1 when the new window ends at index i + k - 1. Let's see this with concrete indices:
i=0: window covers [0..2], sum = build from scratch
i=1: remove arr[i-1]=arr[0], add arr[i+k-1]=arr[3]
i=2: remove arr[i-1]=arr[1], add arr[i+k-1]=arr[4]
i=3: remove arr[i-1]=arr[2], add arr[i+k-1]=arr[5]
Pause & think
Cover the code section below. In array [4, 2, 1, 7, 8, 1, 2, 8, 1, 0] with K = 3, what is the maximum window sum, and which window is it? Trace it step by step.
Check your trace
First window [4,2,1] = 7. best=7
Slide: -4+7 → [2,1,7]=10. best=10
Slide: -2+8 → [1,7,8]=16. best=16
Slide: -1+1 → [7,8,1]=16. best=16
Slide: -7+2 → [8,1,2]=11. best=16
Slide: -8+8 → [1,2,8]=11. best=16
Slide: -1+1 → [2,8,1]=11. best=16
Slide: -2+0 → [8,1,0]=9. best=16
Answer = 16 (the window [1,7,8] starting at index 2)
Now, the code — line by line
def max_sum_fixed(arr, k):
n = len(arr)
if n < k:
return None # not enough elements for even one window
# Step 1: Build the first window from scratch
window_sum = sum(arr[:k]) # add the first k elements
best = window_sum
# Step 2: Slide the window across the rest of the array
for i in range(k, n): # i is the index of the incoming element
window_sum += arr[i] # add the new element entering on the right
window_sum -= arr[i - k] # remove the old element leaving on the left
best = max(best, window_sum)
return best
Mapping each line to the train story:
window_sum = sum(arr[:k])— tally the first K cars in the window before the train starts moving.for i in range(k, n):— for each forward shift of the train (each new car entering the window on the right).window_sum += arr[i]— the new car on the right enters the window; add its weight.window_sum -= arr[i - k]— the old car on the left exits the window; subtract its weight.best = max(best, window_sum)— record the best total seen so far.
The key index relationship: when the incoming element is at index i, the outgoing element is at index i - k (exactly K positions behind).
Why is it correct?
At every step, window_sum equals exactly arr[i-k+1] + arr[i-k+2] + ... + arr[i] — the sum of the K elements ending at position i. We can verify this with the invariant:
- After the first window:
window_sum = arr[0] + arr[1] + ... + arr[k-1]. ✓ - After each slide: we add
arr[i]and removearr[i-k], so the sum shifts from covering[i-k..i-1]to covering[i-k+1..i]. The window always covers exactly K consecutive elements. ✓
Since we track the maximum over all valid windows, and we correctly compute every window's sum, the answer is guaranteed correct.
Why is it so fast?
The first window costs O(k) to build. After that, each of the n - k slides costs exactly 2 operations (one add, one subtract). Total: k + 2(n - k) = 2n - k ≈ O(n).
Compare to the brute force:
n = 100,000, k = 50,000
Slow: ~2,500,000,000 operations
Fast: ~150,000 operations (≈16,000× faster)
| Approach | Time | Space |
|---|---|---|
| Brute force (nested loops) | O(n · k) | O(1) |
| Sliding window (fixed) | O(n) | O(1) |
When should I reach for this? (the trigger list)
Reach for the fixed-size sliding window when you see:
- The problem asks about every subarray (or substring) of length exactly K.
- Words like "maximum," "minimum," "average," or "count" over a fixed-length window.
- A brute-force solution clearly has a nested loop where the inner loop always runs K times.
- The question involves a contiguous segment — order matters, elements must be adjacent.
- You need O(n) and the window size doesn't change as you slide.
The moment K is fixed and you're computing the same aggregate (sum, max, etc.) repeatedly over overlapping windows — slide, don't recompute.
The same trick in four disguises
Disguise 1 — Average of Subarrays of Size K
Instead of tracking the max sum, track the running average. Same slide formula:
def avg_subarrays(arr, k):
n = len(arr)
result = []
window_sum = sum(arr[:k])
result.append(window_sum / k)
for i in range(k, n):
window_sum += arr[i] - arr[i - k]
result.append(window_sum / k)
return result
Identical skeleton. Different output (list of averages instead of one maximum).
Disguise 2 — Maximum of All Subarrays of Size K (LC #239)
Now you want the maximum element (not sum) in each window. Summing doesn't work here — you can't "remove" a maximum arithmetically.
The trick: use a monotonic deque (a double-ended queue that stays sorted). But here's the key insight — the sliding window skeleton is the same. A window of size K still slides one step at a time. Only the data structure inside the window changes.
from collections import deque
def max_sliding_window(nums, k):
dq = deque() # stores indices; front is always the max of current window
result = []
for i in range(len(nums)):
# Remove elements outside the window
while dq and dq[0] < i - k + 1:
dq.popleft()
# Maintain decreasing order (remove smaller elements from back)
while dq and nums[dq[-1]] < nums[i]:
dq.pop()
dq.append(i)
# Window is complete after first k elements
if i >= k - 1:
result.append(nums[dq[0]])
return result
The outer loop slides the window. The deque is just the smarter "aggregate" inside. Same bones.
Disguise 3 — First Negative Number in Each Window of Size K
Find the first negative number in each K-length window:
from collections import deque
def first_negative(arr, k):
dq = deque() # stores indices of negative elements
result = []
for i in range(len(arr)):
if arr[i] < 0:
dq.append(i)
if dq and dq[0] < i - k + 1: # front left the window
dq.popleft()
if i >= k - 1:
result.append(arr[dq[0]] if dq else 0)
return result
Disguise 4 — Count Anagrams (LC #438 — Fixed Char Window)
Find all starting indices in string s where a window of length len(p) is an anagram of p. The window slides across s one character at a time; entering character increments its count, exiting character decrements it.
from collections import Counter
def find_anagrams(s, p):
k = len(p)
need = Counter(p)
have = Counter(s[:k])
result = [0] if have == need else []
for i in range(k, len(s)):
# Add incoming character
have[s[i]] += 1
# Remove outgoing character
outgoing = s[i - k]
have[outgoing] -= 1
if have[outgoing] == 0:
del have[outgoing]
if have == need:
result.append(i - k + 1)
return result
Same slide formula (add incoming, remove outgoing). The "aggregate" is now a frequency counter, not a sum.
Traps that catch beginners
Watch out for these
- Building the first window inside the slide loop. If you start the loop at
i = 0and try to checki >= k - 1to record results, the outgoing element indexi - kgoes negative for the first K-1 steps. Build the first window separately. - Off-by-one on the outgoing index. When the new window ends at index
i, the outgoing element is ati - k, noti - k - 1. Draw it out: if K=3 and the new window is[i-2, i-1, i], the dropped element is ati-3 = i - k. So it'sarr[i - k]. - Forgetting to check
n < k. If the array is shorter than the window, there are no valid windows. Guard at the top. - Thinking the window tracks indices, not values.
window_sumtracks the sum of values, not positions. The pointers are implicit — just the loop variableiand the offseti - k.
| Bug | Fix |
|---|---|
Loop from i=0, subtract arr[i-k] when i < k | Build first window with sum(arr[:k]), then loop from i = k |
Outgoing index is arr[i - k - 1] | It's arr[i - k] — the element exactly K positions behind the incoming one |
| No guard for short arrays | Add if n < k: return None before the first window |
Say it like a pro (interview one-liner)
"Since the window size is fixed at K, I'll compute the first window's sum directly and then slide one position at a time — adding the incoming element on the right and subtracting the outgoing element on the left. This avoids recomputing the K-element sum from scratch each time, reducing the complexity from O(n·K) to O(n)."
Remember this forever
Sliding Window — Fixed Size
Build the first window from scratch: window_sum = sum(arr[:k]).
Then for each slide: window_sum += arr[i] - arr[i - k].
Track the best value seen across all windows.
Cost: O(n) time, O(1) space · Trigger: best/count over every K-length subarray · Outgoing element: always at arr[i - k] when incoming is at arr[i]
Check yourself
Why can't we just use the nested loop? It also gives O(1) space.
The nested loop is O(n · k) time. For large n and k — like n = 100,000 and k = 50,000 — that's around 2.5 billion operations. The sliding window is O(n), roughly 150,000 operations for the same input — about 16,000× faster. The space trade-off doesn't matter when the time difference is that extreme.
When the window slides from position i to i+1, what exactly is the "outgoing" element?
If the new window ends at index i, it covers [i-k+1 .. i]. The previous window covered [i-k .. i-1]. The element that left is at index i-k. So you subtract arr[i - k] — the element exactly K positions to the left of the incoming one.
Can you use a fixed sliding window when the aggregate is "maximum element" instead of sum?
Not with simple arithmetic (you can't "un-max" a value). You still slide the window at fixed size, but you use a monotonic deque inside the window to track the current maximum in O(1) amortized per slide. The outer window skeleton stays identical — only the inner data structure changes.
What is the invariant that keeps the algorithm correct?
After processing each i ≥ k, window_sum equals exactly arr[i-k+1] + arr[i-k+2] + ... + arr[i] — the sum of the K elements ending at position i. This is maintained by the add-incoming / subtract-outgoing operation at each step.
Practice problems
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Maximum Sum Subarray of Size K | Easy | Pure fixed-window template | GFG |
| Average of Subarrays of Size K | Easy | Same slide, divide by K for output | GFG |
| Find All Anagrams in a String | Medium | Window of fixed length, Counter as aggregate | LC #438 |
| Permutation in String | Medium | Same as anagrams — check if any window matches | LC #567 |
| Sliding Window Maximum | Hard | Fixed window, monotonic deque for max | LC #239 |
When you can code the max-sum solution without notes, explain the outgoing-element index, and state why reuse beats recompute — you have learned this pattern.
Next up: Sliding Window — Variable Size, where the window expands and contracts based on a condition, and the trick is knowing when to shrink.