Monotonic Stack: Next Greater Element and Daily Temperatures
A monotonic stack maintains elements in order and pops when that order breaks — finding the next greater element for every popped value in O(n) total.
Monotonic Stack: Next Greater Element and Daily Temperatures
The monotonic stack is one of those patterns that looks like a trick until you internalize the invariant. Once you do, you see it everywhere — next greater element, daily temperatures, largest rectangle in histogram, trapping rain water. They all reduce to the same underlying question: "for each element, when does the next something-bigger arrive?"
A monotonic decreasing stack maintains elements in descending order from bottom to top. Whenever you push something larger than the top, you pop until the invariant holds again. Each pop is the moment you've found the "next greater element" for the popped value.
The Core Invariant
Push onto the stack. If the new element is greater than the top, the top's "next greater" has arrived — pop and record the answer. Repeat until the stack is empty or the top is greater than the new element. Then push.
After processing all elements, anything left in the stack has no next greater element — they never got popped by something larger.
Next Greater Element I (LeetCode 496)
Given two arrays nums1 and nums2, find for each element in nums1 its next greater element in nums2.
Brute force — nested loops:
// For each element in nums1, scan right in nums2 to find NGE
// Time: O(n * m) — for each of n elements, scan up to m positions
// Space: O(1) excluding output
const nextGreaterElementBrute = (nums1, nums2) => {
return nums1.map((target) => {
let found = false;
for (const num of nums2) {
if (num === target) found = true;
if (found && num > target) return num;
}
return -1;
});
};This is O(n * m). The monotonic stack preprocesses nums2 in O(m) so each nums1 lookup is O(1).
Monotonic stack solution:
// Precompute NGE for all of nums2, then answer nums1 in O(1) per query
// Time: O(n + m) — one pass through nums2, one lookup pass through nums1
// Space: O(m) for the map and stack
const nextGreaterElementStack = (nums1, nums2) => {
const ngeMap = new Map(); // element → its next greater element in nums2
const stack = []; // monotonic decreasing stack
for (const num of nums2) {
// Pop everything smaller than num — num is their NGE
while (stack.length && stack[stack.length - 1] < num) {
ngeMap.set(stack.pop(), num);
}
stack.push(num);
}
// Remaining stack elements have no NGE
while (stack.length) ngeMap.set(stack.pop(), -1);
// `?? -1` matters: LeetCode guarantees nums1 is a subset of nums2, but without
// the fallback an element that isn't in nums2 yields `undefined`, not -1.
return nums1.map((num) => ngeMap.get(num) ?? -1);
};The while (stack.length && stack[stack.length - 1] < num) loop is the core. Read it as: "as long as the top of the stack is waiting for something greater, and num is greater — satisfy it."
Daily Temperatures (LeetCode 739)
Given an array of daily temperatures, return an array where each element is the number of days until a warmer temperature. If no warmer day exists, use 0.
This is the same pattern but indexed — you need the distance between positions, not just the next value.
Brute force:
// For each day, scan forward to find the next warmer day
// Time: O(n^2) — nested loops
// Space: O(1) excluding output
const dailyTemperaturesBrute = (temps) => {
const result = new Array(temps.length).fill(0);
for (let i = 0; i < temps.length; i++) {
for (let j = i + 1; j < temps.length; j++) {
if (temps[j] > temps[i]) {
result[i] = j - i;
break;
}
}
}
return result;
};Monotonic stack with indices:
// Stack holds indices, not values — we need distance between positions
// Time: O(n) — each index pushed and popped at most once
// Space: O(n) — stack holds at most n indices
const dailyTemperaturesStack = (temps) => {
const result = new Array(temps.length).fill(0);
const stack = []; // indices, stored in decreasing temperature order
for (let i = 0; i < temps.length; i++) {
// Pop all indices with temperature less than today's
while (stack.length && temps[stack[stack.length - 1]] < temps[i]) {
const prevIdx = stack.pop();
result[prevIdx] = i - prevIdx; // Days waited = current index - previous index
}
stack.push(i);
}
return result; // Indices still in stack get 0 (default) — no warmer day found
};Note the stack stores indices here because the answer requires i - prevIdx. You look up the temperature via temps[stack[stack.length - 1]]. This is the standard pattern for distance-based problems.
temps[stack[stack.length - 1]]. A common bug is storing the temperature in the stack and then losing the index needed to compute the distance.Online Stock Span (LeetCode 901)
Design a class that collects stock prices and returns the span — the number of consecutive days (including today) with a price less than or equal to today's.
This is the "look backward" version of the same pattern. Instead of scanning right for something bigger, you're counting how far left you can go while current stays biggest.
// Stack stores [price, span] pairs
// Time: O(1) amortized — each element pushed and popped at most once
// Space: O(n) — stack at most n deep
class StockSpanner {
constructor() {
this.stack = []; // [price, span] pairs
}
next(price) {
let span = 1;
// Merge spans of all previous days with price <= today's
while (this.stack.length && this.stack[this.stack.length - 1][0] <= price) {
span += this.stack.pop()[1]; // Absorb the span of the popped day
}
this.stack.push([price, span]);
return span;
}
}The key: when you pop a [price, span] pair, you absorb its span into the current span. You don't need to re-examine all those individual days — their span is already stored compactly.
Example trace for prices [100, 80, 60, 70, 60, 75, 85]. Feeding those seven prices
through the class above returns spans [1, 1, 1, 2, 1, 4, 6], and the stack just
before 85 arrives is [[100,1], [80,1], [75,4]] — only three entries, because 75
already absorbed the 60 and 70 behind it. So:
- 85 pops
[75, 4]and then[80, 1], stops at[100, 1], and returns 1 + 4 + 1 = 6
That compression is the whole trick. The three days 60, 70, 60 never get re-examined;
their work is already folded into the single [75, 4] entry.
The Three Variants at a Glance
// Variant 1: Find next greater VALUE (store values)
for (const num of nums) {
while (stack.length && stack[stack.length - 1] < num) {
ngeMap.set(stack.pop(), num);
}
stack.push(num);
}
// Variant 2: Find next greater by DISTANCE (store indices)
for (let i = 0; i < arr.length; i++) {
while (stack.length && arr[stack[stack.length - 1]] < arr[i]) {
const j = stack.pop();
result[j] = i - j; // Distance to next greater
}
stack.push(i);
}
// Variant 3: Count span looking BACKWARD (store [value, span])
let span = 1;
while (this.stack.length && this.stack[this.stack.length - 1][0] <= price) {
span += this.stack.pop()[1];
}
this.stack.push([price, span]);The decreasing order invariant stays the same across all three. What changes is what you store (value, index, or compressed span) and the direction you think about the problem (forward vs. backward).
Interview Tips
State the invariant first. "I'll use a monotonic decreasing stack — I'll maintain elements in decreasing order and pop whenever I see something larger." This immediately signals you know the pattern, not just a solution for this specific problem.
Indices vs. values: If you need positions (distances, spans), store indices. If you only need values (the NGE itself), store values. Storing indices is almost always the safer default since you can always recover the value via arr[idx].
The amortized complexity argument: Each element is pushed once and popped at most once → O(n) total work even though the inner while loop runs multiple times. Be ready to explain this — interviewers sometimes mistake the nested loops for O(n²).
It's worth checking the claim rather than asserting it. On random arrays of 100,000 temperatures in the LeetCode range 30–100 (Node 22.22.3), across five runs dailyTemperaturesStack took 1.5–2.8 ms against 41.6–45.0 ms for the nested-loop version. Call it 20x here — the useful part isn't the ratio, it's that the ratio grows with n, because one side is linear and the other isn't. Both functions returned identical output across 2,500 random arrays plus the empty, single-element, all-equal, strictly-increasing and strictly-decreasing cases.
Decreasing vs. increasing stack:
- Next greater element → decreasing stack (pop when you see something larger)
- Next smaller element → increasing stack (pop when you see something smaller)
- Largest Rectangle in Histogram → increasing stack (pop when the bar gets shorter)
- Trapping Rain Water → also solvable with monotonic stack, though two-pointer is more common
Follow-up to expect: Largest Rectangle in Histogram (LC 84) uses the same pop-on-smaller invariant but needs careful handling of the "remaining stack at end" case. That problem is worth practicing as a harder extension.
Comments (0)
No comments yet. Be the first to share your thoughts!