HARD Lab · Sliding Window · 13

239. Sliding Window Maximum

You're given an array and a window of size k sliding left to right. Return the maximum inside each window. The naive scan is O(n·k); the elegant answer is a monotonic deque that runs in O(n). This module breaks down why.

HardMonotonic DequeSliding WindowTypeScript

PROBLEM What we're solving

Given nums = [1,3,-1,-3,5,3,6,7] and k = 3, the window starts over the first 3 elements and slides one step at a time. At each position we want the max:

  • [1 3 -1] → max 3
  • [3 -1 -3] → max 3
  • [-1 -3 5] → max 5
  • … → result [3,3,5,5,6,7]

There are n - k + 1windows. Recomputing the max from scratch each time costs O(k) per window — that's the trap.

KEY IDEA The monotonic deque

Keep a double-ended queue of indices whose corresponding values are strictly decreasing from front to back. Two invariants do all the work:

  • Front = answer. The index at the front always points to the maximum of the current window. Read it in O(1).
  • The back is a staging area.Before inserting a new index, we pop every back element whose value is ≤ the new value — they're permanently dominated.
The crucial insight → if nums[j] ≤ nums[i] and j < i, then j can never be the maximum of any window that also contains i. A newer, bigger element makes the older smaller one useless. So we discard it forever.

That single observation is what collapses O(n·k) into O(n): every index enters the deque once and leaves at most once.

RECIPE The four moves per step

For each index ias the window's right edge advances:

  • 1 · Evict stale front. If the front index has slid out of the window (deque[0] ≤ i - k), drop it.
  • 2 · Drain the back. While the back's value ≤ nums[i], pop it. Maintains the decreasing order.
  • 3 · Push i onto the back.
  • 4 · Record. Once i ≥ k-1 (first full window reached), nums[deque[0]]is this window's max.
Why indices, not values?We store indices so step 1 can check whether the front has expired. With raw values you'd lose track of where each candidate lives.

COST Complexity & alternatives

Brute force
O(n·k)
Re-scan every window. Times out on large k.
Monotonic deque
O(n)
Each index pushed & popped once. O(k) extra space.

What about a max-heap?

A heap of (value, index) also works and is a great thing to mention in an interview — but lazy deletion of expired entries pushes it to O(n log n) time and O(n) space. The deque dominates it. Knowing both, and being able to say why the deque wins, is the senior-level answer.

Pattern transfer → the monotonic deque/stack shows up in Largest Rectangle in Histogram, Trapping Rain Water, Daily Temperatures, and Sum of Subarray Minimums. Master it once, reuse it everywhere.

RUN IT Step through the algorithm

step 0 / 30
STARTInitialize an empty deque (holds indices) and an empty result.
1
0
3
1
-1
2
-3
3
5
4
3
5
6
6
7
7
Deque (front = max)
empty
Result
empty
current iin dequewindow maxbeing popped
slowfast

TYPESCRIPT The solution, annotated

maxSlidingWindow.ts
function maxSlidingWindow(nums: number[], k: number): number[] {
  const result: number[] = [];
  const deque: number[] = []; // stores INDICES; values decreasing front→back

  for (let i = 0; i < nums.length; i++) {
    // 1 · evict the front if it has slid out of the window
    if (deque.length && deque[0] <= i - k) {
      deque.shift();
    }
    // 2 · drain the back of all values ≤ the incoming value
    while (deque.length && nums[deque[deque.length - 1]] <= nums[i]) {
      deque.pop();
    }
    // 3 · push the current index
    deque.push(i);
    // 4 · record the max once the first window is complete
    if (i >= k - 1) {
      result.push(nums[deque[0]]);
    }
  }
  return result;
}

Reading it block by block

Lines 2–3 — two structures. result collects answers; deque holds indices (a plain array used as a deque via shift/pop/push). The values those indices point to stay in decreasing order.
Lines 7–9 — expire the front. The window covering index i is [i-k+1, i]. If the front index is ≤ i-kit's outside that window, so we shift() it off. Only one element can expire per step, so an if suffices (no loop needed).
Lines 11–13 — maintain monotonicity. Any back element whose value is ≤ nums[i]is dominated and gets popped. This is the heart of the trick — it's what guarantees the deque stays decreasing and the front stays the true max.
Line 15 — admit the newcomer. After draining, push i. It's now the smallest value at the back (everything ≤ it is gone), preserving the invariant.
Lines 17–19 — harvest. We only have a complete window once i ≥ k-1. From then on, every step emits exactly one answer: nums[deque[0]].
Complexity → Amortized analysis → the inner while looks like it could be O(k), but across the whole run each index is pushed once and popped at most once. Total work is bounded by 2n operations ⇒ O(n).

INTERVIEWFollow-ups they'll ask

  • "What if you needed the window minimum?" Flip the comparison to >= — keep an increasing deque. Same structure.
  • "Can you do it with a heap, and why wouldn't you?" Yes: a max-heap of (value,index) with lazy eviction → O(n log n). The deque is strictly better; naming the tradeoff is the point.
  • "Edge cases?" k === 1 (result equals input), k === nums.length (single max), empty input. The code handles all three without special-casing.
  • "Why an if on line 7 but a whileon line 11?" At most one index expires per advance (front), but many can be dominated at once (back). A favorite gotcha.

MNEMONIC The one-liner

Kick out the weak from the back, retire the old from the front.

TRIGGERS When you see ___ → reach for ___

"max/min of every sliding window"monotonic deque of indices
need O(1) window extremumfront of the deque = answer
newer element ≥ older onepop the dominated back
front index slid out of windowshift it off (deque[0] ≤ i-k)

SKELETON The reusable shape

skeleton.ts
const result: number[] = [];
const deque: number[] = []; // indices, values decreasing
for (let i = 0; i < nums.length; i++) {
  if (deque.length && deque[0] <= i - k) deque.shift();      // retire old front
  while (deque.length && nums[deque.at(-1)!] <= nums[i])     // kick out weak back
    deque.pop();
  deque.push(i);
  if (i >= k - 1) result.push(nums[deque[0]]);               // front = max
}
return result;

FLASHCARDS Tap to flip

What does the deque store, and in what order?
Indices, kept so their values are strictly decreasing front→back. The front index points to the current window's max.
tap to flip
1 / 6
Answer to reveal explanations. No penalty for retries.Score: 0 / 6
QUESTION 01
What is the time complexity of the optimal (monotonic-deque) solution?
QUESTION 02
Why do we pop a back element when nums[back] ≤ nums[i]?
QUESTION 03
What does the deque actually store?
QUESTION 04
On line 7 we use an if to evict the front, but line 11 uses a while to drain the back. Why the difference?
QUESTION 05
You mention a max-heap alternative in the interview. What's its complexity and why prefer the deque?
QUESTION 06
For nums=[7,2,4], k=2, what is the result?