A monotonic deque is a plain Deque — the exact same
push/pop primitive at both ends — used with the identical invariant Monotonic Stack applies to one end of a stack,
applied instead from both ends of a deque. Before every push at the back, pop off anything
already there that would break a chosen order (values kept decreasing, or increasing, front to back);
after every push, also drop anything that's fallen off the front because a sliding window moved past
it. That gets a question that looks like it needs to recompute a fresh answer for every window —
what's the maximum (or minimum) value in this window of size k, for every window
position as it slides across the array — down to one O(n) left-to-right pass, instead of
an O(n·k) full rescan per window. The front of the deque is always the current window's
answer, kept correct incrementally rather than recomputed from scratch each time the window moves.
Each column is index:value. Step through the scan: the deque (front → back) holds
candidate indices in decreasing (or increasing) value order, and its front is always the current
window's answer once the window has filled. A push at the back first evicts anything it beats; every
step also checks whether the deque's front index has aged out of the current window.
function slidingWindowExtreme(arr, k, useMax = true) {
const n = arr.length;
const result = new Array(n - k + 1);
const deque = []; // indices only, values stay in decreasing (or increasing) order front to back
for (let i = 0; i < n; i++) {
// 1. evict from the back: anything the new value beats can never win again
while (
deque.length &&
(useMax ? arr[deque[deque.length - 1]] <= arr[i] : arr[deque[deque.length - 1]] >= arr[i])
) {
deque.pop();
}
deque.push(i);
// 2. evict from the front: the current front may have aged out of the window
if (deque[0] <= i - k) {
deque.shift();
}
// 3. once the window has filled, its answer is always the front
if (i >= k - 1) {
result[i - k + 1] = arr[deque[0]];
}
}
return result;
}
The deque holds indices, not values, for the same reason Monotonic Stack's does: the value is one array lookup away, but only the index lets step 2 tell whether an entry has aged out of the window at all — two equal values at different positions are indistinguishable by value alone.
Time: O(n) amortized total, by the same argument as Monotonic
Stack's: every index is pushed onto the back exactly once, and can be popped at most once total
across the entire scan — once from the back (beaten by a later value) or once from the front (aged
out), never both, since a back-pop removes it before it could ever reach the front. That bounds the
whole run's push/pop/evict count well under a small constant multiple of n, regardless
of k. Space: O(k) — the deque never holds more than the
current window's worth of candidate indices, tighter than Monotonic Stack's O(n) worst
case, since aging-out here bounds it independently of value order.
The naive alternative — recompute the max of each window from scratch — does exactly
(n − k + 1) × (k − 1) comparisons no matter how the values are arranged, so unlike
Monotonic Stack (where data order decides who wins), here it's window size that
decides: a small k means few comparisons per window, cheap enough that the naive scan
can beat the deque's own bookkeeping overhead. Measured directly, same 8-element array used above,
across three window sizes, then a longer array across a wider spread to show the gap actually widen:
| array | n | k | deque ops | naive comparisons |
|---|---|---|---|---|
| demo array | 8 | 2 | 15 | 7 |
| demo array | 8 | 3 | 15 | 12 |
| demo array | 8 | 4 | 15 | 15 |
| longer array | 30 | 5 | 57 | 104 |
| longer array | 30 | 15 | 56 | 224 |
At k=2 the naive scan genuinely wins (7 vs. 15) — one comparison per window is hard to
beat with any bookkeeping at all. By k=4 the two are already tied, and past that the
deque's near-flat operation count (bounded by a small multiple of n, barely moving from
57 to 56 as k triples on the longer array) pulls further and further ahead of the naive
count's linear growth in k. The honest takeaway: this structure earns its keep on large
windows, not small ones.
Skipping the front-eviction check leaves a stale index in the deque, silently. The
back-eviction loop (step 1) and the front-aging check (step 2) guard against two different failure
modes, and it's easy to write only the first — a value comparison feels like "the algorithm," while
the index-age check feels like bookkeeping. On [0, -4, -1, 0, 4] with k=2,
the correct window maximums are [0, -1, 0, 4]; dropping only the front-eviction check
produces [0, 0, 0, 4] — the second window, [-4, -1], silently reports the
stale 0 left over from the first window instead of the real answer, -1,
because index 0 was never removed from the front once the window moved past it. Checked across 20,000
random trials (varying n, k, and array values): this omission produces a wrong answer 45.2% of the
time — not a rare edge case, close to a coin flip.
Emitting a result before the window has actually filled is a real off-by-one, not just a
harmless extra entry. The guard if (i >= k - 1) exists because the first
k - 1 positions never see a complete window — dropping it doesn't just add a few
low-confidence answers at the start, it produces an output array of the wrong length, shifted against
the true one. On the demo's own default array with k=3, the correct 6-entry result is
[3, 3, 5, 5, 6, 7]; emitting on every iteration instead produces the 8-entry
[1, 3, 3, 3, 5, 5, 6, 7] — two extra entries at the front (answers for windows that were
never actually 3 elements wide), and every later real answer sitting at the wrong index because of
it.
A plain JS array's shift() isn't really O(1), even though this page's
reference implementation uses it for clarity. Array.prototype.shift re-indexes
every remaining element, an O(n) operation — the same cost the site's own Deque entry exists specifically to avoid, via a circular
buffer that wraps an index instead of physically moving memory. A production implementation of this
technique should be backed by a real O(1)-front-and-back deque, not a raw array leaning on
shift()/push()/pop() — this page keeps the plain-array version
because the indices it evicts are always adjacent at one end, which makes the step-through easy to
follow, not because it's the version to actually ship.
k as it slides across an
array, in one pass.k readings" over a live stream — server load, stock ticks, sensor spikes — without
rescanning the last k readings on every single new one.This site's guide, Choosing a Linear Data Structure, places this entry alongside Monotonic Stack — neither competes for the sequence's storage the way the other seven Linear entries compete with each other.