Skip to content

Monotonic Deque

A sliding window can maintain a sum in O(1)O(1) 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 O(k)O(k) — back to O(nk)O(n \cdot k) 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 O(1)O(1), and the whole scan stays O(n)O(n) 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 whilewhile loop still O(n)O(n).
  • Three real LeetCode problems solved in the browser: 239, 1696, 1438.

The cue

The template

monotonic_deque_template.py
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]
monotonic_deque_template.py
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 2n2n. 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.

TimeSpace
Rescan the window each stepO(nk)O(n \cdot k)O(1)O(1)
Max-heap with lazy deletionO(nlogn)O(n \log n)O(n)O(n)
Monotonic dequeO(n)O(n)O(k)O(k)

The variant map

VariantWhat changesCanonical problem
Window maximumPop back while value <= xvalue <= x; decreasing deque239 Sliding Window Maximum
Window minimumPop back while value >= xvalue >= x; increasing deque1438 (paired with a max deque)
Both at onceRun two deques side by side over the same window1438 · 2762
Windowed DPThe deque holds indices into dpdp, not numsnums1696 Jump Game VI
Prefix sums + dequeDeque over the prefix-sum array to allow negatives862 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 O(n)O(n) amortised. Space O(k)O(k).

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 O(nlogn)O(n \log n). “Can you do O(1)O(1) 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 O(nk)O(n \cdot k), which TLEs at n=k=105n = k = 10^5.

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 O(n)O(n). Space O(n)O(n) for dpdp, O(k)O(k) 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 O(k)O(k) 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 O(n)O(n). Space O(n)O(n) worst case for the two deques.

A sortedcontainers.SortedListsortedcontainers.SortedList also solves this in O(nlogn)O(n \log n) 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

#ProblemDifficultyThe twist
1696Jump Game VIMediumWindowed DP — the deque indexes dpdp, not numsnums
1438Longest Continuous Subarray With Absolute Diff Less Than or Equal to LimitMediumTwo deques over one window; guard the front eviction
2762Continuous SubarraysMedium1438 with limit = 2limit = 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

Interview follow-ups

They askWhat they’re checkingThe answer
“Why a deque and not a heap?”Whether you know why, not just whatA heap cannot evict an arbitrary expired element; a deque discards dominated candidates permanently, giving O(n)O(n) vs O(nlogn)O(n \log n)
“Prove it’s O(n)O(n)Amortised analysisEach index is pushed once and popped at most once, so deque operations total 2n\le 2n regardless of the inner loop
“Why indices and not values?”Attention to the invariantExpiry needs positions (dq[0] <= i - kdq[0] <= i - k), and duplicate values are otherwise indistinguishable
“Window minimum too?”Whether the template generalisesFlip the comparison; run both deques together if you need max and min
“What if kk changes per query?”ModellingThe 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.
  • O(n)O(n) amortised: each index is pushed once and popped once, so the inner whilewhile cannot exceed the total number of pushes.
  • The pattern generalises past arrays: over dpdp it removes the kk from 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 O(1)O(1) range-query workhorse.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did