Pattern family: Stack / Monotonic Stack / Design / Expression Evaluation Difficulty range: Easy β Hard Language: Java
- How to identify this pattern β start here
- What is this pattern?
- Core rules
- 2-Question framework
- Variants table
- Universal Java template
- Quick reference cheatsheet
- Common mistakes
- Complexity summary
| If you see this... | It means |
|---|---|
| "next greater / smaller element" | Monotonic Stack |
| "previous greater / smaller element" | Monotonic Stack (RβL) |
| "largest rectangle in histogram" | Monotonic Stack (width calculation) |
| "trapping rain water" | Monotonic Stack (valley detection) |
| "sum of subarray minimums / maximums" | Monotonic Stack (contribution technique) |
| "stock span / consecutive days" | Monotonic Stack (distance to prev greater) |
| "valid / balanced parentheses" | Stack β bracket matching |
| "decode 3[abc] / nested encoding" | Stack β save state on [, restore on ] |
| "k adjacent equal duplicates" | Stack + Count [char, count] pair |
| "remove duplicates / backspace / stars" | Stack β pop on trigger character |
| "evaluate expression / RPN / calculator" | Stack β operand push, operator pop-2 |
| "collision / asteroid" | Stack β conditional pop |
| "simplify path / canonical path" | Stack β push dirs, pop on .. |
| "min stack / max frequency stack" | Design Stack β augmented state |
| "implement queue using stacks" | Two Stacks β inbox/outbox |
| "browser history / undo/redo" | Two Stacks β back/forward |
Brute force: for "next greater element" check every element to the right β O(nΒ²).
β Interviewer says "O(n)" β cannot scan right for every element.
β Key insight: if you maintain a DECREASING stack (monotonic),
the moment a larger element arrives, it IS the answer for everything it's larger than.
β Total: O(n) β each element pushed and popped at most once.
Signal 1 β "Nearest / next / previous" + "greater / smaller"
Classic monotonic stack. The answer for each element depends on the nearest element satisfying a condition β stack gives O(1) lookup.
Signal 2 β "Remove" + "adjacent" or "trigger character"
Stack naturally undoes the most recent action. Backspace, stars, k-duplicates β all are "pop on condition" problems.
Signal 3 β "Nested structure" (brackets, encoded strings)
When you need to save current state, go deeper, then restore β push on
[, pop on]. Valid parentheses, decode string, score of parentheses all follow this.
Signal 4 β "Evaluate expression" or "calculator"
Operands pushed, operators trigger pop-2-calculate-push-1. RPN is the pure form; Basic Calculator adds operator precedence handling.
Signal 5 β "Design" + "O(1) getMin/getMax" or "history"
Two stacks solve undo/redo and queue-from-stack problems. Augmented stack (storing extra state alongside value) handles min/max in O(1).
Problem involves a stack-like structure?
β
βΌ
What is the core operation?
β
βββ NEAREST GREATER/SMALLER element
β βββ Monotonic Stack
β βββ Next β loop LβR, pop when condition met
β βββ Prev β loop RβL, check top before push
β
βββ REMOVE characters based on condition
β βββ Single char trigger (#, *) β Stack Simulation
β βββ k adjacent same chars β Stack + [char, count]
β
βββ NESTED / BRACKET structure
β βββ Valid/matching β push open, pop+match close
β βββ Decode/encode string β push (count, built-string)
β βββ Score / depth calculation β push 0 on (, compute on )
β
βββ EVALUATE expression
β βββ RPN (postfix) β push operands, operator triggers calc
β βββ Infix (+operator precedence) β two-stack or shunting yard
β
βββ DESIGN problem
β βββ Min/Max Stack β augmented stack (val, minSoFar)
β βββ Queue from Stacks β inbox + outbox stacks
β βββ Browser History β back-stack + forward-stack
β βββ Max Frequency Stack β freq map + group map
β
βββ AREA / WIDTH calculation (histogram, rain water)
βββ Monotonic Stack
βββ Pop = valley/boundary found
βββ Width = i - stack.peek() - 1
Core data structure: Stack = LIFO (Last In First Out). The top always holds the most recently seen / most relevant element.
Monotonic Stack: Maintain a stack where elements are always in sorted order (increasing or decreasing). When the order is violated, pop β and popping means the answer for that element has been found.
Push INDEX, not value. From index you can get value, distance, AND width.
Pop = answer found for the popped element.
Elements left in stack = have no answer β default -1 or 0.
Stack + Count (Pair Stack):
Store [char, count] pairs. When count reaches threshold β pop the entire group.
Cascading is automatic β after a pop, the new top is checked on the very next iteration.
Stack Simulation: Stack as a simulation machine. Each token/character triggers an action: push, pop, or compute. The top always holds the "most recent active state."
Design Stacks: Augment the stack with extra information (min so far, frequency, etc.) or use two stacks to simulate other data structures.
Min Stack: push (val, minSoFar) pair β O(1) getMin
Queue: inbox stack + outbox stack β amortized O(1) dequeue
Browser: back-stack + forward-stack β O(1) navigate
Rule 1 β Always push INDEX, not VALUE (for monotonic stack)
// WRONG β cannot calculate distance or width from value alone
st.push(arr[i]);
// CORRECT β index gives you value, distance, AND width
st.push(i);
// arr[st.peek()] = value
// i - st.peek() = distance
// i - st.peek() - 1 = width between elementsRule 2 β Pop condition determines the variant
// WRONG β mixing up > and < gives you the opposite answer
while (!st.isEmpty() && arr[st.peek()] > arr[i]) // this is NEXT SMALLER, not next greater
// CORRECT mental model:
// Next Greater β pop when top < current (current IS the answer for popped)
// Next Smaller β pop when top > current
// Prev Greater β loop RβL, check top before push
// Prev Smaller β loop RβL, check top before pushRule 3 β Always check !isEmpty() before peek() or pop()
// WRONG β NPE when stack is empty
while (arr[st.peek()] < arr[i]) { ... }
// CORRECT β always guard with isEmpty check
while (!st.isEmpty() && arr[st.peek()] < arr[i]) { ... }Rule 4 β Use Deque<T> not Stack<T>
// WRONG β Stack<T> is synchronized (slow), legacy class
Stack<Integer> st = new Stack<>();
// CORRECT β ArrayDeque is faster, Java-recommended
Deque<Integer> st = new ArrayDeque<>();Rule 5 β For expression evaluation, pop b before a
// WRONG β order matters for subtraction and division!
int a = stack.pop();
int b = stack.pop();
stack.push(a - b); // WRONG: b was pushed first, a is more recent
// CORRECT
int b = stack.pop(); // more recent (right operand)
int a = stack.pop(); // older (left operand)
stack.push(a - b); // a - b β| Answer | Variant |
|---|---|
| A larger element arrives | Monotonic Decreasing Stack (Next Greater) |
| A smaller element arrives | Monotonic Increasing Stack (Next Smaller) |
A special character (#, *) |
Stack Simulation (backspace, star removal) |
| Count reaches threshold k | Stack + Count (k-duplicate removal) |
A closing bracket ) / ] |
Nested structure (parentheses, decode) |
| An operator token | Expression Evaluation (RPN, Calculator) |
| A collision condition | Conditional Simulation (Asteroid) |
| Store | When | Example |
|---|---|---|
| Index | Need distance or width | Monotonic stack, histogram, rain water |
| [char, count] | Need count of consecutive | k-duplicate removal |
| char | Simple removal | Backspace, valid parentheses |
| int value | Computing running result | RPN, asteroid |
| (count, StringBuilder) | Nested string building | Decode String |
| (val, minSoFar) | O(1) min query | Min Stack |
| [price, span] | Online/streaming design | Online Stock Span |
Decision shortcut:
- "next/prev + greater/smaller" β monotonic stack, push index
- "remove on trigger char" β push char, pop on trigger
- "k adjacent same" β push
[char, count], pop when count==k- "bracket matching" β push open, match on close
- "evaluate expression" β push numbers, operator pops 2
- "O(1) getMin" β push (value, min) pair
Common core:
Deque<> st = new ArrayDeque<>()What differs: loop direction, pop condition, what you push, what you record
| Variant | Loop | Pop when | Push | Record |
|---|---|---|---|---|
| Next Greater | LβR | arr[top] < arr[i] |
index | result[pop] = arr[i] |
| Next Smaller | LβR | arr[top] > arr[i] |
index | result[pop] = arr[i] |
| Prev Greater | RβL | arr[top] <= arr[i] |
index | result[i] = arr[top] before push |
| Prev Smaller | RβL | arr[top] >= arr[i] |
index | result[i] = arr[top] before push |
| Histogram | LβR + sentinel | arr[top] > h |
index | h Γ (i - newTop - 1) |
| Rain Water | LβR | height[top] < height[i] |
index | (min(L,R) - bottom) Γ width |
| Circular NGE | LβR 2n |
nums[top] < nums[i%n] |
index (1st pass only) | result[pop] = nums[i%n] |
| Stock Span | LβR | arr[top] <= arr[i] |
index | i - top before push |
| k-Duplicate | LβR | count == k |
[char, count] |
pop entire group |
| Backspace | LβR | ch == '#' |
char | remaining chars |
| Valid Parens | LβR | mismatch on ) |
open bracket | stack empty at end |
| Decode String | LβR | on ] |
(count, sb) |
repeat and restore |
| RPN | LβR | on operator | int value | a op b, push result |
| Min Stack | push | never (keep both) | (val, min) |
peek()[1] = min |
| Queue/2-Stack | push | transfer on empty outbox | int value | front = outbox.peek() |
// βββ TEMPLATE A: Monotonic Stack β Next Greater βββββββββββββββββββββββββββββββ
int[] result = new int[n];
Arrays.fill(result, -1); // default: no answer
Deque<Integer> st = new ArrayDeque<>();
for (int i = 0; i < n; i++) { // β Prev type: i = n-1 to 0
while (!st.isEmpty() && arr[st.peek()] < arr[i]) { // β Smaller: use >
result[st.pop()] = arr[i]; // β Prev type: result[i] = arr[st.peek()]
}
st.push(i); // always push index
}
// Elements still in st have no next greater β result stays -1
// βββ TEMPLATE B: Histogram / Width Calculation βββββββββββββββββββββββββββββββ
Deque<Integer> st = new ArrayDeque<>();
st.push(-1); // sentinel for width calculation
int maxArea = 0;
for (int i = 0; i <= n; i++) {
int h = (i == n) ? 0 : arr[i]; // trailing sentinel forces all pops
while (st.peek() != -1 && arr[st.peek()] > h) {
int height = arr[st.pop()];
int width = i - st.peek() - 1; // width between current and new top
maxArea = Math.max(maxArea, height * width);
}
st.push(i);
}
// βββ TEMPLATE C: Stack + Count (k-Duplicate Removal) βββββββββββββββββββββββββ
Deque<int[]> st = new ArrayDeque<>(); // [charCode, count]
for (char ch : s.toCharArray()) {
if (!st.isEmpty() && st.peek()[0] == ch) {
st.peek()[1]++;
if (st.peek()[1] == k) st.pop(); // β change threshold here
} else {
st.push(new int[]{ch, 1});
}
}
// Rebuild: reverse list, repeat each char by count
// βββ TEMPLATE D: Bracket Matching (Valid Parentheses) ββββββββββββββββββββββββ
Deque<Character> st = new ArrayDeque<>();
for (char ch : s.toCharArray()) {
if (ch == '(' || ch == '{' || ch == '[') {
st.push(ch);
} else {
if (st.isEmpty()) return false;
char top = st.pop();
if (ch == ')' && top != '(') return false;
if (ch == '}' && top != '{') return false;
if (ch == ']' && top != '[') return false;
}
}
return st.isEmpty();
// βββ TEMPLATE E: Decode String (Save/Restore State) ββββββββββββββββββββββββββ
Deque<Integer> counts = new ArrayDeque<>();
Deque<StringBuilder> strings = new ArrayDeque<>();
StringBuilder curr = new StringBuilder();
int k = 0;
for (char ch : s.toCharArray()) {
if (Character.isDigit(ch)) { k = k * 10 + (ch - '0'); }
else if (ch == '[') { counts.push(k); strings.push(curr); curr = new StringBuilder(); k = 0; }
else if (ch == ']') { int t = counts.pop(); StringBuilder prev = strings.pop(); for (int i = 0; i < t; i++) prev.append(curr); curr = prev; }
else { curr.append(ch); }
}
return curr.toString();
// βββ TEMPLATE F: Expression Evaluation (RPN) βββββββββββββββββββββββββββββββββ
Deque<Integer> st = new ArrayDeque<>();
for (String token : tokens) {
if ("+-*/".contains(token)) {
int b = st.pop(); // β b first (right operand)
int a = st.pop(); // β a second (left operand)
switch (token) {
case "+" -> st.push(a + b);
case "-" -> st.push(a - b);
case "*" -> st.push(a * b);
case "/" -> st.push(a / b);
}
} else {
st.push(Integer.parseInt(token));
}
}
return st.pop();
// βββ TEMPLATE G: Min Stack (Augmented) βββββββββββββββββββββββββββββββββββββββ
Deque<int[]> st = new ArrayDeque<>(); // [value, minSoFar]
public void push(int val) {
int min = st.isEmpty() ? val : Math.min(val, st.peek()[1]);
st.push(new int[]{val, min});
}
public void pop() { st.pop(); }
public int top() { return st.peek()[0]; }
public int getMin() { return st.peek()[1]; } // O(1) always
// βββ TEMPLATE H: Queue Using Two Stacks ββββββββββββββββββββββββββββββββββββββ
Deque<Integer> inbox = new ArrayDeque<>(); // push here
Deque<Integer> outbox = new ArrayDeque<>(); // pop/peek from here
public void push(int x) { inbox.push(x); }
public int pop() {
transfer();
return outbox.pop();
}
public int peek() {
transfer();
return outbox.peek();
}
private void transfer() {
if (outbox.isEmpty())
while (!inbox.isEmpty()) outbox.push(inbox.pop());
}
// βββ TEMPLATE I: Browser History (Two Stacks) ββββββββββββββββββββββββββββββββ
Deque<String> back = new ArrayDeque<>(); // visited pages
Deque<String> forward = new ArrayDeque<>(); // pages after current
String curr;
public void visit(String url) { back.push(curr); curr = url; forward.clear(); }
public String back(int steps) { while (steps-- > 0 && !back.isEmpty()) { forward.push(curr); curr = back.pop(); } return curr; }
public String forward(int steps) { while (steps-- > 0 && !forward.isEmpty()) { back.push(curr); curr = forward.pop(); } return curr; }SIGNAL IN PROBLEM β VARIANT β KEY CODE
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
"next greater element" β Monotonic β stack β pop when top < curr; result[pop] = curr
"next smaller element" β Monotonic β stack β pop when top > curr; result[pop] = curr
"previous greater / stock span" β Monotonic, RβL or LβR β result[i] = top before push
"largest rectangle in histogram" β Monotonic + sentinel β area = h Γ (i - newTop - 1)
"trapping rain water" β Monotonic valley β water = (min(L,R) - bottom) Γ width
"sum of subarray minimums" β Monotonic contribution β arr[idx] Γ left Γ right
"circular array next greater" β Monotonic 2n loop β i % n; push only first n
"valid / balanced parentheses" β Bracket match β push open, pop+match close
"remove k adjacent same" β Stack + [char, count] β count == k β pop
"backspace / star / # removal" β Stack simulation β pop on trigger char
"decode 3[abc]" β Save/restore state β push (k, curr) on [; restore on ]
"evaluate RPN / postfix" β Expression stack β b=pop, a=pop; push(a op b)
"basic calculator II" β Operator precedence β handle +- with sign, */ inline
"min stack / O(1) getMin" β Augmented stack β push (val, min); getMin = peek()[1]
"implement queue using stacks" β Two stacks β inbox push; transfer on empty outbox
"browser history / undo redo" β Two stacks β back-stack + forward-stack
"asteroid collision" β Conditional simulation β pop while top>0 and curr<0
Burn these into memory:
// Monotonic Stack β 4 variants in 2 lines each:
// Next Greater: LβR, pop when top < curr, result[pop] = curr
// Next Smaller: LβR, pop when top > curr, result[pop] = curr
// Prev Greater: RβL, pop when top <= curr, result[i] = top (before push)
// Prev Smaller: RβL, pop when top >= curr, result[i] = top (before push)
// Width formula (histogram/rain water):
int width = i - st.peek() - 1; // between current and new stack top
// Expression eval β ORDER MATTERS:
int b = st.pop(); // right operand first
int a = st.pop(); // left operand second
st.push(a - b); // a - b, NOT b - a// BUG β cannot calculate distance or width
st.push(arr[i]); // only value stored
// FIX β push index, derive value when needed
st.push(i);
int val = arr[st.peek()]; // get value
int dist = i - st.pop(); // get distance
int width = i - st.peek() - 1; // get width after pop// BUG β this finds Next SMALLER, not Next Greater
while (!st.isEmpty() && arr[st.peek()] > arr[i])
result[st.pop()] = arr[i];
// FIX β Next Greater: pop when top is LESS than current
while (!st.isEmpty() && arr[st.peek()] < arr[i])
result[st.pop()] = arr[i];// BUG β NPE when stack is empty
while (arr[st.peek()] < arr[i]) { ... }
// FIX
while (!st.isEmpty() && arr[st.peek()] < arr[i]) { ... }// BUG β for subtraction and division, order matters
int a = st.pop(); // actually the RIGHT operand (most recently pushed)
int b = st.pop(); // actually the LEFT operand
st.push(a - b); // WRONG: should be b - a
// FIX β pop b (right) first, then a (left)
int b = st.pop(); // right operand
int a = st.pop(); // left operand
st.push(a - b); // correct: a - b β// BUG β bars taller than all others are never processed
for (int i = 0; i < n; i++) { ... } // loop ends, stack still has indices
// FIX β add trailing sentinel 0 to force all pops
for (int i = 0; i <= n; i++) {
int h = (i == n) ? 0 : arr[i]; // sentinel forces all remaining pops
...
}// WRONG β Stack is synchronized, slow, legacy
Stack<Integer> st = new Stack<>();
// CORRECT β ArrayDeque is the Java-recommended modern stack
Deque<Integer> st = new ArrayDeque<>();
// push β st.push(x) peek β st.peek() pop β st.pop()// BUG β stack is LIFO, reading directly gives reversed result
StringBuilder sb = new StringBuilder();
while (!st.isEmpty()) sb.append((char) st.pop()[0]);
// sb is in reverse order!
// FIX β either reverse at end, or iterate list after Collections.reverse()
List<int[]> list = new ArrayList<>(st);
Collections.reverse(list); // reverse first
for (int[] pair : list) { ... append pair ... }// BUG β old forward history persists after visiting a new page
public void visit(String url) {
back.push(curr);
curr = url;
// forgot: forward.clear() β stale forward history!
}
// FIX
public void visit(String url) {
back.push(curr); curr = url; forward.clear(); // always clear forward on new visit
}| Problem / Variant | Time | Space | Notes |
|---|---|---|---|
| All Monotonic Stack problems | O(n) | O(n) | each element pushed/popped at most once |
| Largest Rectangle in Histogram | O(n) | O(n) | sentinel forces all pops |
| Trapping Rain Water | O(n) | O(n) | single pass with stack |
| Sum of Subarray Minimums | O(n) | O(n) | contribution per element |
| k-Duplicate Removal | O(n) | O(n) | single pass, each char processed once |
| Valid Parentheses | O(n) | O(n) | single pass |
| Decode String | O(n) | O(n) | n = output length (can be larger than input) |
| Evaluate RPN | O(n) | O(n) | n = number of tokens |
| Basic Calculator II | O(n) | O(n) | single pass with sign tracking |
| Min Stack | O(1) per op | O(n) | augmented stack stores min alongside |
| Queue via Two Stacks | O(1) amortized | O(n) | transfer is amortized O(1) per element |
| Browser History | O(steps) per op | O(n) | bounded by history length |
| Max Frequency Stack | O(log n) or O(1) | O(n) | depending on implementation |
General rules:
- All stack problems are O(n) time β each element enters and leaves the stack at most once.
- Space is O(n) β worst case all elements are in the stack at once.
- Exception: Decode String β output can be exponentially larger, but stack depth is O(n).
- Min Stack, Queue via Two Stacks β amortized O(1) per operation.
Stack is the backbone of 5+ distinct problem families. Master the monotonic stack first β it covers histogram, rain water, NGE, stock span, and subarray minimums all with ONE template.