A stack's LIFO discipline does two completely different jobs: it pairs and defers work (brackets, expressions, undo) and it drives the monotonic stack that resolves next-greater / next-smaller queries in a single O(n) pass.
A stack means “most-recent-unresolved-thing first”: the top is always the newest thing you haven't finished dealing with — so whenever the next inputresolves something, the thing it resolves is sitting right on top.
Picture a desk where you stack tasks you can't finish yet. Each new unfinished thing goes on top, and you're only ever allowed to touch the top. The instant something arrives that completesthe most recent unfinished thing, it's right there waiting — you pop it, done.
That single rule powers two patterns that look unrelated but are the same idea:
Watch "([])" match. Every open bracket is pushed; every close pops and checks the top. Nesting falls out for free because the top is always the innermost open bracket:
s = "( [ ] )"
read '(' push stack: [ ( ] ← innermost open on top
read '[' push stack: [ ( [ ] ← '[' is now the open thing
read ']' pop matches? pop '[' vs ']' ✓ stack: [ ( ]
read ')' pop matches? pop '(' vs ')' ✓ stack: [ ]
end: stack empty → every open bracket got closed → VALID
The TOP is always the most-recent unresolved open bracket — the
exact one a closing bracket must answer to. That is why a stack,
and only a stack, matches nesting.Now the monotonic version. We keep a decreasing stack on [2, 1, 3] to find each element's next-greater. When 3 arrives it cascades — popping the 1 then the 2, and each pop resolves one waiting element:
arr = [ 2, 1, 3 ] stack holds indices waiting for a bigger value
(values strictly DECREASING bottom→top)
i=0 val 2 stack empty, push stack(vals): [ 2 ]
i=1 val 1 1 < 2, push stack(vals): [ 2, 1 ]
i=2 val 3 3 > 1 → pop 1, ans[1] = 3 stack(vals): [ 2 ]
3 > 2 → pop 2, ans[0] = 3 stack(vals): [ ]
push 3 stack(vals): [ 3 ]
leftover: 3 has nobody bigger to its right → ans[2] = -1
result: [ 3, 3, -1 ]
The arriving 3 "beats" everything it towers over and pops them.
Each popped element just found its next-greater answer — and each
index is pushed once, popped once → O(n) despite the inner loop.while looks nested, but each index is pushed once and popped once over the whole run. Total pops ≤ total pushes = n. The nesting is an illusion.When you suspect a stack, climb these rungs in order:
i − j distance or a span width when you pop — get the value back with arr[index].-1), or for matching problems it means the input is invalid.Before coding, say in one sentence what each item on the stack is waiting for. If you can finish that sentence, the push/pop logic is decided.
Strip away the specific problem and every monotonic-stack solution is the same three moves: pop everyone the newcomer beats (they just found their answer) → record it → push the newcomer to wait for its own. Read it as a sentence:
for each index i, left to right:
while stack not empty AND arr[i] beats arr[stack.top]:
j = stack.pop() # j was WAITING — i is its answer
record answer for j using i # value arr[i] or distance i - j
push i # i now waits for ITS answer
# anything still on the stack never found an answer → default (-1)The onlythings that change between problems are the “beats” comparison (> vs <, strict vs non-strict for duplicates) and what you record on a pop (a value, a distance, or an area). The shape never changes.
A stack stores work that is incomplete until some future condition is met. You push something now and pop it later when you encounter its match or resolution. Classic examples:
In all these cases the stack is essentially a scratchpad of open obligations waiting to be closed.
A monotonic stack maintains elements in sorted order by aggressively popping elements that violate the invariant as each new element arrives. Every element is pushed exactly once and popped at most once, so the total work is O(n) even though there is a nested while loop inside a for.
Crucially, you almost always push the index, not the value — that way, when you pop, you can compute distances or widths.
The inner while loop can pop at most n elements across the entire outer loop — the total number of pops is bounded by the total number of pushes, which is exactly n. This is the standard amortized argument.
For histogram / span problems (Largest Rectangle, Car Fleet) you often need both boundaries — loop left-to-right with an increasing stack and use the index gap to compute width.
Reach for a stack when you need to remember things in reverse order— either to match/cancel them (brackets, pairs) or to resolve a "what was the last X before this?" query (monotonic). If the problem involves the nearest element that is greater or smaller, the monotonic stack is almost always the right tool.
| matching brackets / valid parentheses | push opens, pop and verify on closes |
| evaluate expression / postfix / RPN | push operands, pop two on each operator |
| next greater / next warmer element | monotonic decreasing stack of indices |
| previous smaller / nearest smaller to the left | monotonic increasing stack; answer is the new top after popping |
| largest rectangle / maximum span / histogram area | monotonic increasing stack of indices; width = i − stack top − 1 |
| undo / most recently added / reverse-chronological access | plain LIFO stack |
| car fleet / group by arrival time, latest dominates | sort descending, monotonic stack of times |
When → Any problem that involves matching opening and closing delimiters or pairing elements that cancel each other out (brackets, tags, asterisks). Push openings, pop and verify on each closing.
function isValid(s: string): boolean {
const stack: string[] = [];
const close: Record<string, string> = { ')': '(', ']': '[', '}': '{' };
for (const ch of s) {
if (!close[ch]) {
stack.push(ch); // opening bracket → defer it
} else {
if (stack.pop() !== close[ch]) return false; // must match top
}
}
return stack.length === 0; // nothing left unmatched
}stack.length before popping, or the pop returns undefined (JavaScript) and the comparison silently fails instead of throwing.When → The problem asks for the next element to the right that is greater (or the distance to it). Keep a decreasing stack of indices; pop when the incoming value exceeds the top.
// Monotonic DECREASING stack → find the NEXT GREATER element to the right.
// Stack holds indices of elements waiting for their "answer".
function nextGreater(arr: number[]): number[] {
const n = arr.length;
const res = new Array<number>(n).fill(-1);
const stack: number[] = []; // indices, values are decreasing top→bottom
for (let i = 0; i < n; i++) {
// Pop while the current element is GREATER than the top of stack.
// Those popped elements have found their next-greater answer.
while (stack.length && arr[i] > arr[stack[stack.length - 1]]) {
res[stack.pop()!] = arr[i];
}
stack.push(i); // push index, NOT value
}
return res; // remaining indices keep -1 (no greater)
}i − stack.pop()). Retrieve the value with arr[index] when comparing.When → The problem asks for the next element to the right that is smaller, or for a left-boundary (previous smaller) by iterating right-to-left. Keep an increasing stack of indices; pop when the incoming value is less than the top.
// Monotonic INCREASING stack → find the NEXT SMALLER element to the right.
// Stack holds indices of elements waiting for their "answer".
function nextSmaller(arr: number[]): number[] {
const n = arr.length;
const res = new Array<number>(n).fill(-1);
const stack: number[] = []; // indices, values are increasing top→bottom
for (let i = 0; i < n; i++) {
// Pop while the current element is SMALLER than the top of stack.
while (stack.length && arr[i] < arr[stack[stack.length - 1]]) {
res[stack.pop()!] = arr[i];
}
stack.push(i);
}
return res;
}When → Problems that compute a max area or span bounded by a shorter bar on either side (Largest Rectangle in Histogram, Trapping Rain Water variant). Append a sentinel 0 to flush the stack cleanly at the end.
// Largest rectangle in histogram — monotonic increasing stack of bar indices.
// When a shorter bar arrives, pop taller bars and compute their max-width rectangle.
function largestRectangle(heights: number[]): number {
const bars = [...heights, 0]; // sentinel 0 flushes all remaining bars
const stack: number[] = []; // increasing stack of indices
let maxArea = 0;
for (let i = 0; i < bars.length; i++) {
while (stack.length && bars[i] < bars[stack[stack.length - 1]]) {
const h = bars[stack.pop()!];
// Width: from the new top-of-stack (exclusive) to i (exclusive)
const w = stack.length ? i - stack[stack.length - 1] - 1 : i;
maxArea = Math.max(maxArea, h * w);
}
stack.push(i);
}
return maxArea;
}j, the left boundary is the new stack top (exclusive) and the right boundary is i (exclusive). If the stack is empty after the pop, the bar at j is the shortest seen so far and its width spans all the way to the left edge: w = i.In JavaScript/TypeScript, Array.prototype.pop() on an empty array returns undefined instead of throwing. This silently corrupts comparisons. Always guard with stack.length > 0 before popping, or assert with stack.pop()! only after the guard.
Monotonic stack problems almost always require the index on the stack, not the value. When you pop to record an answer, you need the index to compute the distance (i − j) or the span width. Storing the value makes that calculation impossible. Retrieve the value via arr[stack.top] during comparison.
Whether you use > or >= (and < vs <=) in the "pop while" condition decides what happens with equal elements. Using >=pops duplicates eagerly (only the rightmost duplicate records the "next greater"); using >keeps duplicates on the stack so each one can answer independently. Read the problem statement carefully — "strictly greater" vs "greater or equal" changes the invariant.
After the main loop finishes, elements still on a monotonic stack have no next greater/smaller element — their answer is -1 (or 0, depending on the problem). For match/pair problems, a non-empty stack after the loop means unmatched opening brackets — the string is invalid. A common bug is returning early without this check.