~/searching/monotonic-stack
Monotonic stack
Find each element's nearest bigger (or smaller) neighbour in one pass, with a stack that stays sorted.
Scan once, keeping a stack of indices still waiting for an answer. A new value pops every smaller one it beats and becomes their answer.
Each element needs its next or previous greater or smaller element: warmer days, spans, histogram rectangles.
O(n)
O(n)
You’ll recognise it when
- Every element needs its nearest bigger or smaller neighbour, to the left or to the right.
- The question sounds like “how many days until a warmer day?” or “how far back does this price stay the highest?”
- You need, for each bar, how far it can stretch left and right before hitting a shorter bar (histogram rectangles, “sum of subarray minimums”).
- The obvious answer is a nested loop that scans forward from each element, and
nis 10⁵.
It’s often confused with the sliding window maximum, which uses the same trick on a deque because old elements also expire from the front.
The idea
Buildings go up on a street one at a time, from west to east. Each building wants to know the first taller building to its east. When a new one goes up, it settles that question for every shorter building still waiting at the east end of the street. As soon as it meets a waiting building at least as tall as itself, it can stop: every building still waiting further west is at least as tall again, or it would have been answered already.
That’s the whole algorithm. Keep a stack of indices whose answer isn’t known yet. When a new value arrives, it is the first bigger value for every smaller value on top of the stack, so pop them and record the answer. Then push the new index, because it’s waiting too. The stack stays sorted, with values going down from bottom to top, which is where the name comes from.
How it works
We’ll solve next greater element: for each index, the first value to its right that is strictly bigger, or -1 if there is none.
Scroll through the steps and the graphic follows along. Bars are the array, the row under them is ans, and the lane at the bottom is the stack, bottom on the left. You can also press play, or edit the input: try equal values, or a decreasing run followed by one big number.
- Start with
ansfull of -1 (“nothing bigger yet”) and an emptystack. - Move
ito the next index. The valuea[i]might be the answer for some of the indices waiting on the stack. - When the stack is empty, there’s nothing to compare, so push
i: it now waits for something bigger thana[i]. - Otherwise compare the
topof the stack witha[i]. Ifa[top]is bigger, it’s still waiting, and so is everything under it. Stop and pushi. - If
a[top] < a[i], thena[i]is the first bigger value to the right oftop: pop it and setans[top] = a[i]. - Equal isn’t bigger. Here
a[1] = 2meets another 2, so it stays on the stack and keeps waiting. - One big value can pop several in a row. The 5 answers the three indices it beats, stopping only when the stack is empty.
- At the end, whatever is left on the stack never saw a bigger value, so its
ansstays -1.
Why it’s correct: the stack always holds exactly the indices so far that haven’t seen a bigger value, and their values never increase from bottom to top. So the smaller values are all on top, and a new value pops precisely the indices it answers. Each pop happens at the first bigger value to the right, because anything earlier would have popped it already.
def next_greater(a):
"""ans[i] = the first value to the right of a[i] that is bigger, or -1."""
n = len(a)
ans = [-1] * n
stack = [] # indices still waiting for an answer; their values decrease
for i in range(n):
while stack and a[stack[-1]] < a[i]:
top = stack.pop()
ans[top] = a[i]
stack.append(i)
return ans#include <vector>
using namespace std;
// ans[i] = the first value to the right of a[i] that is bigger, or -1.
vector<int> nextGreater(const vector<int>& a) {
int n = a.size();
vector<int> ans(n, -1);
vector<int> stack; // indices still waiting for an answer; their values decrease
for (int i = 0; i < n; i++) {
while (!stack.empty() && a[stack.back()] < a[i]) {
int top = stack.back(); stack.pop_back();
ans[top] = a[i];
}
stack.push_back(i);
}
return ans;
}import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
class NextGreater {
// ans[i] = the first value to the right of a[i] that is bigger, or -1.
static int[] nextGreater(int[] a) {
int n = a.length;
int[] ans = new int[n];
Arrays.fill(ans, -1);
Deque<Integer> stack = new ArrayDeque<>(); // indices still waiting; their values decrease
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && a[stack.peek()] < a[i]) {
int top = stack.pop();
ans[top] = a[i];
}
stack.push(i);
}
return ans;
}
}The stack holds indices, not values. You need the index to know which ans slot to fill, and many variations need positions to measure distances or widths.
Why it’s O(n)
There’s a while inside a for, so it looks quadratic. Count per index instead of per loop: every index is pushed exactly once and popped at most once. Across the whole run the while loop body runs at most n times in total, however the pops are spread out. A single step can pop many, but only elements that earlier steps pushed. That’s an amortised O(1) per element.
| Input | Pushes | Pops | Total work |
|---|---|---|---|
Decreasing, 9 7 5 3 |
n | 0 | O(n) |
Increasing, 1 3 5 7 |
n | n − 1 | O(n) |
| Decreasing run then a spike | n | n − 1, all at once | O(n) |
Space is O(n): ans, plus a stack that holds every index on decreasing input.
Common mistakes
Pushing values instead of indices
With values on the stack you know what got popped but not where to write its answer, and duplicates make it impossible to look up.
stack.append(a[i]) # ✗ which ans slot does this belong to?
stack.append(i) # ✓ the value is always a[i]
The wrong comparison for ties
< pops only on a strictly bigger value, so equal values keep waiting. <= pops on equal values too, and finds the next greater-or-equal element. Decide which the problem wants, especially in counting problems where each subarray must be attributed to exactly one of two equal elements.
while stack and a[stack[-1]] <= a[i]: # ✗ for "strictly greater"
while stack and a[stack[-1]] < a[i]: # ✓
Using if instead of while
A new value can answer many waiting indices at once. With if, only the top one gets popped, and the stack stops being sorted.
if stack and a[stack[-1]] < a[i]: # ✗ pops at most one
while stack and a[stack[-1]] < a[i]: # ✓ pops everything it beats
Forgetting what’s left at the end
Some variations only compute on a pop, like the rectangle area in a histogram. Indices still on the stack at the end never get measured. Either handle them after the loop, or append a sentinel (height 0) that pops everything.
Variations
- Next smaller, previous greater, previous smaller. Flip the comparison to find smaller values. For the previous neighbour, the answer is whatever is on top of the stack right after you finish popping, just before you push
i. - Daily temperatures. Store the distance instead of the value:
ans[top] = i - top, the number of daystopwaited. - Stock span. For each day, count how many days back the price stayed at or below today’s. Pop while
price[top] <= price[i]; the span isiminus the index left on top (ori + 1if the stack is empty). - Largest rectangle in a histogram. Keep heights increasing. When bar
topis popped by a shorter bar ati, it can’t reachi, and it couldn’t reach past the new top on the left either, so its widest rectangle isa[top] * (i - stack[-1] - 1). A height-0 sentinel at the end flushes the stack. - Sliding window maximum. Keep a decreasing deque of indices. The back works like this stack; the front also drops indices that have slid out of the window. The front is then the maximum of the window, in O(n) total.
Check yourself
5 quick questions. Pick an answer to see why it's right or wrong.
-
1
For each day, how many days until a warmer temperature? There are up to 10⁵ days. Which approach fits?
“First warmer day to the right” is next greater element, the monotonic stack’s home ground: O(n) total. The sliding-window deque is a close relative, but it answers “max of each window of size k”, and there is no fixed window here.
-
2
The loop has a
whileinside afor. What’s its total running time on n elements?Count work per element, not per loop: n pushes and at most n pops in total, however they’re spread across iterations. A decreasing array never pops at all, so it’s the cheapest case, not the worst.
-
3
What does this print?
a = [3, 1, 3, 5, 2]ans = [-1] * len(a)stack = []for i in range(len(a)):while stack and a[stack[-1]] < a[i]:ans[stack.pop()] = a[i]stack.append(i)print(ans)The 1 at index 1 is popped by the 3 at index 2. The 3 at index 0 is not, because
3 < 3is false, so it waits for the 5. The 5 and the final 2 have nothing bigger after them. Switching to<=would give index 0 the answer 3. -
4
This computes the largest rectangle in a histogram, but it returns 0 for
[1, 2, 3]instead of 4. What’s wrong?def largest(h):best, stack = 0, []for i, x in enumerate(h):while stack and h[stack[-1]] > x:height = h[stack.pop()]width = i - stack[-1] - 1 if stack else ibest = max(best, height * width)stack.append(i)return bestEvery bar here stays on the stack to the end, so no area is ever computed. A height-0 sentinel pops them all: bar 2 then gets width 2 and area 4. The width formula is right: the rectangle spans strictly between the new top and
i. -
5
Stock span: for each day, how many days in a row, ending today, had a price at most today’s? What does this print?
prices = [90, 70, 50, 60, 50, 80, 95]span, stack = [], []for i, p in enumerate(prices):while stack and prices[stack[-1]] <= p:stack.pop()span.append(i - stack[-1] if stack else i + 1)stack.append(i)print(span)The stack keeps the previous greater prices. For 80 it pops 50, 60, 50 and 70, stopping at 90 (index 0), so the span is
5 - 0 = 5. For 95 the stack empties, so the span isi + 1 = 7. The answer is read from the top after popping: this is previous greater element, not next greater.