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 · 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
sizealways exactly kgrows and shrinks
how it movesadd nums[right]; the item at right − k falls outexpand right; while the window is invalid, shrink left
typical questionssum / average / max of every window of size klongest 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)

patternuse it when…
Two pointersyou only care about the two ends, not what's between them
Sliding windowyou need everything between a start and an end (whole windows)
Prefix summany range-sum queries
Kadanebest 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).

actionPython (collections.deque)Java (the teacher's code)cost
add at the backdq.append(x)addLastO(1)
remove from the backdq.pop()pollLastO(1)
remove from the frontdq.popleft()pollFirstO(1)
look at the back / frontdq[-1] / dq[0]peekLast / peekFirstO(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.

index01234567
nums13-1-35367k = 3
windowitemsmax
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

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

Doubt 1: how far can the start 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.
Doubt 2: where does the inner 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.
Doubt 3: what should 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).
Doubt 4: the answer for start 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

  1. Make res of size n − k + 1.
  2. For each start i from 0 to n − k: mx = nums[i].
  3. For j from i to i + k − 1: mx = max(mx, nums[j]).
  4. res[i] = mx. Return res.

6Code (Python)

Brute force, O((n − k + 1) · k)
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 res

7Code line by line

linewhat 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] = mxSave the max of the window that starts at i.

8Dry run

ij scansmxres after
01, 3, −13[3, _, _, _, _, _]
13, −1, −33[3, 3, _, _, _, _]
2−1, −3, 55[3, 3, 5, _, _, _]
3−3, 5, 35[3, 3, 5, 5, _, _]
45, 3, 66[3, 3, 5, 5, 6, _]
53, 6, 77[3, 3, 5, 5, 6, 7] ✓
i = 113-1-353673 and −1 were already read for i = 0
L R

Every window re-reads k − 1 items that the previous window already read. That's the waste.

9Complexity & remember

Doubt 5: at the end she says the brute force becomes n² "when k equals n". Is that right?
→ 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.
Remember the brute forcei 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.

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.

Doubt 1: Python's 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.
Doubt 2: so what's wrong with the heap?
→ 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

  1. For each right: push (−nums[right], right).
  2. While the top's index ≤ right − k (outside the window): pop it.
  3. If right ≥ k − 1 (the window is full): the answer is −heap[0][0].

6Code (Python)

Heap with lazy deletion, O(n log n)
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 res

7Code line by line

linewhat 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 pushstale top removed?answer
0 (1)1nowindow not full
1 (3)3, 1nowindow not full
2 (−1)3, 1, −1top 3 is index 1 > −1 → keep3
3 (−3)3, 1, −1, −3top 3 is index 1 > 0 → keep3
4 (−5)3, 1, −1, −3, −5top 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

Remember the heap ideaOne variable can't survive "the max left the window". A heap keeps all candidates in order, but pays log per item. Good, not best.

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?"

The ruleA smaller item arriving after a bigger one → keep it (it may become the max once the bigger one leaves).
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].

  1. Windows only move to the right. A window that contains j covers some range [s, s+k−1] with s ≤ j.
  2. From now on, every window we look at ends at r or later. If it still contains j, then it starts at or before j < r and ends at or after r, so it also contains r.
  3. In that window, nums[r] > nums[j], so j is not the max.
  4. Once a window no longer contains j, j can'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.

Doubt 1: why 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.
Doubt 2: do I compare the new item with the front or the back?
→ 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.
Doubt 3: what about equal values: pop with < 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.
Doubt 4: when do we start writing answers, and into which slot?
→ 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.
Doubt 5: the teacher pops smaller items first, then appends, then checks the front. Does the order matter?
→ 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

  1. Make res of size n − k + 1 and an empty deque of indexes.
  2. For each right: while the deque isn't empty and the value at its back is smaller than nums[right], pop from the back.
  3. Append right at the back.
  4. If the front index ≤ right − k, pop it from the front (it has left the window).
  5. If right ≥ k − 1: res[right − k + 1] = nums[front].
  6. Return res.

6Code (Python)

Monotonic deque, O(n): the final answer
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 res

7Code line by line

linewhat 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.

stepright (value)pop from back (why)after appendfront expired? (≤ right − k)deque afteranswer
10 (1)empty, nothing[0:1]0 ≤ −3? no[0:1]not full
21 (3)1 < 3 → pop 0:1[1:3]1 ≤ −2? no[1:3]not full
32 (−1)3 < −1? no[1:3, 2:−1]1 ≤ −1? no[1:3, 2:−1]res[0] = 3
43 (−3)−1 < −3? no[1:3, 2:−1, 3:−3]1 ≤ 0? no[1:3, 2:−1, 3:−3]res[1] = 3
54 (5)−3, −1, 3 all < 5 → pop all three[4:5]4 ≤ 1? no[4:5]res[2] = 5
65 (3)5 < 3? no[4:5, 5:3]4 ≤ 2? no[4:5, 5:3]res[3] = 5
76 (6)3 < 6 pop, 5 < 6 pop[6:6]6 ≤ 3? no[6:6]res[4] = 6
87 (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):

r=01first item, nothing to compare
r=131 popped: older and smaller than 3, useless forever
r=23-1−1 kept: it may lead once 3 leaves → answer 3
r=33-1-3still decreasing, 3 still in window 1..3 → answer 3
r=455 beats −3, −1, 3 → all popped → answer 5
r=5533 kept behind 5 → answer 5
r=666 beats 3 and 5 → answer 6
r=777 beats 6 → answer 7

The window on the array at two key moments:

r=313-1-35367window 1..3, deque [3, −1, −3]
L R
r=413-1-35367window 2..4, deque [5]
L R

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 backafter appendfront expired?deque afteranswer
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
before3-1-3-53 is at index 1, outside window 2..4
after-1-3-5this is why we kept −1 at r = 2

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

Remember the monotonic dequeStore indexes, values decreasing front → back. New item: pop smaller from the back (they're dead forever), append. Pop expired from the front (index ≤ right − k). Once right ≥ k − 1, the answer is nums[dq[0]].

Part D · Revision page

Brute forceHeapMonotonic deque
idearescan each windowkeep all values sorted by sizekeep only possible future maxes
storesnothing(−value, index)index (values decreasing)
removing old itemsn/alazily, only when on topfrom the front when index ≤ right − k
max is atthe variable mxthe heap topthe deque front
timeO(n·k)O(n log n)O(n)
spaceO(1)O(n)O(k)
questionanswer
number of windowsn − k + 1
window ending at rightright − k + 1 .. right
front has expired whendq[0] ≤ right − k
first answer atright = k − 1, slot right − k + 1
If you remember only 5 lines 1. Answer length = n − k + 1; the first k − 1 items give no answer alone.
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.
Mistakes to avoid ✗ storing values instead of indexes (can't tell when the front expired)
✗ 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()
test it yourself (paste under the final solution)
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