DSA sheet · Arrays · Sliding window pattern (fixed size + monotonic deque)
Sliding Window Maximum
The window here has a fixed size k, so sliding it is easy. The hard part is knowing the maximum of the window after every slide. A sum is easy to update (add the new item, subtract the old one), but a maximum isn't: when the biggest item leaves, you don't know who's next. The teacher goes from the two-loop brute force, to the idea of a heap, to the best answer, a monotonic deque that always keeps the current maximum at its front.
The deque trick (throw away items that can never be the answer again) is the same idea as the monotonic stack. It's worth learning properly, because it comes back in many "max/min of every window" problems.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · Sliding window and deques from scratch
- Part A · Brute force: max of every window with two loops
- Part B · Why a plain slide fails, and the heap idea
- Part C · Optimal: the monotonic deque
- Part D · Revision page
Part 0 · Before starting
Subarray and window
A subarray is a continuous piece of the array (no gaps). A window is a subarray we look at through two indexes, left and right, both included. Its length is right − left + 1.
The brute-force way to visit windows is two loops: one for the start, one walking over the items of that window. Neighbouring windows share almost all their items, so the inner loop keeps re-reading the same numbers. A sliding window moves step by step and only handles what changes: one item comes in on the right, one item leaves on the left.
Fixed vs variable windows
| Fixed-size window (this page) | Variable-size window | |
|---|---|---|
| size | always exactly k | grows and shrinks |
| how it moves | add nums[right]; the item at right − k falls out | expand right; while the window is invalid, shrink left |
| typical questions | sum / average / max of every window of size k | longest valid window, shortest valid window, count of valid windows (count += right − left + 1), and "exactly K = atMost(K) − atMost(K−1)" |
A variable window needs a monotonic rule (if a window is valid, every smaller window inside it is valid too). That's why it fails for sums with negative numbers. A fixed window doesn't need this: its size never changes, we just slide it.
For a fixed window of size k over n items, the windows start at 0, 1, …, n − k. So there are n − k + 1 windows, and that's the length of the answer list.
Which optimisation pattern? (the teacher's 4-pattern check)
| pattern | use it when… |
|---|---|
| Two pointers | you only care about the two ends, not what's between them |
| Sliding window | you need everything between a start and an end (whole windows) |
| Prefix sum | many range-sum queries |
| Kadane | best subarray sum with negative numbers |
The problem literally says "a window of size k moving from left to right" → fixed sliding window. The question is only how to get each window's max quickly.
What is a deque?
A deque ("deck", double-ended queue) is a line of items where you can add or remove at both ends, each in O(1).
| action | Python (collections.deque) | Java (the teacher's code) | cost |
|---|---|---|---|
| add at the back | dq.append(x) | addLast | O(1) |
| remove from the back | dq.pop() | pollLast | O(1) |
| remove from the front | dq.popleft() | pollFirst | O(1) |
| look at the back / front | dq[-1] / dq[0] | peekLast / peekFirst | O(1) |
A plain Python list is O(n) for removing from the front, so always use deque here.
What is a monotonic deque?
Monotonic = always going one way. In this problem the deque keeps indexes whose values are decreasing from front to back. Before adding a new item at the back, we pop every item at the back that is smaller than it. Result: the front is always the biggest value of the current window. Part C proves why the popped items are never needed again.
Part A · Brute force: max of every window with two loops
LeetCode 239 · Hard
1The question in simple words
You get an array nums and a window size k. Put the window on the first k items, then slide it one step to the right at a time until it hits the end. For every position of the window, write down the biggest number inside it. Return that list.
| window | items | max |
|---|---|---|
| 0..2 | [1, 3, −1] | 3 |
| 1..3 | [3, −1, −3] | 3 |
| 2..4 | [−1, −3, 5] | 5 |
| 3..5 | [−3, 5, 3] | 5 |
| 4..6 | [5, 3, 6] | 6 |
| 5..7 | [3, 6, 7] | 7 |
Answer: [3, 3, 5, 5, 6, 7]. The input has 8 items but the answer only has 6.
Why n − k + 1 answers? (the teacher derives it)
The very first answer appears only once the window is full, at index k − 1. The first k − 1 items can't produce an answer on their own. After that, every new item gives exactly one answer. So the answer length is n − (k − 1) = n − k + 1. Here 8 − 3 + 1 = 6 ✓. (Opening the bracket: −(k − 1) = −k + 1.)
2What the constraints tell us
1 ≤ nums.length ≤ 10⁵, and1 ≤ k ≤ nums.length, so k can also be as big as 10⁵.- Values can be negative (−10⁴ to 10⁴). So don't start a max at 0: start it from a real item of the window.
- With n and k both large, a "for each window, scan k items" approach can reach about 10⁹ steps (see step 9). Over the ~10⁸ limit → we need O(n) or close.
3Intuition
For each start i, look at the k items nums[i..i+k−1], find the biggest one, write it at res[i].
4Building the logic, the way the teacher does
i go?→ Only to n − k. From index 5 (8 − 3) there are exactly 3 items left: 5, 6, 7. From index 6 there would be only 2, which isn't a full window. So
i runs over range(n - k + 1), i.e. up to and including n − k.j loop stop? The teacher first wrote "until n", then "until k", then fixed it.→ "Until n" scans to the end of the array, which is more than one window. "Until k" only works for
i = 0: for i = 1 it would scan indexes 1..2, just 2 items. The window that starts at i ends at i + k − 1, so j runs in range(i, i + k). When i = 0 that's 0..k−1; when i = 1 it's 1..k; when i = 2 it's 2..k+1. Always k items.mx start as?→ The first item of the window,
nums[i]. Starting at 0 would be wrong for an all-negative window like [−1, −3, −5] (the answer is −1, not 0).i goes to which slot?→
res[i]. The window starting at 0 gives the 0th answer, the one starting at 1 gives the 1st answer, and so on.5Approach steps
- Make
resof size n − k + 1. - For each start
ifrom 0 to n − k:mx = nums[i]. - For
jfrom i to i + k − 1:mx = max(mx, nums[j]). res[i] = mx. Returnres.
6Code (Python)
class Solution:
def maxSlidingWindow(self, nums, k):
n = len(nums)
res = [0] * (n - k + 1) # one answer per window
for i in range(n - k + 1): # i = start of the window, up to n - k
mx = nums[i] # start from a real item (values can be negative)
for j in range(i, i + k): # exactly the k items of this window
mx = max(mx, nums[j])
res[i] = mx
return res7Code line by line
| line | what it means |
|---|---|
| res = [0] * (n - k + 1) | The answer list has one slot per window. |
| for i in range(n - k + 1): | Every possible window start. The last one is n − k. |
| mx = nums[i] | Guess: the first item is the biggest. We'll correct it. |
| for j in range(i, i + k): | Walk over the k items of this window. |
| mx = max(mx, nums[j]) | Keep the bigger of "best so far" and the current item. |
| res[i] = mx | Save the max of the window that starts at i. |
8Dry run
| i | j scans | mx | res after |
|---|---|---|---|
| 0 | 1, 3, −1 | 3 | [3, _, _, _, _, _] |
| 1 | 3, −1, −3 | 3 | [3, 3, _, _, _, _] |
| 2 | −1, −3, 5 | 5 | [3, 3, 5, _, _, _] |
| 3 | −3, 5, 3 | 5 | [3, 3, 5, 5, _, _] |
| 4 | 5, 3, 6 | 6 | [3, 3, 5, 5, 6, _] |
| 5 | 3, 6, 7 | 7 | [3, 3, 5, 5, 6, 7] ✓ |
Every window re-reads k − 1 items that the previous window already read. That's the waste.
9Complexity & remember
- Time: about (n − k) windows × k items each = n·k − k². The teacher's example: n = 10⁵, k = 10⁴ → 10⁹ − 10⁸ ≈ 9·10⁸. The k² part doesn't help much; this is far above 10⁸ → TLE.
- Space O(1) extra, besides the answer list.
→ Small correction: when k = n there is only one window, so the work is just n. The cost (n − k + 1)·k is largest when k is about n/2: then it's about n²/4, which for n = 10⁵ is 2.5·10⁹. So yes, the worst case is O(n²); it just happens around k = n/2, not k = n.
i in range(n − k + 1), j in range(i, i + k), max starts at nums[i]. Correct but O(n·k).Part B · Why a plain slide fails, and the heap idea
1The question
Same question. Now we try to slide the window instead of rescanning it.
2What the constraints tell us
n up to 10⁵. An O(n log n) solution is about 10⁵ × 17 ≈ 1.7·10⁶ steps, fine. O(n) is even better.
3Intuition: try the normal slide with one "max" variable
A normal fixed window keeps one running value and updates it: add the new item, remove the old item. Let's keep one variable mx.
- Window 1
[1, 3, −1]: mx = 3. - Slide: −3 comes in, 1 leaves. The item leaving (1) wasn't the max, so we just compare: max(3, −3) = 3 ✓. Easy.
- Slide: 5 comes in, 3 leaves, and 3 was the max. Here max(…, 5) = 5 happens to save us. But suppose that 5 were −5: the window is
[−1, −3, −5]. We know 3 is gone, but who's the new max? One variable can't tell us. We'd have to scan the whole window again, and then it's the brute force again.
So the problem is: when the max leaves, we need the second max, then maybe the third… We need a structure that keeps the window's values in order.
4The heap idea
A max-heap (priority queue) always gives the biggest item it holds in O(1), and adding or removing an item costs O(log size) because it has to rearrange itself to keep the biggest on top. So the heap does the "first max, second max, third max…" bookkeeping for us.
- Window
[1, 3, −1]: heap has 3, 1, −1 → top 3. - 1 leaves, −3 comes in: heap 3, −1, −3 → top 3.
- 3 leaves, −5 comes in (the bad case): heap −1, −3, −5 → top −1 ✓. The heap gave us the "second max" automatically.
heapq is a min-heap, and it can't delete a specific item quickly. How do we use it?→ (1) Store negative values, so the smallest stored item is the biggest real value. (2) Store the index with each value. We don't delete items the moment they leave the window. Instead, whenever we read the top, we first throw away tops whose index is outside the window (
index ≤ right − k). Items buried deeper don't matter until they reach the top. This is called lazy deletion.→ It's correct, but every insert and delete costs O(log k) in the teacher's version (O(log n) with lazy deletion, since old items can stay in the heap longer). That's O(n log n) overall. The teacher asks: can we avoid paying log for every item? Part C gets O(1) per item.
5Approach steps
- For each
right: push(−nums[right], right). - While the top's index ≤ right − k (outside the window): pop it.
- If
right ≥ k − 1(the window is full): the answer is−heap[0][0].
6Code (Python)
import heapq
class Solution:
def maxSlidingWindow(self, nums, k):
heap = [] # items: (-value, index)
res = []
for right in range(len(nums)):
heapq.heappush(heap, (-nums[right], right))
while heap[0][1] <= right - k: # top has left the window
heapq.heappop(heap)
if right >= k - 1: # window is full
res.append(-heap[0][0]) # biggest value in the window
return res7Code line by line
| line | what it means |
|---|---|
| heapq.heappush(heap, (-nums[right], right)) | Add the new item. The minus sign turns Python's min-heap into a max-heap. |
| while heap[0][1] <= right - k: | The current window is right−k+1 .. right. An index ≤ right − k is outside it. Only the top matters, so we only clean the top. |
| heapq.heappop(heap) | Throw away the stale top. |
| if right >= k - 1: | From index k − 1 on, every step completes a window. |
| res.append(-heap[0][0]) | Undo the minus sign to get the real max. |
The heap never runs empty in that while: the item we just pushed has index right, which is always inside the window, so the loop stops at it at the latest.
8Dry run (the teacher's "−5" variation)
nums = [1, 3, −1, −3, −5], k = 3. Heap shown as real values, biggest first.
| right (added) | heap after push | stale top removed? | answer |
|---|---|---|---|
| 0 (1) | 1 | no | window not full |
| 1 (3) | 3, 1 | no | window not full |
| 2 (−1) | 3, 1, −1 | top 3 is index 1 > −1 → keep | 3 |
| 3 (−3) | 3, 1, −1, −3 | top 3 is index 1 > 0 → keep | 3 |
| 4 (−5) | 3, 1, −1, −3, −5 | top 3 (index 1 ≤ 1) → pop; top 1 (index 0 ≤ 1) → pop; top −1 (index 2) → keep | −1 |
Notice the 1 (index 0) stayed in the heap from step 2 to step 5 even though it left the window early. That's lazy deletion: harmless, because it wasn't on top.
9Complexity & remember
- Time O(n log n): n pushes, at most n pops, each O(log n).
- Space O(n): with lazy deletion, old items can pile up (e.g. an increasing array never pops anything).
Part C · Optimal: the monotonic deque
1The question
Same question, now in O(n): each item should be added once and removed at most once, each in O(1).
2What the constraints tell us
n up to 10⁵ → O(n) is about 2·10⁵ deque operations. Instant.
3Intuition: who can still become a max?
The teacher starts again from the beginning and asks of every stored item: "can this ever be the answer in the future?"
- We store 1. Then 3 arrives. 3 is bigger, and 3 is newer (further right). The window only moves right, so 1 will leave the window before 3 does. Any future window that contains 1 also contains 3, and 3 beats 1. So 1 can never be a max again → delete it.
- Then −1 arrives. It's smaller than 3, but we must keep it. Why? Because 3 is older and will leave first. After 3 leaves, −1 might be the biggest. In the "−5" version, the window
[−1, −3, −5]has max −1. If we hadn't stored −1, we couldn't answer that window.
A bigger item arriving after smaller ones → remove those smaller ones (they can never be the max again).
Following this rule, the stored values are always decreasing from front to back. So the front is the biggest → the answer is read from the front.
4Building the logic, the way the teacher does
The proof: why the smaller items behind can never be the max again
Say index j is in the deque, a new index r arrives (r > j), and nums[j] < nums[r].
- Windows only move to the right. A window that contains
jcovers some range[s, s+k−1]with s ≤ j. - From now on, every window we look at ends at
ror later. If it still containsj, then it starts at or beforej<rand ends at or afterr, so it also containsr. - In that window,
nums[r]>nums[j], sojis not the max. - Once a window no longer contains
j,jcan't be its max anyway.
So j is useless forever → popping it loses nothing. Each pop is permanent and safe.
Why store indexes, not values?
We need to know when the front item has left the window, and only its index tells us where it sits. The values we can always read back with nums[index]. The teacher stresses this: she draws values on the board for clarity, but the deque really holds indexes.
When does the front leave the window?
The window ending at right is right − k + 1 .. right. The front index is out if it's ≤ right − k. The teacher checks it as a distance: at right = 3 the front is index 1 (value 3): 3 − 1 = 2, a window of length 3 → still inside, keep it. At right = 4: 4 − 1 = 3, which would mean a length of 4 → too long, remove it.
Why a deque?
We add at the back, pop small items from the back, and pop expired items from the front. Working at both ends in O(1) is exactly what a deque does. Unlike a heap, it never rearranges anything, so it's O(1) per operation. That's why it beats the heap.
while (not if) to pop smaller items from the back?→ A new big item can beat several stored items. When 5 arrives, the deque holds 3, −1, −3. −3 < 5 pop, −1 < 5 pop, 3 < 5 pop. Three pops for one arrival.
→ The back. The back holds the smallest values, which are the ones the new item can beat. As soon as the back is bigger than (or equal to) the new item, everything in front of it is bigger too (decreasing order), so we stop.
< or <=?→ Both give correct answers. With
< (the teacher's choice), equal values stay side by side and the older one is popped from the front when it expires. With <=, the older copy is popped right away and the newer one takes its place. Same max either way.→ At right = 0 and right = 1 the window isn't full yet (k = 3), so no answer. From
right ≥ k − 1 on, every step finishes a window. At right = 2 the answer goes to slot 0: 2 − 3 = −1, but it should be 0, so add 1 → slot = right − k + 1.→ Not for correctness, as long as both clean-ups happen before reading the front. In her example the expired 3 at right = 4 actually goes out with the back pops (5 > 3), so the front check has nothing to do there. The front check is what saves us in the "−5" version (dry run 2 below), where nothing new is bigger.
5Approach steps
- Make
resof size n − k + 1 and an empty deque of indexes. - For each
right: while the deque isn't empty and the value at its back is smaller thannums[right], pop from the back. - Append
rightat the back. - If the front index ≤ right − k, pop it from the front (it has left the window).
- If
right ≥ k − 1:res[right − k + 1] = nums[front]. - Return
res.
6Code (Python)
from collections import deque
class Solution:
def maxSlidingWindow(self, nums, k):
n = len(nums)
res = [0] * (n - k + 1)
dq = deque() # indexes; their values decrease front -> back
for right in range(n):
# smaller items at the back can never be a max again
while dq and nums[dq[-1]] < nums[right]:
dq.pop()
dq.append(right)
# the front has slid out of the window right-k+1 .. right
while dq[0] <= right - k:
dq.popleft()
# from right = k-1 on, every step completes a window
if right >= k - 1:
res[right - k + 1] = nums[dq[0]]
return res7Code line by line
| line | what it means |
|---|---|
| res = [0] * (n - k + 1) | One slot per window. |
| dq = deque() | Holds indexes. Their values are decreasing from front to back. |
| while dq and nums[dq[-1]] < nums[right]: dq.pop() | The new item beats the items at the back. Those can never be a max again (see the proof), so remove them. dq and stops us from reading an empty deque. |
| dq.append(right) | The new item always goes in: it's the newest, so it could be the max of future windows. |
| while dq[0] <= right - k: dq.popleft() | The oldest candidate has slid out on the left. (Only one item can expire per step, so an if would also do; the teacher writes while, which is safe too.) The deque is never empty here because right itself was just added. |
| if right >= k - 1: | The window is full. |
| res[right - k + 1] = nums[dq[0]] | The front is the biggest value in the window. |
8Dry run 1: [1, 3, −1, −3, 5, 3, 6, 7], k = 3
Deque shown as index:value, front on the left.
| step | right (value) | pop from back (why) | after append | front expired? (≤ right − k) | deque after | answer |
|---|---|---|---|---|---|---|
| 1 | 0 (1) | empty, nothing | [0:1] | 0 ≤ −3? no | [0:1] | not full |
| 2 | 1 (3) | 1 < 3 → pop 0:1 | [1:3] | 1 ≤ −2? no | [1:3] | not full |
| 3 | 2 (−1) | 3 < −1? no | [1:3, 2:−1] | 1 ≤ −1? no | [1:3, 2:−1] | res[0] = 3 |
| 4 | 3 (−3) | −1 < −3? no | [1:3, 2:−1, 3:−3] | 1 ≤ 0? no | [1:3, 2:−1, 3:−3] | res[1] = 3 |
| 5 | 4 (5) | −3, −1, 3 all < 5 → pop all three | [4:5] | 4 ≤ 1? no | [4:5] | res[2] = 5 |
| 6 | 5 (3) | 5 < 3? no | [4:5, 5:3] | 4 ≤ 2? no | [4:5, 5:3] | res[3] = 5 |
| 7 | 6 (6) | 3 < 6 pop, 5 < 6 pop | [6:6] | 6 ≤ 3? no | [6:6] | res[4] = 6 |
| 8 | 7 (7) | 6 < 7 pop | [7:7] | 7 ≤ 4? no | [7:7] | res[5] = 7 |
The deque after every step (values; the yellow box is the front = the window's max):
The window on the array at two key moments:
Final answer [3, 3, 5, 5, 6, 7] ✓.
Dry run 2: the "−5" version, where the front really expires
nums = [1, 3, −1, −3, −5], k = 3. Steps r = 0..3 are the same as above (deque [3, −1, −3], answers 3, 3).
| right (value) | pop from back | after append | front expired? | deque after | answer |
|---|---|---|---|---|---|
| 4 (−5) | −3 < −5? no → nothing | [1:3, 2:−1, 3:−3, 4:−5] | front index 1 ≤ 4 − 3 = 1 → yes, pop 1:3 | [2:−1, 3:−3, 4:−5] | res[2] = −1 |
Answer [3, 3, −1] ✓. If we had thrown −1 away at r = 2 just because it was smaller than 3, we'd have nothing left to answer this window.
9Complexity & remember
- Time O(n): there are
whileloops inside thefor, but every index is appended once and popped at most once (from the back or from the front). Total work ≈ 2n → O(n). The brute force is O(n·k), which becomes O(n²) for large k. - Space O(k): the deque only holds indexes from the current window, so at most k of them (plus the answer list of n − k + 1).
right ≥ k − 1, the answer is nums[dq[0]].Part D · Revision page
| Brute force | Heap | Monotonic deque | |
|---|---|---|---|
| idea | rescan each window | keep all values sorted by size | keep only possible future maxes |
| stores | nothing | (−value, index) | index (values decreasing) |
| removing old items | n/a | lazily, only when on top | from the front when index ≤ right − k |
| max is at | the variable mx | the heap top | the deque front |
| time | O(n·k) | O(n log n) | O(n) |
| space | O(1) | O(n) | O(k) |
| question | answer |
|---|---|
| number of windows | n − k + 1 |
| window ending at right | right − k + 1 .. right |
| front has expired when | dq[0] ≤ right − k |
| first answer at | right = k − 1, slot right − k + 1 |
2. One max variable breaks when the max leaves; a heap works but costs log.
3. A smaller, older item behind a bigger, newer one is dead forever → pop it from the back.
4. A smaller, newer item is kept → it may lead after the big one leaves.
5. Deque of indexes: pop back while smaller, append, pop front if ≤ right − k, answer = front.
✗ comparing the new item with the front instead of the back
✗
if instead of while for the back pops✗ starting the brute-force max at 0 (values can be negative)
✗ inner loop
range(i, k) or range(i, n) instead of range(i, i + k)✗ writing answers before
right ≥ k − 1, or into slot right − k✗ using a list with
pop(0) (O(n)) instead of deque.popleft()s = Solution() print(s.maxSlidingWindow([1, 3, -1, -3, 5, 3, 6, 7], 3)) # [3, 3, 5, 5, 6, 7] print(s.maxSlidingWindow([1, 3, -1, -3, -5], 3)) # [3, 3, -1] print(s.maxSlidingWindow([1], 1)) # [1] print(s.maxSlidingWindow([4, 2, 9, 1], 4)) # [9] (k = n: one window) print(s.maxSlidingWindow([5, 4, 3, 2, 1], 2)) # [5, 4, 3, 2]
Based on this video: Sliding Window Maximum | Monotonic Deque