DSA DSA In Depth
0%
design monotonic-dequesliding-windowamortized

Monotonic Deques: O(N) Sliding Window Minimum/Maximum

7 min read Module 1.3: Linked Structures, Monotonic Stacks/Queues & Cache-Conscious Lists Updated Oct 23, 2026
📄 Cheat Sheet
Sliding Window Maximum — Monotonic Queue / Deque
NeetCode Sliding Window Maximum — Monotonic Queue / Deque Watch on YouTube ↗
0:00 / --:--

The Pareto Dominance Invariant: Value vs. Expiration Time

Consider computing the maximum of every contiguous window of length $K$ across $A[0 \dots N-1]$. A balanced BST (std::multiset) or Binary Heap takes $O(N \log K)$ time because it retains every element in the window. Why is retaining all $K$ elements wasteful? Suppose index $i < R$ is currently inside the window, and the newly arriving element at index $R$ satisfies $A[i] \le A[R]$.

Compare $i$ and $R$ across two dimensions: (1) Value: $A[R] \ge A[i]$, and (2) Lifetime: $R > i$, so $R$ will remain inside all future sliding windows strictly longer than $i$ will! In optimization theory, $R$ Pareto-dominates $i$: $i$ can never be the maximum of the current window or any future window. Therefore, we can permanently discard $i$ from the back of our candidate list before pushing $R$!

Dual-Ordering Invariant of a Monotonic Decreasing Deque

At the end of step $R$ (for window $[R - K + 1, R]$), the indices $d_0, d_1, \dots, d_m$ stored in the deque from front to back simultaneously satisfy two strict invariants:

  1. Strictly Increasing Indices (Timestamps): $R - K + 1 \le d_0 < d_1 < \dots < d_m = R$.
  2. Strictly Decreasing Values: $A[d_0] > A[d_1] > \dots > A[d_m]$.

Consequently, $d_0 = \text{deque.front()}$ is guaranteed to be a valid in-window index holding the maximum value of $A[R - K + 1 \dots R]$ in $O(1)$ time! Each index $0 \dots N-1$ is pushed to the back once and popped from either the back (by dominance) or the front (by window expiration) at most once, proving $O(1)$ amortized work per step ($\le 2N$ deque operations total).

Sliding Window Extremum & Constrained DP Techniques Compared

Method Total Time (Length $N$) Worst-Case Latency per Step Auxiliary Space Supports Dynamic Window Bounds $[L(R), R]$?
Naive Window Scan $O(N K)$ $O(K)$ $O(1)$ Yes
Balanced BST / Heap $O(N \log K)$ $O(\log K)$ $O(K)$ Yes
Monotonic Deque $O(N)$ $O(1)$ amortized ($O(K)$ worst step) $O(K)$ Yes! Any monotonic $[L(R), R]$
Two-Stack Queue (Min-Stacks) $O(N)$ $O(1)$ amortized $O(K)$ Yes (works for ANY associative monoid!)

Visualizing Back-Dominance Pruning & Front-Expiration

Monotonic Decreasing Deque Architecture (Window [R - K + 1 .. R]) pop_front() if d.front() <= R-K FRONT (d_0) Window MAX (O(1)) MIDDLE (d_1 ..) Strictly Decreasing BACK: pop_back() while A[back] <= A[R] Dual Monotonicity: From Front to Back, Indices Strictly INCREASE (d_0 < d_1 < d_2) while Values DECREASE! Prefix-Sum + Deque Pattern (Shortest Subarray >= K): Pop front while P[R] - P[d.front()] >= K; pop back while P[d.back()] >= P[R]

Monotonic Deque Optimization of DP & Negative-Weight Prefix Sums

flowchart LR A["Step R: Advance right endpoint"] --> B["1. Expire Front:
while !dq.empty() && dq.front() < R - K + 1
dq.pop_front()"] B --> C["2. Prune Dominated Back:
while !dq.empty() && A[dq.back()] <= A[R]
dq.pop_back()"] C --> D["3. Insert & Query:
dq.push_back(R)
window_max = A[dq.front()]"]
Hardware Reality: Flat Array Ring or Two-Index Vector Beats std::deque by $4\times$

When implementing a Monotonic Deque over an array of length $N$, since at most $N$ pushes ever occur across the entire algorithm, you don't even need a circular buffer! Allocating a flat std::vector<int> q(N) with two integer cursors int head = 0, tail = 0; implements pop_front() as ++head, pop_back() as --tail, and push_back(r) as q[tail++] = r. This reduces the entire monotonic deque to a single contiguous L1-cached array and two CPU registers.

Monotonic Stack (Histogram) & Monotonic Queue (Sliding Window Max)
O(N) Single Pass

Every index is pushed once and popped at most once. Inspect how maintaining a strict monotonic invariant resolves Next Greater/Smaller boundaries and sliding window extrema in O(N) time.

Step 1 / 7
Custom Input:
Timeline

Invariant & State

Stack heights strictly non-decreasing: [2]
🔗 Live Pointer & State Transition Graph — Monotonic Stack (Histogram) & Monotonic Queue (Sliding Window Max) Active Pointer Jump/Transition Matched/Optimal
curr 2 [0] 1 [1] 5 [2] 6 [3] 2 [4] 3 [5]
[0]2curr
[1]1
[2]5
[3]6
[4]2
[5]3
Monotonic Increasing Stack: [0(h=2)]
Scan Index i
0 (h = 2)
Stack Depth
1
Popped Rectangles
0
Max Rectangle Area
0 (0)
[Step 1] h[0]=2 maintains increasing order; pushed index 0 onto stack.

Production Implementation: Sliding Window Maximum & Shortest Subarray with Sum $ge K$

#include <algorithm>
#include <cstdint>
#include <vector>

// O(N) Sliding Window Maximum using a register-indexed flat array deque
std::vector<int> sliding_window_maximum(const std::vector<int>& nums, std::size_t k) {
  const std::size_t n = nums.size();
  if (n == 0 || k == 0) return {};

  std::vector<std::size_t> dq(n); // Flat array deque: at most N pushes total!
  std::size_t head = 0, tail = 0;
  std::vector<int> result;
  result.reserve(n - k + 1);

  for (std::size_t r = 0; r < n; ++r) {
    // 1. Evict expired front index outside [r - k + 1 .. r]
    if (head < tail && dq[head] + k <= r) {
      ++head;
    }
    // 2. Evict dominated back indices with value <= nums[r]
    while (head < tail && nums[dq[tail - 1]] <= nums[r]) {
      --tail;
    }
    dq[tail++] = r;

    if (r + 1 >= k) {
      result.push_back(nums[dq[head]]);
    }
  }
  return result;
}

// LeetCode 862: Shortest Subarray with Sum >= K (handles negative numbers in O(N)!)
int shortest_subarray_sum_at_least_k(const std::vector<int>& nums, std::int64_t k) {
  const std::size_t n = nums.size();
  std::vector<std::int64_t> pref(n + 1, 0);
  for (std::size_t i = 0; i < n; ++i) {
    pref[i + 1] = pref[i] + nums[i];
  }

  std::vector<std::size_t> dq(n + 1);
  std::size_t head = 0, tail = 0;
  std::size_t min_len = n + 1;

  for (std::size_t r = 0; r <= n; ++r) {
    // 1. Pop front while valid sum >= k is achieved; once L is paired with current R,
    //    no future R' > R can form a shorter subarray with L, so L is permanently retired!
    while (head < tail && pref[r] - pref[dq[head]] >= k) {
      min_len = std::min(min_len, r - dq[head]);
      ++head;
    }
    // 2. Maintain strictly increasing prefix sums in deque: if pref[back] >= pref[r],
    //    then r is both further right (shorter subarray) AND has a smaller prefix sum!
    while (head < tail && pref[dq[tail - 1]] >= pref[r]) {
      --tail;
    }
    dq[tail++] = r;
  }
  return (min_len <= n) ? static_cast<int>(min_len) : -1;
}
Production Pitfall: Storing Values Instead of Indices in the Monotonic Deque

A frequent bug is pushing values nums[r] into the deque instead of indices r. If you only store values, you cannot tell whether dq.front() has slid outside the left boundary $R - K + 1$ when duplicate values exist in the array! Always store array indices inside the monotonic deque (which gives you both the timestamp idx and the value nums[idx]). Also note the unsigned-safe expiration test: write dq[head] + k <= r instead of dq[head] <= r - k so r - k never underflows when r < k.

Check yourself Question 1 of 4
Concept Check Why Store Indices Instead of Values in a Monotonic Deque recall

In the Sliding Window Maximum algorithm of window size $W$, why does the monotonic deque typically store array indices $i$ (or `(index, timestamp)` pairs) rather than only the raw values `nums[i]`?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

What is the internal memory layout of C++ `std::deque` compared to a flat ring buffer?

Key Takeaways

A Monotonic Deque maintains indices that are strictly increasing in position and strictly monotonic in value from front to back.
Each element is pushed once and popped at most once (either from the back via Pareto dominance or from the front via expiration), guaranteeing O(N) total time.
Combining Prefix Sums with a Monotonic Increasing Deque solves Shortest Subarray with Sum >= K in O(N) time even when the array contains negative numbers.
Always store indices (not raw values) in the deque to accurately expire out-of-window elements, and use a flat pre-allocated array with head/tail cursors for maximum L1 speed.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue