Stacks and Monotonic Stacks
Use a stack for anything nested or most-recent-first, and a monotonic stack to find the next greater or smaller element in O(n).
- O(n) amortisedTime: n pushes, at most n pops
- O(n)Space in the worst case, everything nested
- O(1)Push, pop or peek at the array end
Show what to say
“When things open and close in a nested way, or I need the next greater or smaller element for each position, I use a stack, because the newest unfinished item resolves first. A monotonic stack pops everything a new element beats, so every answer comes in one O(n) pass instead of O(n²).”
Trade-offs
- Stack vs a counter per bracket type: a counter accepts ([)], the stack catches the crossing the moment it happens, at O(n) worst-case memory.
- Monotonic stack vs brute force: O(n) amortised against O(n²) on a falling array, for O(n) extra space.
- Indices vs values on the stack: indices cost the same and give positions and distances; values alone cannot compute a width.
Bring it up before they ask
Check the stack is non-empty before you peek (stack.at(-1) is undefined and the comparison silently fails), and read whether ties are strict or 'or equal'.
A stack is a pile of plates: the last one on is the first one off, the order nested things close in. A monotonic stack answers every "next greater" question in one O(n) pass instead of O(n²).
When to reach for it
Tap the signals you can see in the problem.
Rememberopen and closeundolatestnext (or previous) greater or smaller
- Matching and nesting
The stack holdsopeners still waiting
valid parentheses - Expression evaluation
The stack holdsoperands waiting
reverse Polish notation - Monotonic decreasing
The stack holdsindices waiting for a greater value
daily temperatures - Monotonic increasing
The stack holdsindices waiting for a smaller value
largest rectangle in histogram
The core idea: the most recent open item closes first
In { [ ( ) ] } the last bracket opened is the first one closed.
Read more · 63 words
A stack keeps the newest on top and takes from the top, with O(1) push and pop. A closer never searches: it looks at the top.
Counting isn't enough. ([)] has one of each bracket, but when ) arrives the newest open bracket is [. The stack catches the crossing at once:
Always ask: what is still open, and does the newest one resolve first?
Matching brackets: the newest open bracket closes first
- size
- 0
Start with an empty stack. It will hold the opening brackets that are still waiting to be closed.
function isBalanced(s) {
const PAIRS = new Map([[")", "("], ["]", "["], ["}", "{"]]);
const stack = [];
for (const ch of s) {
if (PAIRS.has(ch)) {
const open = stack.pop();
if (open !== PAIRS.get(ch)) return false;
} else {
stack.push(ch);
}
}
return stack.length === 0;
}All steps as text
- Start with an empty stack. It will hold the opening brackets that are still waiting to be closed.
- { opens a new pair. Push it: it must be closed before anything that was opened earlier.
- [ opens a new pair. Push it: it must be closed before anything that was opened earlier.
- ( opens a new pair. Push it: it must be closed before anything that was opened earlier.
- ) must close the most recently opened bracket, which is the top: ( at index 2. They are partners, so pop it.
- ] must close the most recently opened bracket, which is the top: [ at index 1. They are partners, so pop it.
- } must close the most recently opened bracket, which is the top: { at index 0. They are partners, so pop it.
- Every closer matched the top of the stack and the stack is empty: the brackets are balanced.
Play with it
Tap brackets: each opener lands on the stack, each closer takes the top one off. Try ([)]. Then switch to Next greater.
Read more · 90 words
The recipe:
Name what is waiting
"Openers not yet closed", "indices without a greater value yet". That is your invariant.
Decide what resolves the top
A closer, an operator, or a value that beats the top. Use a
whilewhen one element can resolve several.Do the work at pop time
An item's answer is known the moment it is popped.
Push the new element
Push an index if you need a position or distance later.
Handle what is left
Leftovers never resolved: unclosed brackets (invalid), no greater value (-1).
Templates
Template 1: matching and nesting
Push what opens; when something closes, pop and check it against the top.
This solves Valid ParenthesesEasy. With numbers it becomes Evaluate Reverse Polish NotationMedium: each operator pops two, and the first pop is the right operand.
// Template 1: matching and nesting. Push what is still open; a closer must match the most recent open item.
const PAIRS = new Map([[")", "("], ["]", "["], ["}", "{"]]);
export function isBalanced(s) {
const stack = [];
for (const ch of s) {
if (PAIRS.has(ch)) {
const open = stack.pop(); // undefined when nothing is open
if (open !== PAIRS.get(ch)) return false;
} else {
stack.push(ch);
}
}
return stack.length === 0; // anything left was never closed
}
// The same shape handles "undo the most recent": ".." cancels the latest directory.
export function simplifyPath(path) {
const stack = [];
for (const part of path.split("/")) {
if (part === "..") stack.pop();
else if (part !== "" && part !== ".") stack.push(part);
}
return "/" + stack.join("/");
}Template 2: monotonic decreasing stack (next greater)
Brute force scans right from every index: O(n²).
Read more · 77 words
Instead, walk left to right and keep on a stack the indices that have not found their answer yet.
Each new value pops everything smaller. A popped index j just found its answer: i is its next greater element, and the first, since any bigger value in between would have popped it earlier. Try your own numbers with Edit input:
Store indices, not values. Next Greater ElementEasy records nums[i] at pop time; Daily TemperaturesMedium records the distance i - j.
Monotonic decreasing stack: next greater element
- i
- null
Every answer starts as -1. The stack will hold indices that are still waiting for a bigger value to their right.
function nextGreaterIndex(nums) {
const answer = new Array(nums.length).fill(-1);
const stack = [];
for (let i = 0; i < nums.length; i++) {
while (stack.length && nums[stack.at(-1)] < nums[i]) {
answer[stack.pop()] = i;
}
stack.push(i);
}
return answer;
}All steps as text
- Every answer starts as -1. The stack will hold indices that are still waiting for a bigger value to their right.
- The stack is empty, so nothing else is waiting. Push index 0: it waits for a bigger value to its right.
- 5 on top is bigger than 2, so popping stops. Push index 1: it waits for a bigger value to its right.
- 2 on top is smaller than 3, and nothing between them was bigger (or 2 would be gone already). So index 2 is the next greater for index 1: pop it.
- 5 on top is bigger than 3, so popping stops. Push index 2: it waits for a bigger value to its right.
- 3 on top is bigger than 1, so popping stops. Push index 3: it waits for a bigger value to its right.
- 1 on top is smaller than 6, and nothing between them was bigger (or 1 would be gone already). So index 4 is the next greater for index 3: pop it.
- 3 on top is smaller than 6, and nothing between them was bigger (or 3 would be gone already). So index 4 is the next greater for index 2: pop it.
- 5 on top is smaller than 6, and nothing between them was bigger (or 5 would be gone already). So index 4 is the next greater for index 0: pop it.
- The stack is empty, so nothing else is waiting. Push index 4: it waits for a bigger value to its right.
- 6 on top is bigger than 4, so popping stops. Push index 5: it waits for a bigger value to its right.
- Indices 4, 5 are still on the stack: nothing to their right is bigger, so they keep -1. Each index was pushed once and popped at most once: O(n).
// Template 2: monotonic decreasing stack. For each index, the index of the first strictly greater value to its
// right, or -1. The stack holds indices still waiting for an answer; their values never rise from bottom to top.
export function nextGreaterIndex(nums) {
const answer = new Array(nums.length).fill(-1);
const stack = [];
for (let i = 0; i < nums.length; i++) {
while (stack.length && nums[stack.at(-1)] < nums[i]) {
answer[stack.pop()] = i; // nums[i] resolves every smaller value waiting on top
}
stack.push(i);
}
return answer;
}
// Template 3: monotonic increasing stack. right[i] is the first index to the right with a smaller value (n if none);
// left[i] is the nearest index to the left with a value that is not larger (-1 if none).
// Each pop resolves the popped index's right side; the top left after popping is the current index's left side.
export function smallerNeighbours(nums) {
const n = nums.length;
const left = new Array(n).fill(-1);
const right = new Array(n).fill(n);
const stack = [];
for (let i = 0; i < n; i++) {
while (stack.length && nums[stack.at(-1)] > nums[i]) {
right[stack.pop()] = i;
}
left[i] = stack.length ? stack.at(-1) : -1;
stack.push(i);
}
return { left, right };
}Template 3: monotonic increasing stack (next smaller, both sides)
Flip the comparison and each pop answers "next smaller".
Read more · 52 words
A second answer comes free: after the pops, the top is the nearest value left of i that is not larger.
Those two neighbours are all Largest Rectangle in HistogramHard needs: the widest rectangle of height heights[j] has width right - left - 1.
Strictness decides ties. Read for "strictly" or "or equal" before choosing < or <=.
Monotonic increasing stack: nearest smaller on both sides
- i
- null
left starts at -1 and right at n = 6, meaning "no smaller neighbour". The stack will keep indices whose values never go down from bottom to top.
function smallerNeighbours(nums) {
const n = nums.length;
const left = new Array(n).fill(-1);
const right = new Array(n).fill(n);
const stack = [];
for (let i = 0; i < n; i++) {
while (stack.length && nums[stack.at(-1)] > nums[i]) {
right[stack.pop()] = i;
}
left[i] = stack.length ? stack.at(-1) : -1;
stack.push(i);
}
return { left, right };
}All steps as text
- left starts at -1 and right at n = 6, meaning "no smaller neighbour". The stack will keep indices whose values never go down from bottom to top.
- The stack is empty: nothing to the left of index 0 is smaller than or equal to 4, so left[0] stays -1.
- Push index 0. Values on the stack still never go down from bottom to top, and 4 waits for a smaller value to its right.
- 4 at index 0, right next to it, is smaller than 6: left[1] = 0.
- Push index 1. Values on the stack still never go down from bottom to top, and 6 waits for a smaller value to its right.
- 5 is smaller than 6 on top, so 6 can't stretch past index 2: the first smaller value to its right is at 2. Pop index 1.
- Everything between index 0 and index 2 was larger and has been popped, so 4 at index 0 is the nearest smaller value on the left: left[2] = 0.
- Push index 2. Values on the stack still never go down from bottom to top, and 5 waits for a smaller value to its right.
- 2 is smaller than 5 on top, so 5 can't stretch past index 3: the first smaller value to its right is at 3. Pop index 2.
- 2 is smaller than 4 on top, so 4 can't stretch past index 3: the first smaller value to its right is at 3. Pop index 0.
- The stack is empty: nothing to the left of index 3 is smaller than or equal to 2, so left[3] stays -1.
- Push index 3. Values on the stack still never go down from bottom to top, and 2 waits for a smaller value to its right.
- 2 at index 3, right next to it, is smaller than 7: left[4] = 3.
- Push index 4. Values on the stack still never go down from bottom to top, and 7 waits for a smaller value to its right.
- 3 is smaller than 7 on top, so 7 can't stretch past index 5: the first smaller value to its right is at 5. Pop index 4.
- Everything between index 3 and index 5 was larger and has been popped, so 2 at index 3 is the nearest smaller value on the left: left[5] = 3.
- Push index 5. Values on the stack still never go down from bottom to top, and 3 waits for a smaller value to its right.
- Indices 3, 5 are never popped: no smaller value follows them, so their right stays n = 6. One pass found both neighbours of every element.
Try it
// Edit the inputs and press Run. These are the templates above.
console.log(isBalanced('{[()()]}'), isBalanced('([)]')); // true false
console.log(simplifyPath('/home//user/./docs/../pics/')); // /home/user/pics
const temps = [5, 2, 3, 1, 6, 4];
const next = nextGreaterIndex(temps);
console.log(next); // [4, 2, 4, 4, -1, -1]
console.log(next.map((j, i) => (j === -1 ? 0 : j - i))); // days to wait: [4, 1, 2, 1, 0, 0]
const { left, right } = smallerNeighbours([4, 6, 5, 2, 7, 3]);
console.log(left, right); // [-1, 0, 0, -1, 3, 3] [3, 2, 3, 6, 5, 6]Complexity
Read more · 27 words
| Variant | Time | Extra space |
|---|---|---|
| Matching / expression evaluation | O(n) | O(n) worst case |
| Monotonic stack | O(n) amortised | O(n) |
| Brute-force next greater | O(n²) | O(1) |
JavaScript you'll use
JavaScript refresher: Stacks, queues and deques Arrays: create, read, updateRead more · 39 words
The top of a JavaScript stack is the end of the array; shift and unshift are O(n).
const stack = [];
stack.push(x); // O(1): add on top
const top = stack.pop(); // O(1): remove the top (undefined when the stack is empty)
stack.at(-1); // peek at the top without removing it
stack.length === 0; // empty check
new Array(n).fill(-1); // default answers for "never resolved"Interview questions
Why is the monotonic stack O(n) with a while loop inside a for loop?
Each index is pushed once and popped at most once, so all the while iterations together pop at most n items.
How do you find the next greater element in a circular array?
Walk it twice, reading nums[i % n]. Still O(n).
You only need the answer for a few query values. What changes?
Run the stack once, store answers in a Map, then each query is one lookup.
How do you build a queue from two stacks?
Push onto in; pop from out, refilling it from in only when empty. O(1) amortised.
When would you use a stack instead of recursion?
When nesting can be very deep: 10^5 levels can overflow the call stack.
Check yourself
Question 1 of 3Score 2 out of 3 or better to mark this page complete.
Practice problems
Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.
- Valid ParenthesesCheck that every bracket in a string is closed by the right kind of bracket, in the right order.Easy
- Next Greater ElementFor every number, find the first strictly larger number to its right, or -1 if there is none.Easy
- Min StackBuild a stack that also reports its current minimum in O(1), even after pops.Medium
- Evaluate Reverse Polish NotationCompute the value of an arithmetic expression written in postfix order, where each operator follows its two operands.Medium
- Car FleetCount how many groups of cars arrive at a destination when faster cars get stuck behind slower ones.Medium
- Daily TemperaturesFor each day, count how many days you wait until a strictly warmer one, or 0 if it never comes.Medium
- Largest Rectangle in HistogramFind the area of the largest rectangle that fits inside a histogram of unit-width bars.Hard
- Trapping Rain WaterGiven an elevation map of bars, compute how much rain water is trapped between them.Hard
- Maximal RectangleFind the area of the largest rectangle made only of 1s inside a grid of 0s and 1s.Hard