Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Monotonic Queue
DSA

Monotonic Queue

Understand how monotonic queues maintain useful candidates for efficient range maximum or minimum queries.

A monotonic queue is a deque with an invariant: elements stay sorted (increasing or decreasing) because anything that violates the order is evicted — it can never be the answer.

Focus on recognizing:

“Window max/min” / “next greater/smaller element” → eviction invariant


The Invariant

Decreasing deque (max queries):  pop back while back < new
Increasing deque (min queries):  pop back while back > new
Front                            = current answer

Eviction is permanent and safe: an evicted element is smaller than a NEWER element that outlives it in the window — it will never be queried again.


Pattern 1: Sliding Window Minimum

Minimum of every 3-wide window over [2,1,4,3,6,5] — bigger values die at the back so the smallest lives at the front. Press to animate.

Sliding Window Minimum

Min of every fixed-size window as it slides across an array.

Maintain an increasing deque of indices. Before adding i, drop indices whose value ≥ nums[i] from the back (they can never beat nums[i] as the min) and drop indices that fell out of the window from the front. The front is always the current window minimum. O(n) total.

QUEUE VISUALIZER
Steps
← DEQUEUE (Front)
2
1
4
3
6
5
ENQUEUE (Rear) →
Press ▶ to animate, or step through manually.
Variables
keys: ← → space F
Pseudocode

                        1
                        for i in 0..n-1:
                      
                        2
                          if deque[0] <= i - k: poll left   // out of window
                      
                        3
                          while deque[-1] value > nums[i]: pop right
                      
                        4
                          push i
                      
                        5
                          if i >= k-1: output nums[deque[0]]
                      
public int[] minSlidingWindow(int[] nums, int k) {
    Deque<Integer> dq = new ArrayDeque<>();   // increasing
    int[] result = new int[nums.length - k + 1];

    for (int i = 0; i < nums.length; i++) {
        if (!dq.isEmpty() && dq.peekFirst() <= i - k)
            dq.pollFirst();                   // expired

        while (!dq.isEmpty() && nums[dq.peekLast()] > nums[i])
            dq.pollLast();                    // break invariant

        dq.offerLast(i);

        if (i >= k - 1)
            result[i - k + 1] = nums[dq.peekFirst()];
    }

    return result;
}
from collections import deque

def min_sliding_window(nums, k):
    dq = deque()                  # increasing
    result = []

    for i, num in enumerate(nums):
        if dq and dq[0] <= i - k:
            dq.popleft()          # expired

        while dq and nums[dq[-1]] > num:
            dq.pop()              # break invariant

        dq.append(i)

        if i >= k - 1:
            result.append(nums[dq[0]])

    return result
vector<int> minSlidingWindow(vector<int>& nums, int k) {
    deque<int> dq;                        // increasing
    vector<int> result;

    for (int i = 0; i < (int)nums.size(); i++) {
        if (!dq.empty() && dq.front() <= i - k)
            dq.pop_front();               // expired

        while (!dq.empty() && nums[dq.back()] > nums[i])
            dq.pop_back();                // break invariant

        dq.push_back(i);

        if (i >= k - 1)
            result.push_back(nums[dq.front()]);
    }

    return result;
}
function minSlidingWindow(nums, k) {
  const dq = []; // increasing
  const result = [];

  for (let i = 0; i < nums.length; i++) {
    if (dq.length && dq[0] <= i - k) dq.shift(); // expired

    while (dq.length && nums[dq.at(-1)] > nums[i])
      dq.pop(); // break invariant

    dq.push(i);

    if (i >= k - 1) result.push(nums[dq[0]]);
  }

  return result;
}

Invariant direction picks the answer end: increasing front = min, decreasing front = max.


Pattern 2: Next Greater Element (Same Skeleton, No Window)

Drop the window, keep the deque — identical resolution logic.

Next Greater Element

For each element, find the next greater element to its right. A decreasing stack holds elements waiting for their answer. When a bigger element arrives, it resolves all smaller ones.

Array: [4,5,2,25]. Decreasing stack of values. Each new element pops everything smaller — those popped elements get the new element as their answer.

ARRAY VISUALIZER
Steps
4
0
5
1
2
2
25
3
Press ▶ to animate, or step through manually.
Variables
keys: ← → space F
Pseudocode

                        1
                        stack = []                  // values waiting for their NGE
                      
                        2
                        for x in nums:
                      
                        3
                          while stack and x > stack.top:
                      
                        4
                            answer[pop()] = x
                      
                        5
                          push(x)
                      
                        6
                        leftovers → answer = -1
                      

Pop-from-back answers “next greater” as elements are evicted:

public int[] nextGreater(int[] nums) {
    int[] result = new int[nums.length];
    Arrays.fill(result, -1);
    Deque<Integer> stack = new ArrayDeque<>(); // indices

    for (int i = 0; i < nums.length; i++) {
        while (!stack.isEmpty()
                && nums[stack.peek()] < nums[i]) {
            result[stack.pop()] = nums[i];   // found its answer
        }

        stack.push(i);
    }

    return result;
}
def next_greater(nums):
    result = [-1] * len(nums)
    stack = []                    # indices, decreasing

    for i, num in enumerate(nums):
        while stack and nums[stack[-1]] < num:
            result[stack.pop()] = num   # found its answer

        stack.append(i)

    return result
vector<int> nextGreater(vector<int>& nums) {
    vector<int> result(nums.size(), -1);
    stack<int> st;                        // indices

    for (int i = 0; i < (int)nums.size(); i++) {
        while (!st.empty() && nums[st.top()] < nums[i]) {
            result[st.top()] = nums[i];   // found its answer
            st.pop();
        }
        st.push(i);
    }

    return result;
}
function nextGreater(nums) {
  const result = new Array(nums.length).fill(-1);
  const stack = []; // indices, decreasing

  for (let i = 0; i < nums.length; i++) {
    while (stack.length && nums[stack.at(-1)] < nums[i]) {
      result[stack.pop()] = nums[i]; // found its answer
    }
    stack.push(i);
  }

  return result;
}

Same monotonic idea minus expiry — no window means no pollFirst check.


What Are We Actually Storing?

Indices — never values:

  • Expiry needs positions (index <= i − k).
  • Values are one lookup away (nums[index]).
  • Equal values: keep or evict consistently — either works if you stay uniform.

Common Mistakes

Mixing up eviction directions.

Max wants < eviction (decreasing), min wants > (increasing). Write the invariant as a comment first.


Expiring after reading the answer.

Remove the expired front BEFORE using peekFirst() as output.


Forgetting leftover candidates.

In next-greater problems, unpopped indices stay -1 — pre-fill the array.


Complexity

MetricValue
TimeO(n) — each index enters/leaves once
SpaceO(k) window / O(n) next-greater

My Private Notes

Notes are auto-saved locally to this device.