Sliding Window Maximum with a Monotonic Deque

October 10, 2026 · 4 min read

Given an array and a window size k, report the largest value in every window as it slides from left to right:

nums = [1, 3, -1, -3, 5, 3, 6, 7],  k = 3

[1  3  -1] -3  5  3  6  7     → 3
 1 [3  -1  -3] 5  3  6  7     → 3
 1  3 [-1  -3  5] 3  6  7     → 5
 1  3  -1 [-3  5  3] 6  7     → 5
 1  3  -1  -3 [5  3  6] 7     → 6
 1  3  -1  -3  5 [3  6  7]    → 7

Recomputing each window's maximum costs O(k), so O(n·k) in total — 10¹⁰ operations for a million elements and a window of ten thousand. A running maximum doesn't work either: when the maximum leaves the window, you don't know what the next largest is.

The insight: some elements can never win

Look at the window [3, -1, -3] and then the 5 that arrives next. Once 5 is in the window, −1 and −3 can never be a window's maximum again. They're smaller than 5, and they'll leave the window before 5 does. They can be thrown away.

What's left is always a sequence that decreases from oldest to newest. Each element in it is "the largest of everything that came after the one before it". Keep that sequence in a double-ended queue:

  • Back: when a new value arrives, pop every smaller-or-equal value off the back — they're dominated — then push the new one.
  • Front: if the front index has slid out of the window, drop it.
  • Answer: the front is the window's maximum.

The deque stores indices, not values, so you can tell when the front has expired.

1
0
3
1
-1
2
-3
3
5
4
3
5
6
6
7
7
deque
front →i0=1
output
— window not full yet

The deque holds indices, and their values are always decreasing from front to back. The front is the current window's maximum. Push index 0 (value 1).

0 / 7

The code

JavaScript has no built-in deque, and Array.prototype.shift is O(n). A head pointer into an ordinary array gives O(1) operations at both ends:

function maxSlidingWindow(nums, k) {
  const deque = []   // indices; their values decrease from front to back
  let head = 0       // deque[head] is the front
  const out = []

  for (let i = 0; i < nums.length; i++) {
    // Front: drop the index that just left the window.
    if (head < deque.length && deque[head] <= i - k) head++

    // Back: drop values the new one dominates.
    while (deque.length > head && nums[deque[deque.length - 1]] <= nums[i]) {
      deque.pop()
    }
    deque.push(i)

    if (i >= k - 1) out.push(nums[deque[head]])
  }
  return out
}

maxSlidingWindow([1, 3, -1, -3, 5, 3, 6, 7], 3)   // [3, 3, 5, 5, 6, 7]

Only one index can expire per step, because the window moves by one — hence if, not while, at the front. (In this version the array keeps growing behind head. For a long-running stream, compact it from time to time or use a ring buffer.)

Why it's O(n)

There's a while loop inside a for loop, which looks quadratic. Count operations per element instead: each index is pushed exactly once and popped — from the back or past the front — at most once. That's at most 2n operations in total, so O(n) time. The deque holds at most k live indices, so O(k) extra space.

This "amortised" argument is the same one that makes the monotonic stack linear. A monotonic deque is a monotonic stack that can also lose elements from the bottom, because the window has a left edge.

Variations

  • Minimum instead of maximum: keep the deque increasing — flip <= to >=.
  • Longest subarray where max − min ≤ limit: run two deques (one for max, one for min) inside a two-pointer window that shrinks from the left whenever the difference is too big.
  • Shortest subarray with sum ≥ K (with negative numbers): a monotonic deque over prefix sums.
  • DP with a window: dp[i] = nums[i] + max(dp[i-k..i-1]) (the "jump game VI" family) turns O(n·k) into O(n) by keeping that max in a deque.

Outside interviews

The same structure computes a rolling maximum over a time window — the peak latency over the last five minutes on a dashboard, or the highest price in the last N trades. Each new sample is pushed, old ones expire from the front, and the current peak is always at the front without rescanning the window.