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
- 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
whilewhileloop still . - Three real LeetCode problems solved in the browser: 239, 1696, 1438.
The cue
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]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
Each index is appendappended exactly once and poppopped at most once, so the
total number of deque operations across the entire run is at most .
The inner whilewhile 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 |
The variant map
| Variant | What changes | Canonical problem |
|---|---|---|
| Window maximum | Pop back while value <= xvalue <= x; decreasing deque | 239 Sliding Window Maximum |
| Window minimum | Pop back while value >= xvalue >= 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 dpdp, not numsnums | 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
LC 239 — Sliding Window Maximum · Hard
Problem. Given an array numsnums and a window size kk, return an array
of the maximum value in each window as it slides from left to right.
Constraints. 1 <= len(nums) <= 10^51 <= len(nums) <= 10^5, 1 <= k <= len(nums)1 <= k <= len(nums),
-10^4 <= nums[i] <= 10^4-10^4 <= nums[i] <= 10^4.
Examples. nums = [1,3,-1,-3,5,3,6,7], k = 3nums = [1,3,-1,-3,5,3,6,7], k = 3 gives
[3,3,5,5,6,7][3,3,5,5,6,7] · nums = [1], k = 1nums = [1], k = 1 gives [1][1]
Editorial — approach, complexity, follow-ups
The invariant: the deque holds indices in increasing order, whose values
are strictly decreasing. Consequently dq[0]dq[0] is always the index of
the window’s maximum.
When a new value xx arrives, every index at the back whose value is
<= x<= x is permanently useless: xx 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 kk candidates.
LC 1696 — Jump Game VI · Medium
Problem. You start at index 00 and must reach the last index. From
index ii you may jump to any index in [i+1, i+k][i+1, i+k]. Your score is the sum
of nums[j]nums[j] over every index you land on (including 00 and the last).
Return the maximum score.
Constraints. 1 <= len(nums), k <= 10^51 <= len(nums), k <= 10^5,
-10^4 <= nums[i] <= 10^4-10^4 <= nums[i] <= 10^4.
Examples. nums = [1,-1,-2,4,-7,3], k = 2nums = [1,-1,-2,4,-7,3], k = 2 gives 77 ·
nums = [10,-5,-2,4,0,3], k = 3nums = [10,-5,-2,4,0,3], k = 3 gives 1717 ·
nums = [1,-5,-20,4,-1,3,-6,-3], k = 2nums = [1,-5,-20,4,-1,3,-6,-3], k = 2 gives 00
Editorial — approach, complexity, follow-ups
The DP is immediate: dp[i] = nums[i] + max(dp[i-k] … dp[i-1])dp[i] = nums[i] + max(dp[i-k] … dp[i-1]), with
dp[0] = nums[0]dp[0] = nums[0]. Evaluated naively that is , which TLEs at
.
The inner maxmax is a maximum over a sliding window of dpdp — so it is
LC 239 embedded in a DP recurrence. The deque holds indices into dpdp,
front = best reachable predecessor.
Time . Space for dpdp, for the deque.
Order matters here in a way it does not in 239: evict the expired front
first, then read dp[dq[0]]dp[dq[0]], then push ii. Pushing before reading
would let ii be its own predecessor.
This “max over the last kk states” shape is worth memorising — it turns
up in constrained-jump, stock-cooldown, and bounded-knapsack problems, and
a deque removes the kk 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 dpdp array?” — you only need the last kk
entries, so a ring buffer of size kk gives space.
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 limitlimit.
Constraints. 1 <= len(nums) <= 10^51 <= len(nums) <= 10^5, 1 <= nums[i] <= 10^91 <= nums[i] <= 10^9,
0 <= limit <= 10^90 <= limit <= 10^9.
Examples. nums = [8,2,4,7], limit = 4nums = [8,2,4,7], limit = 4 gives 22 ·
nums = [10,1,2,4,7,2], limit = 5nums = [10,1,2,4,7,2], limit = 5 gives 44 ·
nums = [4,2,2,2,4,4,2,2], limit = 0nums = [4,2,2,2,4,4,2,2], limit = 0 gives 33
Editorial — approach, complexity, follow-ups
This is a longest-valid sliding window whose validity test is
window_max - window_min <= limitwindow_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.SortedListsortedcontainers.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_leftbest_left when bestbest updates. “What if limitlimit were on the sum
instead?” — back to an ordinary prefix-sum window. “Count all valid
subarrays rather than the longest?” — add right - left + 1right - left + 1 per step
(that is LC 2762).
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 1696 | Jump Game VI | Medium | Windowed DP — the deque indexes dpdp, not numsnums |
| 1438 | Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit | Medium | Two deques over one window; guard the front eviction |
| 2762 | Continuous Subarrays | Medium | 1438 with limit = 2limit = 2, counting subarrays instead of the longest |
| 239 | Sliding Window Maximum | Hard | The base template |
| 862 | Shortest Subarray with Sum at Least K | Hard | Deque over prefix sums — the answer to “what if values are negative?” for LC 209 |
| 1499 | Max Value of Equation | Hard | Rewrite the objective so one term is a windowed maximum, then apply the template |
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 - kdq[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 kk 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
k = 1k = 1— every window is one element; output equals the input.k = len(nums)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
kk; this is the space worst case. - Negative values only — do not initialise a running best to
00. limit = 0limit = 0(1438) — valid; only runs of identical values qualify.- Front eviction guard (1438) — pop the front only when its index
equals
leftleft.
Recap
- 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
whilewhilecannot exceed the total number of pushes. - The pattern generalises past arrays: over
dpdpit removes thekkfrom 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.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
