Monotonic Deque
A sliding window can maintain a sum in per step, because adding and removing are both reversible arithmetic. A maximum is not: when the current maximum slides out of the window, you have no idea what the new maximum is, and rescanning the window costs — back to overall.
The fix is a monotonic deque: a double-ended queue holding indices whose values are kept sorted. The window’s maximum is then always at the front, readable in , and the whole scan stays because each index is pushed once and popped once.
What you’ll learn
Section titled “What you’ll learn”- Why a heap is the wrong tool here, and what a deque gives you instead.
- The “pop from the back while smaller, pop from the front when expired” template.
- The amortised argument that makes an inner
whileloop still . - Three real LeetCode problems solved in the browser: 239, 1696, 1438.
The cue
Section titled “The cue”The template
Section titled “The template”from collections import deque
def sliding_window_maximum(nums, k):
dq = deque() # holds INDICES; their values are strictly decreasing
out = []
for i, x in enumerate(nums):
# 1. maintain monotonicity: anything smaller than x is now useless
while dq and nums[dq[-1]] <= x:
dq.pop()
dq.append(i)
# 2. evict the front if it has slid out of the window
if dq[0] <= i - k:
dq.popleft()
# 3. the front is the window's maximum
if i >= k - 1:
out.append(nums[dq[0]])
return out
print(sliding_window_maximum([1, 3, -1, -3, 5, 3, 6, 7], 3)) # [3, 3, 5, 5, 6, 7]Three moves, always in this order: pop the back to restore monotonicity, pop the front to drop expired indices, read the front for the answer.
Why it is O(n) despite the inner while
Section titled “Why it is O(n) despite the inner while”Each index is appended exactly once and popped at most once, so the
total number of deque operations across the entire run is at most .
The inner while can run many times on one iteration, but only by
consuming pushes that already happened — it can never run more times in
total than there were pushes.
| Time | Space | |
|---|---|---|
| Rescan the window each step | ||
| Max-heap with lazy deletion | ||
| Monotonic deque |
Visual intuition
Section titled “Visual intuition”A deque, not a stack — because entries leave from both ends. Watch the two eviction rules fire independently:
Back eviction: a new value that is bigger AND later makes everything smaller behind it permanently useless. Front eviction: the current maximum has slid out of the window. A stack can do the first and has no answer for the second, which is precisely why this needs a deque.
Complexity
Section titled “Complexity”| Approach | Time | Space |
|---|---|---|
Recompute max() per window | ||
| Max-heap with lazy deletion | ||
| Monotonic deque |
Each index is appended exactly once and removed exactly once across the whole run, so the total deque work is at most — the same amortised argument as the monotonic stack. The heap solution is worth mentioning as the more obvious answer before giving the deque, since it shows you know why is possible.
The variant map
Section titled “The variant map”| Variant | What changes | Canonical problem |
|---|---|---|
| Window maximum | Pop back while value <= x; decreasing deque | 239 Sliding Window Maximum |
| Window minimum | Pop back while value >= x; increasing deque | 1438 (paired with a max deque) |
| Both at once | Run two deques side by side over the same window | 1438 · 2762 |
| Windowed DP | The deque holds indices into dp, not nums | 1696 Jump Game VI |
| Prefix sums + deque | Deque over the prefix-sum array to allow negatives | 862 Shortest Subarray with Sum at Least K |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”LC 239 — Sliding Window Maximum · Hard
Section titled “LC 239 — Sliding Window Maximum · Hard”Problem. Given an array nums and a window size k, return an array
of the maximum value in each window as it slides from left to right.
Constraints. 1 <= len(nums) <= 10^5, 1 <= k <= len(nums),
-10^4 <= nums[i] <= 10^4.
Examples. nums = [1,3,-1,-3,5,3,6,7], k = 3 gives
[3,3,5,5,6,7] · nums = [1], k = 1 gives [1]
Editorial — approach, complexity, follow-ups
The invariant: the deque holds indices in increasing order, whose values
are strictly decreasing. Consequently dq[0] is always the index of
the window’s maximum.
When a new value x arrives, every index at the back whose value is
<= x is permanently useless: x is at least as large and stays in the
window longer. Pop them. Then evict the front if it has aged out.
Time amortised. Space .
Why <= and not <: with <, equal values pile up in the deque. That is
still correct (the front remains a maximum) but wastes space. Using
<= keeps the deque minimal. Either passes; be ready to justify yours.
Follow-ups you should expect: “Window minimum?” — flip the comparison
to >=. “Why not a heap?” — see the note above: a heap cannot evict an
arbitrary expired element, so it degrades to . “Can you do
space?” — no; you provably need to remember up to k candidates.
LC 1696 — Jump Game VI · Medium
Section titled “LC 1696 — Jump Game VI · Medium”Problem. You start at index 0 and must reach the last index. From
index i you may jump to any index in [i+1, i+k]. Your score is the sum
of nums[j] over every index you land on (including 0 and the last).
Return the maximum score.
Constraints. 1 <= len(nums), k <= 10^5,
-10^4 <= nums[i] <= 10^4.
Examples. nums = [1,-1,-2,4,-7,3], k = 2 gives 7 ·
nums = [10,-5,-2,4,0,3], k = 3 gives 17 ·
nums = [1,-5,-20,4,-1,3,-6,-3], k = 2 gives 0
Editorial — approach, complexity, follow-ups
The DP is immediate: dp[i] = nums[i] + max(dp[i-k] … dp[i-1]), with
dp[0] = nums[0]. Evaluated naively that is , which TLEs at
.
The inner max is a maximum over a sliding window of dp — so it is
LC 239 embedded in a DP recurrence. The deque holds indices into dp,
front = best reachable predecessor.
Time . Space for dp, for the deque.
Order matters here in a way it does not in 239: evict the expired front
first, then read dp[dq[0]], then push i. Pushing before reading
would let i be its own predecessor.
This “max over the last k states” shape is worth memorising — it turns
up in constrained-jump, stock-cooldown, and bounded-knapsack problems, and
a deque removes the k factor every time.
Follow-ups you should expect: “Greedy instead?” — no, greedy fails
because a locally poor landing can be the only route to a large later
value. “Can you drop the dp array?” — you only need the last k
entries, so a ring buffer of size k gives space.
LC 1438 — Longest Continuous Subarray With Absolute Diff ≤ Limit · Medium
Section titled “LC 1438 — Longest Continuous Subarray With Absolute Diff ≤ Limit · Medium”Problem. Return the length of the longest contiguous subarray in which
the difference between the maximum and minimum element is at most limit.
Constraints. 1 <= len(nums) <= 10^5, 1 <= nums[i] <= 10^9,
0 <= limit <= 10^9.
Examples. nums = [8,2,4,7], limit = 4 gives 2 ·
nums = [10,1,2,4,7,2], limit = 5 gives 4 ·
nums = [4,2,2,2,4,4,2,2], limit = 0 gives 3
Editorial — approach, complexity, follow-ups
This is a longest-valid sliding window whose validity test is
window_max - window_min <= limit. Neither extremum is maintainable by
arithmetic, so keep one deque for each.
Time . Space worst case for the two deques.
A sortedcontainers.SortedList also solves this in and is
a fine thing to mention — but it is not in the standard library, so it is
unavailable in many interview environments.
Follow-ups you should expect: “Return the subarray?” — track
best_left when best updates. “What if limit were on the sum
instead?” — back to an ordinary prefix-sum window. “Count all valid
subarrays rather than the longest?” — add right - left + 1 per step
(that is LC 2762).
LeetCode problem set
Section titled “LeetCode problem set”Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 1438Longest Continuous Subarray With Absolute Diff Less Than or Equal to LimitmediumTwo deques over one window; guard the front eviction
- 1696Jump Game VImediumWindowed DP -- the deque indexes `dp`, not `nums`
- 2762Continuous Subarraysmedium1438 with `limit = 2`, counting subarrays instead of the longest
- 239Sliding Window MaximumhardThe base template
- 862Shortest Subarray with Sum at Least KhardDeque over **prefix sums** -- the answer to "what if values are negative?" for LC 209
- 1499Max Value of EquationhardRewrite the objective so one term is a windowed maximum, then apply the template
Dry run
Section titled “Dry run”nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3. Deque holds indices; values
in brackets.
i | v | front evicted | back evicted | deque after | output |
|---|---|---|---|---|---|
| 0 | 1 | — | — | [0(1)] | — |
| 1 | 3 | — | 0(1) | [1(3)] | — |
| 2 | −1 | — | — | [1(3), 2(-1)] | 3 |
| 3 | −3 | — | — | [1, 2, 3(-3)] | 3 |
| 4 | 5 | 1(3) out of range | 3(-3), 2(-1) | [4(5)] | 5 |
| 5 | 3 | — | — | [4(5), 5(3)] | 5 |
| 6 | 6 | — | 5(3), 4(5) | [6(6)] | 6 |
| 7 | 7 | — | 6(6) | [7(7)] | 7 |
Output [3, 3, 5, 5, 6, 7].
Step 4 is the one to study: both rules fire in the same iteration. Index 1 leaves from the front because the window has moved past it, and indices 2 and 3 leave from the back because 5 outclasses them. Those are independent mechanisms — conflating them into one condition is the usual way this goes wrong.
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “Why a deque and not a heap?” | Whether you know why, not just what | A heap cannot evict an arbitrary expired element; a deque discards dominated candidates permanently, giving vs |
| “Prove it’s ” | Amortised analysis | Each index is pushed once and popped at most once, so deque operations total regardless of the inner loop |
| “Why indices and not values?” | Attention to the invariant | Expiry needs positions (dq[0] <= i - k), and duplicate values are otherwise indistinguishable |
| “Window minimum too?” | Whether the template generalises | Flip the comparison; run both deques together if you need max and min |
“What if k changes per query?” | Modelling | The deque is tied to one window width — for arbitrary ranges use a sparse table or segment tree instead |
Edge-case checklist
Section titled “Edge-case checklist”k = 1— every window is one element; output equals the input.k = len(nums)— one window; the answer is a single global max.- All equal values — exercises the
<=vs<choice; both work but only<=keeps the deque small. - Strictly increasing input — the deque never exceeds length 1.
- Strictly decreasing input — the deque grows to
k; this is the space worst case. - Negative values only — do not initialise a running best to
0. limit = 0(1438) — valid; only runs of identical values qualify.- Front eviction guard (1438) — pop the front only when its index
equals
left.
Self-check
Section titled “Self-check”-
Why a deque rather than a stack?
The back eviction is the monotonic-stack rule. The front eviction is new and is what a stack has no mechanism for: the current maximum eventually leaves the window through the far end.
pch.quizShowAnswer
B — Entries must be evicted from BOTH ends — from the back when outclassed, from the front when they slide out of the window — The back eviction is the monotonic-stack rule. The front eviction is new and is what a stack has no mechanism for: the current maximum eventually leaves the window through the far end.
-
Why does the deque store indices rather than values?
Same reason as the monotonic stack. Read values through indices; a bare value cannot tell you whether it has slid out of range.
pch.quizShowAnswer
B — The front-eviction test dq[0] <= i - k is a position comparison, which a stored value cannot answer — Same reason as the monotonic stack. Read values through indices; a bare value cannot tell you whether it has slid out of range.
-
When a new value arrives, why is it safe to discard smaller values behind it?
Both properties are needed. Larger alone would not justify it; later alone would not either. Together they mean the discarded values are dominated for the rest of the run.
pch.quizShowAnswer
B — Because the new value is both larger AND later, so it survives every window they would have survived — they can never be a maximum again — Both properties are needed. Larger alone would not justify it; later alone would not either. Together they mean the discarded values are dominated for the rest of the run.
-
What is the time complexity, and how do you justify it given the inner while loop?
The same amortised argument as the monotonic stack: bound the total, not the per-iteration work. One iteration may evict five entries and the next none.
pch.quizShowAnswer
B — O(n) — each index is appended once and removed once, so total deque operations are at most 2n — The same amortised argument as the monotonic stack: bound the total, not the per-iteration work. One iteration may evict five entries and the next none.
Recall card
Section titled “Recall card”- Cue — the maximum or minimum of a sliding window, or any window aggregate that cannot be undone by subtraction (max and min cannot; a sum can, which is why a plain sliding window handles sums).
- Invariant — indices in the deque, values decreasing front to back. The front is always the window’s answer.
- Two evictions — back: while the back’s value ≤ the incoming value. Front:
if
dq[0] <= i - k. - Complexity — amortised, space.
- Remember — store indices; the two eviction rules are independent;
start recording output at
i >= k - 1. - Alternative to name — a max-heap with lazy deletion gives ; mention it, then give the deque.
- A sliding window handles sums by arithmetic, but an extremum needs a monotonic deque: indices in, values sorted, extremum at the front.
- Three moves in order: pop the back while dominated, pop the front when expired, read the front.
- Store indices, never values — expiry is positional.
- amortised: each index is pushed once and popped once, so the
inner
whilecannot exceed the total number of pushes. - The pattern generalises past arrays: over
dpit removes thekfrom windowed-DP transitions (1696), and over prefix sums it rescues sum-windows from negative numbers (862).
Next: Prefix Sums and Difference Arrays — the other answer to “negative numbers broke my window”, and the range-query workhorse.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading