Monotonic Stack Pattern Fixes O(n²) Stock Indexing
- A monotonic stack maintains sorted order, discarding elements that can never be useful.
- Each element is pushed and popped at most once — total O(n) time for the entire algorithm.
- Decreasing stack: pop when arr[new] > arr[top] → finds next greater element.
- Declarative structure: maintain sorted order in a stack by popping violations on push
- Two variants: decreasing (next greater) and increasing (next smaller)
- Each element pushed and popped at most once → O(n) amortised
- Real‑world: stock span APIs, histogram area, trapped rainwater
- Production trap: wrong comparison sign (<= vs <) silently shifts answers
Quick Debug Cheat Sheet: Monotonic Stack
Output array is empty or all `-1`
`print(f'index {i}: stack = {stack}')` inside the loop`assert len(result) == len(arr)` after the loopDuplicate values break the logic
`trace_condition(arr[stack.top], arr[i])``print(f'pop {j} because {arr[j]} {op} {arr[i]}')`Production Incident
for loop inside a for loop was 'simple and correct'.Production Debug GuideSymptom‑driven checks for the three most common mistakes
arr[stack.top] < arr[i]; using <= treats equal values as not greater and may skip valid answers.< or >) or non‑strict (<= or >=). For Next Greater Element with duplicates, using <= gives the strictly greater — that's usually correct.Most developers hit a wall with array problems that demand knowing the 'next greater' or 'previous smaller' element at every index. The naive approach — a nested loop — works on paper but collapses at scale. When you're processing stock prices, rendering bar charts, or evaluating weather forecasts over millions of data points, O(n²) isn't a trade-off; it's a dealbreaker. The monotonic stack pattern is one of those rare tools that turns a quadratic problem into a linear one without any exotic data structure magic — just a regular stack used with discipline.
The core problem it solves is deceptively simple: for each element in an array, find the nearest element to its left or right that is greater or smaller than it. Brute force checks every pair. The monotonic stack makes one observation that changes everything — if you're scanning left-to-right and you encounter an element that 'dominates' the element on top of the stack, the dominated element has found its answer. You process it, pop it, and never look at it again. That amortized O(1) pop-per-element is where the O(n) total time comes from.
By the end of this article you'll be able to implement both monotonically increasing and decreasing stacks from scratch, identify which variant a problem requires, handle tricky edge cases like duplicate values and circular arrays, and explain the amortized time complexity to an interviewer without flinching. You'll also walk away with a mental checklist that maps problem phrasing directly to the right stack orientation — so you stop guessing.
What is Monotonic Stack? — Plain English
A monotonic stack maintains elements in either strictly increasing or strictly decreasing order. When a new element is pushed, any elements on the stack that violate the monotonic property are popped first. The moment an element is popped because of a new incoming element, that defines a relationship — typically 'the next greater element' or 'the next smaller element'.
Monotonic stacks solve in O(n) problems that naively take O(n^2): next greater element, previous smaller element, largest rectangle in histogram, trapped rainwater.
This section sets the foundation. You'll see the pattern re‑occur in dozens of interview and production problems.
- The list (stack) always stays sorted by height (value).
- When someone leaves (pop), they've found their answer.
- If no one taller arrives, they stay in the list forever (answer = -1).
- The bouncer never re‑adds a person — O(1) per event.
How Monotonic Stack Works — Step by Step
Let's build a monotonic decreasing stack to find the next greater element (NGE) for each index. The stack maintains indices of elements in decreasing order of their values (biggest on top? No — actually, decreasing stack means values decrease from bottom to top). Here's the algorithm:
- Initialize an empty stack.
- For each index i from 0 to n-1:
- - While stack is not empty AND
arr[stack.top] < arr[i]: - pop top index j → record
result[j] = arr[i] - - Push i onto stack.
- Any indices still in stack have no rightward greater elements → their result stays -1.
For an increasing stack (next smaller element), flip the condition to arr[stack.top] > arr[i].
Key insight: the popped element's answer is the current element. This is a one‑pass algorithm.
< when you need <=.Worked Example — Tracing the Algorithm
Find next greater element for [4, 5, 2, 10, 8].
i=0 (4): stack empty. Push 0. stack=[0]. i=1 (5): arr[0]=4 < 5. Pop 0. result[0]=5. Stack empty. Push 1. stack=[1]. i=2 (2): arr[1]=5 >= 2. Push 2. stack=[1,2]. i=3 (10): arr[2]=2 < 10. Pop 2. result[2]=10. arr[1]=5 < 10. Pop 1. result[1]=10. Stack empty. Push 3. stack=[3]. i=4 (8): arr[3]=10 >= 8. Push 4. stack=[3,4]. End: stack has [3,4] remaining. result[3]=-1, result[4]=-1.
Result: [5, 10, 10, -1, -1]
def next_greater_element(arr): n = len(arr) result = [-1] * n stack = [] for i in range(n): while stack and arr[stack[-1]] < arr[i]: j = stack.pop() result[j] = arr[i] stack.append(i) return result print(next_greater_element([4, 5, 2, 10, 8]))
stack=[index of 10, index of 8] at the end.<= instead of <.Implementation — Python and Java
Below are clean implementations in Python and Java. The pattern is identical across languages: a stack of indices, a while loop with the monotonic condition, and a final sweep for unpopped indices.
Important: always store indices in the stack, not values. You need the index to fill the result array. The value is retrieved via arr[stack.top].
def next_greater_element(arr): result = [-1] * len(arr) stack = [] for i in range(len(arr)): while stack and arr[stack[-1]] < arr[i]: idx = stack.pop() result[idx] = arr[i] stack.append(i) return result def next_smaller_element(arr): result = [-1] * len(arr) stack = [] for i in range(len(arr)): while stack and arr[stack[-1]] > arr[i]: idx = stack.pop() result[idx] = arr[i] stack.append(i) return result
Deque<Integer> instead of Stack (deprecated).ArrayDeque is faster and not synchronized.ArrayIndexOutOfBounds when accessing arr[stack.peek()] on empty stack.Deque in Java, list in Python.while loop per element structure.Real‑World Use Cases — Beyond the Basics
The monotonic stack isn't just an interview trick. It appears in production code for:
- Largest Rectangle in Histogram: For each bar, the area is
height * (right_smaller - left_smaller - 1). Monotonic increasing stack finds both boundaries in one pass. - Trapping Rain Water: Maintain stack of decreasing heights; when a taller bar arrives, compute water trapped above the popped bar.
- Stock Span Problem: For each day, find how many consecutive days before today the price was ≤ today's. Monotonic decreasing stack with indices.
Each of these uses the same core pattern with a small twist.
Complexity Analysis — Why O(n) Amortised
Each element is pushed onto the stack exactly once. Each element can be popped at most once. Total number of push/pop operations is therefore ≤ 2n. The inner while loop may run many times for a single outer iteration, but across the entire run, the total pops equal the number of pushes. This gives an amortised O(1) per element. Space is O(n) for the stack in the worst case (e.g., decreasing array for next greater).
Formally: each iteration of the while loop pops an element, and each element is popped at most once → O(n) total while iterations.
| Aspect | Brute Force (Nested Loop) | Monotonic Stack |
|---|---|---|
| Time Complexity | O(n²) | O(n) |
| Space Complexity | O(1) (excluding output) | O(n) stack overhead |
| Implementation | Simple, two loops | Single loop + while, harder to get right |
| Scalability | Fails beyond ~10⁴ elements | Handles 10⁶+ comfortably |
🎯 Key Takeaways
- A monotonic stack maintains sorted order, discarding elements that can never be useful.
- Each element is pushed and popped at most once — total O(n) time for the entire algorithm.
- Decreasing stack: pop when arr[new] > arr[top] → finds next greater element.
- Increasing stack: pop when arr[new] < arr[top] → finds next smaller element.
- Common applications: next greater element, previous smaller element, histogram rectangles, trapped rainwater.
- Always use index stack, not value stack, to correctly fill result array.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat is a monotonic stack and when should you use one?JuniorReveal
- QWalk through the 'next greater element' algorithm with a concrete example.Mid-levelReveal
- QHow does a monotonic stack achieve O(n) when a naive solution is O(n^2)?SeniorReveal
- QWhat is the difference between a monotonically increasing and decreasing stack?JuniorReveal
Frequently Asked Questions
What makes a stack 'monotonic' and why does that help?
A stack is monotonic if its elements are always in sorted order (increasing or decreasing) from bottom to top. This ensures that when an element is popped because a larger/smaller element arrived, the pop answers the 'next greater/smaller' question directly. Without the monotonic invariant, you'd need to scan the entire array for each element — O(n^2).
What is the classic 'largest rectangle in histogram' and how does the monotonic stack help?
For each bar, the largest rectangle with that bar as the shortest bar extends left to the first shorter bar and right to the first shorter bar. A monotonic increasing stack (of indices in increasing height order) finds both left and right boundaries: when a bar is popped because a shorter bar arrived, we know the right boundary is the current index and the left boundary is the new stack top.
Can a monotonic stack process elements from right to left?
Yes. For 'previous greater element', traverse right to left and use the same logic. The stack stores candidates that might be the previous greater element for future (earlier) indices. Right-to-left traversal is also used for next smaller element in some formulations.
What if the array is circular (e.g., next greater element II)?
For circular arrays, simulate the wrap-around by iterating twice (2n) while only first n indices receive results. Use modulo indexing. The stack may hold indices up to 2n-1, but ensure you only assign results for indices < n. This extension still runs in O(n) time and O(n) space.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.