Range query problems—finding properties of ranges or subarrays—often seem to require O(n²) nested loops. But monotonic stacks can optimize many of them to O(n).
The key insight: Instead of asking "what's the property of this range?", ask "for which ranges does this element determine the property?" This reversal of perspective unlocks the O(n) solution.
This comprehensive guide will teach you how monotonic stacks solve range queries, the stock span pattern, trapping rain water, and when to store indices vs values.
TL;DR
Monotonic stack for range queries:
- Stock Span: Find consecutive days with price ≤ today → Decreasing stack
- Trapping Rain Water: Calculate trapped water → Increasing stack or two pointers
- Maximum Width Ramp: Find max distance with constraint → Decreasing stack
Key technique: For each element, find the range where it's relevant (min, max, or boundary)
Time: O(n), Space: O(n)
Pattern 1: Stock Span Problem
Problem Statement
The stock span for a given day is the maximum number of consecutive days (including today) where the stock price was less than or equal to today's price.
Example:
Prices: [100, 80, 60, 70, 60, 75, 85]
Spans: [1, 1, 1, 2, 1, 4, 6]
Explanation:
- Day 0 (100): No previous days, span = 1
- Day 1 (80): 80 < 100, span = 1
- Day 2 (60): 60 < 80, span = 1
- Day 3 (70): 70 > 60, span = 2 (includes day 2)
- Day 4 (60): 60 < 70, span = 1
- Day 5 (75): 75 > 60, 70, 60, span = 4 (includes days 2,3,4)
- Day 6 (85): 85 > all previous, span = 6 (includes days 0-5)Brute Force: O(n²)
def stock_span_brute(prices):
"""
For each day, scan backward to count consecutive days.
Time: O(n²)
Space: O(1)
"""
n = len(prices)
spans = []
for i in range(n):
span = 1 # Include today
j = i - 1
# Count consecutive days with price <= today
while j >= 0 and prices[j] <= prices[i]:
span += 1
j -= 1
spans.append(span)
return spansTime complexity: O(n²) in worst case (strictly increasing prices)
Monotonic Stack Solution: O(n)
Insight: Use a decreasing stack to track potential "span boundaries."
Key observation: If today's price is higher than yesterday's, we can "absorb" yesterday's span.
class StockSpanner:
"""
LeetCode #901: Online Stock Span
Approach: Monotonic decreasing stack
- Stack stores (price, span) pairs
- When new price arrives, pop all smaller prices and accumulate spans
Time: O(1) amortized per call
Space: O(n) worst case
"""
def __init__(self):
self.stack = [] # (price, span)
def next(self, price):
span = 1 # Start with today
# Pop all smaller or equal prices
while self.stack and self.stack[-1][0] <= price:
span += self.stack.pop()[1] # Accumulate their spans
self.stack.append((price, span))
return span
# Usage
spanner = StockSpanner()
print(spanner.next(100)) # 1
print(spanner.next(80)) # 1
print(spanner.next(60)) # 1
print(spanner.next(70)) # 2
print(spanner.next(60)) # 1
print(spanner.next(75)) # 4
print(spanner.next(85)) # 6Why It Works
Visual trace:
Prices: [100, 80, 60, 70, 60, 75, 85]
Day 0, price=100:
stack=[], span=1
stack=[(100,1)]
Day 1, price=80:
80 < 100, don't pop
span=1
stack=[(100,1), (80,1)]
Day 2, price=60:
60 < 80, don't pop
span=1
stack=[(100,1), (80,1), (60,1)]
Day 3, price=70:
70 > 60, pop (60,1), span=1+1=2
70 < 80, stop
stack=[(100,1), (80,1), (70,2)]
Day 4, price=60:
60 < 70, don't pop
span=1
stack=[(100,1), (80,1), (70,2), (60,1)]
Day 5, price=75:
75 > 60, pop (60,1), span=1+1=2
75 > 70, pop (70,2), span=2+2=4
75 < 80, stop
stack=[(100,1), (80,1), (75,4)]
Day 6, price=85:
85 > 75, pop (75,4), span=1+4=5
85 > 80, pop (80,1), span=5+1=6
85 < 100, stop
stack=[(100,1), (85,6)]
Final spans: [1, 1, 1, 2, 1, 4, 6] ✓Key insight: When we pop a price, we "absorb" its span because all those days are also ≤ current price.
Complexity Analysis
Time: O(1) amortized per call
- Each price is pushed once and popped at most once
- Total operations: 2n for n calls = O(n)
- Amortized: O(n) / n = O(1) per call
Space: O(n) worst case (strictly decreasing prices)
Pattern 2: Trapping Rain Water
Problem Statement (LeetCode #42)
Given heights of bars, calculate how much water can be trapped after raining.
Example:
Heights: [0,1,0,2,1,0,1,3,2,1,2,1]
Visual:
█
█ ██ █
█ ██ ███████
0120101321 21
Water trapped: 6 unitsApproach 1: Monotonic Increasing Stack
Insight: Water is trapped in "valleys" between bars. Use stack to find valleys.
def trap_stack(height):
"""
LeetCode #42: Trapping Rain Water
Approach: Monotonic increasing stack
- Stack stores indices of bars
- When we find a taller bar, calculate water trapped
Time: O(n)
Space: O(n)
"""
if not height:
return 0
water = 0
stack = [] # Store indices, maintain increasing heights
for i in range(len(height)):
# When current bar is taller, we found a right boundary
while stack and height[i] > height[stack[-1]]:
bottom = stack.pop() # This is the valley bottom
if not stack:
break # No left boundary
# Calculate trapped water
left_boundary = stack[-1]
right_boundary = i
width = right_boundary - left_boundary - 1
bounded_height = min(height[left_boundary], height[right_boundary]) - height[bottom]
water += width * bounded_height
stack.append(i)
return water
# Example
print(trap_stack([0,1,0,2,1,0,1,3,2,1,2,1]))
# Output: 6Step-by-Step Trace
Heights: [0,1,0,2,1,0,1,3,2,1,2,1]
[0 1 2 3 4 5 6 7 8 9 10 11]
i=0, h=0: stack=[0]
i=1, h=1: 1 > 0, pop 0, no left boundary, stack=[1]
i=2, h=0: 0 < 1, stack=[1,2]
i=3, h=2:
2 > 0, pop 2 (bottom), left=1, right=3
width = 3-1-1 = 1
height = min(1,2) - 0 = 1
water = 1×1 = 1
2 > 1, pop 1, no left boundary
stack=[3]
Total water: 1
i=4, h=1: 1 < 2, stack=[3,4]
i=5, h=0: 0 < 1, stack=[3,4,5]
i=6, h=1:
1 > 0, pop 5, left=4, right=6
width = 6-4-1 = 1
height = min(1,1) - 0 = 1
water = 1×1 = 1
1 = 1, stop
stack=[3,4,6]
Total water: 2
i=7, h=3:
3 > 1, pop 6, left=4, right=7
width = 7-4-1 = 2
height = min(1,3) - 1 = 0
water = 2×0 = 0
3 > 1, pop 4, left=3, right=7
width = 7-3-1 = 3
height = min(2,3) - 1 = 1
water = 3×1 = 3
3 > 2, pop 3, no left boundary
stack=[7]
Total water: 5
i=8, h=2: 2 < 3, stack=[7,8]
i=9, h=1: 1 < 2, stack=[7,8,9]
i=10, h=2:
2 > 1, pop 9, left=8, right=10
width = 10-8-1 = 1
height = min(2,2) - 1 = 1
water = 1×1 = 1
2 = 2, stop
stack=[7,8,10]
Total water: 6
i=11, h=1: 1 < 2, stack=[7,8,10,11]
Final water: 6 ✓Approach 2: Two Pointers (Alternative)
For comparison, here's the two-pointer approach:
def trap_two_pointers(height):
"""
Alternative: Two pointers approach
Time: O(n)
Space: O(1) ← Better space complexity
"""
if not height:
return 0
left, right = 0, len(height) - 1
left_max, right_max = height[left], height[right]
water = 0
while left < right:
if height[left] < height[right]:
if height[left] >= left_max:
left_max = height[left]
else:
water += left_max - height[left]
left += 1
else:
if height[right] >= right_max:
right_max = height[right]
else:
water += right_max - height[right]
right -= 1
return waterComparison:
- Monotonic Stack: O(n) time, O(n) space, more intuitive for some
- Two Pointers: O(n) time, O(1) space, more space-efficient
See detailed guide: Trapping Rain Water Edge Cases
Pattern 3: Maximum Width Ramp
Problem (LeetCode #962)
Find the maximum j - i such that i < j and nums[i] <= nums[j].
Example:
Input: nums = [6,0,8,2,1,5]
Output: 4
Explanation: i=1, j=5, nums[1]=0 <= nums[5]=5, width=5-1=4Solution: Monotonic Decreasing Stack
def maxWidthRamp(nums):
"""
LeetCode #962: Maximum Width Ramp
Approach:
1. Build decreasing stack of indices (potential left boundaries)
2. Scan right to left, find maximum width
Time: O(n)
Space: O(n)
"""
n = len(nums)
stack = []
# Build decreasing stack (potential left boundaries)
for i in range(n):
if not stack or nums[i] < nums[stack[-1]]:
stack.append(i)
max_width = 0
# Scan right to left to find maximum width
for j in range(n - 1, -1, -1):
while stack and nums[j] >= nums[stack[-1]]:
i = stack.pop()
max_width = max(max_width, j - i)
return max_width
# Example
print(maxWidthRamp([6,0,8,2,1,5]))
# Output: 4Why it works:
- Decreasing stack contains all potential left boundaries
- Scanning right to left ensures we find the maximum width for each left boundary
Pattern 4: Remove K Digits
Problem (LeetCode #402)
Remove k digits from a number to make it the smallest possible.
Example:
Input: num = "1432219", k = 3
Output: "1219"
Explanation: Remove 4, 3, 2 to get 1219Solution: Monotonic Increasing Stack
def removeKdigits(num, k):
"""
LeetCode #402: Remove K Digits
Approach: Greedy + monotonic increasing stack
- Maintain increasing digits
- Remove larger digits when we see smaller
Time: O(n)
Space: O(n)
"""
stack = []
for digit in num:
# Remove larger digits (make number smaller)
while stack and k > 0 and stack[-1] > digit:
stack.pop()
k -= 1
stack.append(digit)
# If k > 0, remove from end
if k > 0:
stack = stack[:-k]
# Remove leading zeros and convert to string
result = ''.join(stack).lstrip('0')
return result if result else '0'
# Example
print(removeKdigits("1432219", 3))
# Output: "1219"Why it works:
- Greedy: Always remove larger digits when we see smaller
- Monotonic increasing stack ensures we keep smallest digits in order
When to Store Indices vs Values
Store Indices When:
✅ Need to calculate distances
- Example: Daily Temperatures (days until warmer)
- Need:
j - i
✅ Need to access original array
- Example: Trapping Rain Water (access heights)
- Need:
height[stack[-1]]
✅ Need to track positions
- Example: Maximum Width Ramp
- Need: Position information
Example:
stack = [] # Store indices
for i in range(n):
while stack and nums[i] > nums[stack[-1]]:
idx = stack.pop()
result[idx] = i - idx # Calculate distance
stack.append(i)Store Values When:
✅ Only need comparisons
- Example: Remove K Digits
- Only compare digit values
✅ Don't need distances or positions
- Example: Valid Parentheses
- Only match brackets
✅ Simpler implementation
- When indices aren't needed, values are cleaner
Example:
stack = [] # Store values
for digit in num:
while stack and stack[-1] > digit:
stack.pop()
stack.append(digit)Decision Framework
Do you need distances (j - i)?
├─ YES → Store indices
└─ NO → Continue
Do you need to access original array?
├─ YES → Store indices
└─ NO → Continue
Do you need position information?
├─ YES → Store indices
└─ NO → Store values (simpler)See detailed guide: Indices vs Values
Common Mistakes
Mistake 1: Wrong Stack Order for Stock Span
❌ Wrong:
# Using increasing stack
while stack and stack[-1][0] >= price: # Wrong!
stack.pop()✅ Correct:
# Use decreasing stack
while stack and stack[-1][0] <= price: # Correct
span += stack.pop()[1]Mistake 2: Not Checking for Left Boundary
❌ Wrong:
while stack and height[i] > height[stack[-1]]:
bottom = stack.pop()
# Assume left boundary exists
left = stack[-1] # Can crash if stack is empty!✅ Correct:
while stack and height[i] > height[stack[-1]]:
bottom = stack.pop()
if not stack:
break # No left boundary
left = stack[-1]Mistake 3: Wrong Width Calculation
❌ Wrong:
width = right - left # Off by one!✅ Correct:
width = right - left - 1 # Exclude boundariesComplexity Analysis
Time Complexity: O(n)
All patterns use amortized O(n):
- Each element pushed once
- Each element popped at most once
- Total: 2n = O(n)
Space Complexity: O(n)
Stack can grow to size n in worst case.
Practice Problems
Master range queries with monotonic stack:
- Online Stock Span (#901) ⭐⭐ - Classic stock span
- Trapping Rain Water (#42) ⭐⭐⭐ - Water trapping
- Maximum Width Ramp (#962) - Width optimization
- Remove K Digits (#402) - Greedy + stack
- Largest Rectangle in Histogram (#84) - Area calculation
- 132 Pattern (#456) - Complex pattern matching
Conclusion
Monotonic stacks optimize range query problems by reversing the question: instead of "what's the property of this range?", ask "for which ranges does this element determine the property?"
Key patterns:
- Stock Span: Decreasing stack, accumulate spans
- Trapping Rain Water: Increasing stack, calculate trapped water
- Maximum Width Ramp: Decreasing stack, find maximum distance
- Remove K Digits: Increasing stack, greedy removal
When to store indices vs values:
- Indices: Need distances, positions, or array access
- Values: Only need comparisons
Time complexity: O(n) through amortized analysis
Space complexity: O(n) for stack storage
Master these patterns, and you'll solve complex range query problems with elegance.
Next steps:
Next time you see a range query problem, ask: "Can I use a monotonic stack to optimize this?"
Want to Practice LeetCode Smarter?
LeetCopilot is a free browser extension that enhances your LeetCode practice with AI-powered hints, personalized study notes, and realistic mock interviews — all designed to accelerate your coding interview preparation.
Also compatible with Edge, Brave, and Opera
