Monotonic Stack
A stack that keeps its elements in sorted order — and pops anything that would break it. Every pop answers a "next greater / next smaller" question, and since each element is pushed and popped at most once, the whole thing is one amortized O(n) pass.
Pop until the invariant holds
A monotonic stack maintains elements in strictly increasing or decreasing order. When a new element violates the order, we pop elements until the invariant is restored — and the moment an element is popped, it has found its answer: the arriving element is its "next greater" (or "next smaller").
Which way does the stack lean?
V1Decreasing stack → next GREATER element
The stack keeps decreasing values (top is smallest). A larger arrival triggers pops — those popped elements just found their "next greater."
V2Increasing stack → next SMALLER element
The stack keeps increasing values (top is largest). A smaller arrival triggers pops — those popped elements just found their "next smaller."
When to reach for it
| Signal in problem | Stack type | Example |
|---|---|---|
| "Next greater element" | Decreasing | Daily Temperatures |
| "Next smaller element" | Increasing | Next Smaller Element |
| "Previous greater element" | Decreasing | Stock Span |
| "Previous smaller element" | Increasing | Histogram boundaries |
| "Largest rectangle in histogram" | Increasing | LC 84 |
| "Remove digits to make smallest" | Increasing | Remove K Digits |
| "Trapping rain water" | Decreasing | LC 42 (stack method) |
Strategy framework
- What relationship? Next / previous, greater / smaller.
- Choose the direction: next greater → decreasing stack; next smaller → increasing stack.
- Store indices, not values. Access values via
arr[stack[-1]]— indices let you compute distances and widths. - The answer is determined at pop time. The current element is the answer for every element it pops.
The templates to memorize
T1Next greater element
def next_greater_element(nums):
"""For each element, find the next element that is greater."""
n = len(nums)
result = [-1] * n
stack = [] # stores INDICES, maintains decreasing values
for i in range(n):
# Pop all smaller elements — current is their "next greater"
while stack and nums[stack[-1]] < nums[i]:
idx = stack.pop()
result[idx] = nums[i]
stack.append(i)
return result
# nums = [2, 1, 2, 4, 3]
# result = [4, 2, 4,-1,-1] (traced step by step in the visualizer above)
T2Next smaller element
Identical shape — only the comparison flips.
def next_smaller_element(nums):
"""For each element, find the next element that is smaller."""
n = len(nums)
result = [-1] * n
stack = [] # stores INDICES, maintains increasing values
for i in range(n):
while stack and nums[stack[-1]] > nums[i]:
idx = stack.pop()
result[idx] = nums[i]
stack.append(i)
return result
T3Daily Temperatures — answer is a distance
Classic next-greater, but the answer stored at pop time is i - idx
— the distance, not the value. This is why the stack holds indices.
def dailyTemperatures(temperatures):
"""Days until a warmer temperature. Classic next greater element."""
n = len(temperatures)
result = [0] * n
stack = [] # decreasing stack of indices
for i in range(n):
while stack and temperatures[stack[-1]] < temperatures[i]:
idx = stack.pop()
result[idx] = i - idx # distance, not value
stack.append(i)
return result
# temps = [73, 74, 75, 71, 69, 72, 76, 73]
# result = [ 1, 1, 4, 2, 1, 1, 0, 0]
#
# i=0 (73): stack=[0]
# i=1 (74): 74>73 → pop 0, result[0]=1-0=1 stack=[1]
# i=2 (75): 75>74 → pop 1, result[1]=2-1=1 stack=[2]
# i=3 (71): 71<75 stack=[2,3]
# i=4 (69): 69<71 stack=[2,3,4]
# i=5 (72): 72>69 → pop 4, result[4]=5-4=1
# 72>71 → pop 3, result[3]=5-3=2 stack=[2,5]
# i=6 (76): 76>72 → pop 5, result[5]=6-5=1
# 76>75 → pop 2, result[2]=6-2=4 stack=[6]
# i=7 (73): 73<76 stack=[6,7]
# Remaining: result[6]=0, result[7]=0
T4Circular array — iterate twice
def nextGreaterElements(nums):
"""Circular array: treat as if array is doubled."""
n = len(nums)
result = [-1] * n
stack = []
# Iterate through array TWICE (simulate circular)
for i in range(2 * n):
idx = i % n
while stack and nums[stack[-1]] < nums[idx]:
result[stack.pop()] = nums[idx]
if i < n: # only push indices from first pass
stack.append(i)
return result
# nums = [1, 2, 1] → conceptually [1, 2, 1, | 1, 2, 1]
# First pass: normal next greater
# Second pass: wrap around to find answers for remaining
# result = [2, -1, 2] (1→2, 2→none since it's the max, 1→2 wrapping)
T5Largest Rectangle in Histogram
Increasing stack. When a shorter bar arrives, every popped bar knows both its boundaries: the arriving bar is its first shorter bar on the right, and the new stack top is its first shorter bar on the left.
def largestRectangleArea(heights):
"""Find the largest rectangle that fits under the histogram."""
stack = [] # increasing stack of indices
max_area = 0
heights.append(0) # sentinel to flush remaining bars
for i in range(len(heights)):
while stack and heights[stack[-1]] > heights[i]:
h = heights[stack.pop()]
# Width: from current i to the new stack top
w = i if not stack else i - stack[-1] - 1
max_area = max(max_area, h * w)
stack.append(i)
heights.pop() # restore input
return max_area
# heights = [2, 1, 5, 6, 2, 3]
#
# i=0(2): push stack=[0]
# i=1(1): pop 0, h=2, w=1, area=2 stack=[1]
# i=2(5): push stack=[1,2]
# i=3(6): push stack=[1,2,3]
# i=4(2): pop 3, h=6, w=4-2-1=1, area=6
# pop 2, h=5, w=4-1-1=2, area=10 ← MAX
# push stack=[1,4]
# i=5(3): push stack=[1,4,5]
# sentinel(0): pop 5, h=3, w=6-4-1=1, area=3
# pop 4, h=2, w=6-1-1=4, area=8
# pop 1, h=1, w=6, area=6
#
# Answer: 10 (bar of height 5, width 2)
T6Remove K Digits — greedy build
An increasing stack builds the smallest number: any digit larger than the arriving one is dead weight, so pop it (while removals remain).
def removeKdigits(num, k):
"""Remove k digits to make the smallest possible number."""
stack = [] # increasing stack (build smallest number)
for digit in num:
while k > 0 and stack and stack[-1] > digit:
stack.pop()
k -= 1
stack.append(digit)
# If k removals remain, remove from the end
stack = stack[:len(stack) - k]
# Remove leading zeros and handle empty result
return "".join(stack).lstrip("0") or "0"
# num = "1432219", k = 3
#
# '1': stack=[1]
# '4': stack=[1,4]
# '3': 3<4, pop 4 (k=2), stack=[1,3]
# '2': 2<3, pop 3 (k=1), stack=[1,2]
# '2': stack=[1,2,2]
# '1': 1<2, pop 2 (k=0), stack=[1,2,1]
# '9': stack=[1,2,1,9]
#
# k=0, done. Result: "1219"
T7Trapping Rain Water — layer by layer
def trap(height):
"""Calculate trapped rainwater using a monotonic stack."""
stack = [] # decreasing stack of indices
water = 0
for i in range(len(height)):
while stack and height[i] > height[stack[-1]]:
bottom = height[stack.pop()]
if not stack:
break
# Water trapped between stack[-1] and i, above bottom
w = i - stack[-1] - 1
h = min(height[stack[-1]], height[i]) - bottom
water += w * h
stack.append(i)
return water
T8Stock Span — previous greater element
Here the answer is computed at push time instead of pop time: after clearing everything ≤ today's price, the stack top is the previous greater element, and the span is the distance to it.
def stock_span(prices):
"""For each day, how many consecutive days price was <= today's price."""
n = len(prices)
span = [0] * n
stack = [] # decreasing stack of indices
for i in range(n):
while stack and prices[stack[-1]] <= prices[i]:
stack.pop()
# Span = distance to previous greater element (or start)
span[i] = i + 1 if not stack else i - stack[-1]
stack.append(i)
return span
# prices = [100, 80, 60, 70, 60, 75, 85]
# span = [ 1, 1, 1, 2, 1, 4, 6]
#
# Day 5 (75): previous greater is day 1 (80), span = 5-1 = 4
# Day 6 (85): previous greater is day 0 (100), span = 6-0 = 6
Everything is O(n)
| Problem | Time | Space | Notes |
|---|---|---|---|
| Next Greater Element | O(n) | O(n) | Each element pushed/popped once |
| Next Smaller Element | O(n) | O(n) | Same as above |
| Daily Temperatures | O(n) | O(n) | Next greater variant |
| Next Greater Element II | O(n) | O(n) | 2n iterations, still O(n) |
| Largest Rectangle (Histogram) | O(n) | O(n) | Finding left/right bounds |
| Remove K Digits | O(n) | O(n) | Single pass |
| Trapping Rain Water (stack) | O(n) | O(n) | Layer-by-layer computation |
| Stock Span | O(n) | O(n) | Previous greater element |
Where stacks go wrong
nums[stack[-1]], but indices give you the positional information
needed for distances and widths — values alone can't.
0 at the end, bars remaining in the stack never get
processed. Either add a sentinel or handle the leftovers after the loop.
width = i if not stack else i - stack[-1] - 1.
The -1 accounts for the boundary bar not being included.
2 * n times using i % n, but only push indices during
the first pass (if i < n) — otherwise you double-push everything.
Practice ladder
In this order — easiest first. Tick each one as you solve it, and re-solve anything that took more than 25 minutes two days later.