Skip to main content

Monotonic Stack & Queue

Monotonic Stack and Monotonic Queue are powerful patterns used to solve problems involving next/previous greater or smaller elements and sliding window extremes in O(n) time β€” problems that would otherwise take O(nΒ²) with brute force.


🧠 What is a Monotonic Stack?​

A Monotonic Stack is a regular stack with one extra rule:

Elements inside the stack are always maintained in a strictly increasing or strictly decreasing order.

When a new element violates this order, we pop elements until the order is restored, then push the new element.

INCREASING Monotonic Stack (bottom β†’ top goes small β†’ large):

Push 3: [3]
Push 5: [3, 5] 5 > 3 βœ… just push
Push 2: pop 5, pop 3 β†’ [2] 2 < 5 ❌ pop until valid, push 2
Push 4: [2, 4] 4 > 2 βœ… just push
Push 1: pop 4, pop 2 β†’ [1] 1 < 4 ❌ pop until valid, push 1

πŸ—‚οΈ Types of Monotonic Stack​

TypeOrderUsed For
Increasingbottom→top: small→largeNext Smaller Element, Previous Smaller Element, Largest Rectangle
Decreasingbottom→top: large→smallNext Greater Element, Previous Greater Element, Stock Span

πŸ” Classic Problem 1: Next Greater Element​

Problem: For each element in the array, find the next element to its right that is greater than it. If none exists, return -1.

Visual Dry-Run​

Array: [2, 1, 5, 3, 6, 4]
0 1 2 3 4 5

We use a DECREASING stack (stores indices).
When we find a greater element, it's the answer for everything popped.

i=0: stack=[] β†’ push 0 stack=[0] (val=2)
i=1: arr[1]=1 < arr[0]=2 β†’ push 1 stack=[0,1] (val=2,1)
i=2: arr[2]=5 > arr[1]=1 β†’ pop 1: NGE[1]=5
5 > arr[0]=2 β†’ pop 0: NGE[0]=5
stack empty β†’ push 2 stack=[2] (val=5)
i=3: arr[3]=3 < arr[2]=5 β†’ push 3 stack=[2,3] (val=5,3)
i=4: arr[4]=6 > arr[3]=3 β†’ pop 3: NGE[3]=6
6 > arr[2]=5 β†’ pop 2: NGE[2]=6
stack empty β†’ push 4 stack=[4] (val=6)
i=5: arr[5]=4 < arr[4]=6 β†’ push 5 stack=[4,5] (val=6,4)

End: remaining in stack β†’ NGE = -1
NGE[4]=-1, NGE[5]=-1

βœ… Result: [5, 5, 6, 6, -1, -1]

Code Template​

def next_greater_element(arr):
n = len(arr)
result = [-1] * n
stack = [] # stores indices

for i in range(n):
# Pop elements smaller than current
while stack and arr[stack[-1]] < arr[i]:
idx = stack.pop()
result[idx] = arr[i] # current element is the NGE
stack.append(i)

# Remaining in stack have no NGE β†’ result stays -1
return result

# Example
arr = [2, 1, 5, 3, 6, 4]
print(next_greater_element(arr)) # Output: [5, 5, 6, 6, -1, -1]

Time Complexity: O(n) β€” each element is pushed and popped at most once Space Complexity: O(n)


πŸ” Classic Problem 2: Previous Smaller Element​

Problem: For each element, find the nearest element to its left that is smaller.

Visual Dry-Run​

Array: [4, 5, 2, 10, 8]
0 1 2 3 4

We use an INCREASING stack (stores indices).

i=0: stack=[] β†’ push 0 stack=[0] (val=4)
PSE[0] = -1 (stack was empty before push)

i=1: arr[1]=5 > arr[0]=4 β†’ no pop β†’ push 1 stack=[0,1]
PSE[1] = arr[0] = 4

i=2: arr[2]=2 < arr[1]=5 β†’ pop 1
2 < arr[0]=4 β†’ pop 0
stack empty β†’ push 2 stack=[2] (val=2)
PSE[2] = -1 (stack was empty)

i=3: arr[3]=10 > arr[2]=2 β†’ no pop β†’ push 3 stack=[2,3]
PSE[3] = arr[2] = 2

i=4: arr[4]=8 < arr[3]=10 β†’ pop 3
8 > arr[2]=2 β†’ stop β†’ push 4 stack=[2,4]
PSE[4] = arr[2] = 2

βœ… Result: [-1, 4, -1, 2, 2]

Code Template​

def previous_smaller_element(arr):
n = len(arr)
result = [-1] * n
stack = [] # stores indices

for i in range(n):
# Pop elements greater than or equal to current
while stack and arr[stack[-1]] >= arr[i]:
stack.pop()

# Top of stack is the previous smaller element
if stack:
result[i] = arr[stack[-1]]

stack.append(i)

return result

# Example
arr = [4, 5, 2, 10, 8]
print(previous_smaller_element(arr)) # Output: [-1, 4, -1, 2, 2]

Time Complexity: O(n) Β |Β  Space Complexity: O(n)


πŸͺŸ Monotonic Queue (Deque)​

A Monotonic Deque is used when you need to efficiently track the maximum or minimum in a sliding window.

Core Idea​

Maintain a deque of indices.
- Add new element to BACK (remove smaller elements first for max-deque)
- Remove elements from FRONT if they're outside the window
- Front always holds the index of the MAXIMUM in current window

Window size K=3, Array = [1, 3, -1, -3, 5, 3, 6, 7]

i=0: deque=[0] window=[1] max=1
i=1: 3>1 β†’ pop 0, push 1 deque=[1] window=[1,3] max=3
i=2: -1<3 β†’ push 2 deque=[1,2] window=[1,3,-1] max=3
i=3: -3<-1 β†’ push 3 front=1 in window window=[3,-1,-3] max=3
i=4: 5>all β†’ pop 3,2,1 deque=[4] window=[-1,-3,5] max=5
i=5: 3<5 β†’ push 5 deque=[4,5] window=[-3,5,3] max=5
i=6: 6>3,5 β†’ pop 5,4 deque=[6] window=[5,3,6] max=6
i=7: 7>6 β†’ pop 6 deque=[7] window=[3,6,7] max=7

βœ… Result: [3, 3, 5, 5, 6, 7]

Code Template: Sliding Window Maximum​

from collections import deque

def sliding_window_maximum(arr, k):
n = len(arr)
result = []
dq = deque() # stores indices, front = index of max

for i in range(n):
# Remove indices outside the window from front
while dq and dq[0] < i - k + 1:
dq.popleft()

# Remove indices of smaller elements from back
while dq and arr[dq[-1]] < arr[i]:
dq.pop()

dq.append(i)

# Window is fully formed
if i >= k - 1:
result.append(arr[dq[0]])

return result

# Example
arr = [1, 3, -1, -3, 5, 3, 6, 7]
print(sliding_window_maximum(arr, 3)) # Output: [3, 3, 5, 5, 6, 7]

Time Complexity: O(n) β€” each element enters and exits deque at most once Space Complexity: O(k)


⚑ Brute Force vs Monotonic Stack​

Problem: Next Greater Element for [2, 1, 5, 3, 6, 4]

❌ BRUTE FORCE β€” O(nΒ²)
For each element, scan all elements to its right:
index 0 (val=2): scan 1,5,3,6,4 β†’ find 5 (5 comparisons)
index 1 (val=1): scan 5,3,6,4 β†’ find 5 (4 comparisons)
index 2 (val=5): scan 3,6,4 β†’ find 6 (3 comparisons)
index 3 (val=3): scan 6,4 β†’ find 6 (2 comparisons)
index 4 (val=6): scan 4 β†’ none (1 comparison)
Total: 15 comparisons

βœ… MONOTONIC STACK β€” O(n)
Each element is pushed once and popped once β†’ max 12 operations

πŸ“Š Complexity Summary​

ProblemTimeSpace
Next Greater ElementO(n)O(n)
Previous Smaller ElementO(n)O(n)
Sliding Window Maximum (Deque)O(n)O(k)
Stock Span ProblemO(n)O(n)
Largest Rectangle in HistogramO(n)O(n)

❌ Common Mistakes​

  1. Using values instead of indices β€” Always store indices in the stack/deque so you can calculate spans and access original values.
  2. Wrong pop condition β€” For Next Greater, pop when arr[stack.top()] < current. Flipping this gives Next Smaller.
  3. Forgetting remaining stack elements β€” Elements left in stack after the loop have no NGE β†’ answer is -1.
  4. Not removing out-of-window indices β€” In monotonic deque, always check dq.front() < i - k + 1.
  5. Using Stack for sliding window β€” Sliding window max needs a Deque (not a stack) because you remove from both ends.

πŸ‹οΈ Practice Problems​

#ProblemPatternDifficulty
1Next Greater Element IMonotonic Stack🟒 Easy
2Next Greater Element II (circular)Monotonic Stack🟑 Medium
3Daily TemperaturesMonotonic Stack🟑 Medium
4Previous Smaller ElementMonotonic Stack🟑 Medium
5Stock Span ProblemMonotonic Stack🟑 Medium
6Largest Rectangle in HistogramMonotonic StackπŸ”΄ Hard
7Sliding Window MaximumMonotonic DequeπŸ”΄ Hard
8Shortest Subarray with Sum β‰₯ KMonotonic DequeπŸ”΄ Hard

πŸ”— References​