Stack
Scope — LIFO fundamentals and the canonical stack templates: bracket matching, min-stack, the short monotonic-stack form, explicit-stack traversal and the scope/context ledger. See also: stack_expression_parsing.md — calculators, decode-string and postfix evaluation, the whole expression-parsing family; stack_examples.md — the worked-solution archive behind these templates; monotonic_stack.md — next-greater / previous-smaller / span problems in depth; queue.md — the FIFO counterpart; iterator.md — stack-backed iterators.
LeetCode Problem Lists
Time Complexity
| Data structure | Search | Insert | Delete | Min/Max |
|---|---|---|---|---|
| Stack | O(n) | O(1) | O(1) | O(n) |
Insert = push, Delete = pop, peek — all at the top, all O(1). Min/Max can be made O(1) with an auxiliary min/max-stack (monotonic_stack.md). Space is O(n).
Overview

Stack is a data structure with Last-In-First-Out (LIFO) property. Each operation adds/removes from the top of the stack.
Key Properties
- Complexity: see the Time Complexity table above
- Core Principle: Last element added is the first one removed
- Use Case: Problems involving order reversal, pattern matching, or maintaining context

References
Recognition
| Signal in the statement | Constraint cue | Reach for | Not this if |
|---|---|---|---|
| “remove adjacent duplicates”, “backspace”, “undo the last” | n ≤ 10⁵ → one O(n) pass | Template 1 cancel against the top | a run must reach length k first → [char, count] pairs (stack_examples.md) |
| “valid / balanced parentheses” with several bracket types | n ≤ 10⁴ → O(n) | Template 2 bracket matching | one bracket type only → a balance counter, O(1) space |
| “minimum removals / longest valid” parentheses | n ≤ 10⁵ | Template 2 § LC 1249 index stack | |
| “next greater / warmer / taller”, “days until”, “nearest smaller” | n ≤ 10⁵ → O(n²) scan times out | Template 3 monotonic stack | max of every window of width k → monotonic_queue.md |
| “retrieve the minimum in constant time” alongside push / pop | every op O(1) | Template 4 per-layer aggregate | deletions from anywhere, not LIFO → heap.md with lazy deletion |
| “iteratively”, “without recursion” over a tree | skewed depth up to 10⁵ overflows recursion | Template 5 explicit stack | “level by level” → a queue (bfs.md) |
tab-indented paths, start / end logs, “who is running now” |
n ≤ 10⁴ → O(n) | Template 6 scope ledger | the nesting must be evaluated → stack_expression_parsing.md |
| “basic calculator”, “decode string”, “reverse Polish notation” | stack_expression_parsing.md |
Problem Categories
Ten shapes cover almost every stack question. The Where column says which template below owns the code, or which sheet the worked solutions moved to.
| Category | What the stack holds | LC | Where |
|---|---|---|---|
| Bracket / nesting validation | the openers still owed a closer | 20, 921, 1541, 1614 | Template 2 |
| Bracket repair / measurement | indices of unmatched brackets | 1249, 32, 856 | Template 2 § LC 1249, stack_examples.md |
| Monotonic — next greater / smaller | elements still waiting for an answer | 496, 503, 739, 84, 907, 2104 | Template 3, monotonic_stack.md |
| Monotonic — greedy removal | the best prefix built so far | 402, 316, 1081, 1673 | monotonic_stack.md |
| Monotonic — span accumulation | [value, span] pairs, streaming |
901, 735 | monotonic_stack.md, stack_examples.md |
Stack with [element, count] pairs |
a run-length-compressed prefix | 1047, 1209, 1544 | stack_examples.md |
| O(1) aggregate on a stack | one min / max / delta state per layer |
155, 716, 1381, 895 | Template 4 |
| Expression parsing | operands / deferred terms / open scopes | 224, 227, 772, 394, 150, 682 | stack_expression_parsing.md |
| Scope / context ledger | the enclosing context, keyed by depth | 388, 636, 591, 71 | Template 6 |
| Order reversal / paused traversal | work not yet done | 144, 145, 173, 341, 445 | Template 5 |
Stack Variants Worth Knowing
- Single stack
- Build queue via stack
- LC 232 (use
2 stack)
- LC 232 (use
- Build stack via queue
- Stack with Pair (char, count)
- Store
[element, count]pairs instead of raw elements - LC 1047 (k=2 special case, simple pop)
- LC 1209 (k consecutive duplicates removal)
- LC 1544 (Make The String Great)
- LC 394 (Decode String, stack with count for repetition)
- LC 726 (Number of Atoms)
- Store
Templates & Algorithms
Template Comparison Table
| Template | Stack element | Loop shape | Complexity | When to Use |
|---|---|---|---|---|
| 1 — Cancel against the top | the unmatched prefix | one pass, pop on a match | O(n) / O(n) | adjacent cancellation, undo, backspace |
| 2 — Bracket matching | opener chars | one pass, pop on closer | O(n) / O(n) | validate nesting, >1 bracket type |
| 3 — Monotonic stack | indices | while inside for |
O(n) / O(n) | next greater / smaller / span |
| 4 — Min stack | value + one aggregate state per layer | — | O(1) per op | O(1) getMin() / getMax(); prefix aggregates, path state |
| 5 — Explicit stack | pending nodes | while stack |
O(n) / O(h) | iterative traversal, order reversal |
| 6 — Scope ledger | enclosing context per depth | one pass, trim to depth | O(n) / O(depth) | indented input, start/end events |
Template 1: Cancel Against the Top — LC 1047
Recognise — “remove adjacent duplicates”, “backspace”, “undo”, “two neighbours annihilate”: each new item either cancels the most recent survivor or joins the survivors. n ≤ 10⁵ → one O(n) pass is expected; re-scanning the string after every removal is O(n²).
Key point — the stack is the answer so far: it holds the prefix with every cancellation
already applied, so the only item a new one can interact with is stack[-1].
# python
# LC 1047 - Remove All Adjacent Duplicates In String
# IDEA: the stack holds the reduced prefix; a char equal to the top cancels it, otherwise it joins
# time = O(n), space = O(n)
def removeDuplicates(s):
stack = [] # the reduced prefix, top = last surviving char
for ch in s:
if stack and stack[-1] == ch:
stack.pop() # ch cancels its neighbour; the char below becomes adjacent
else:
stack.append(ch)
return ''.join(stack) # bottom -> top is left -> right order
// java
// LC 1047 - Remove All Adjacent Duplicates In String
// IDEA: the stack holds the reduced prefix; a char equal to the top cancels it, otherwise it joins
// time = O(n), space = O(n)
public String removeDuplicates(String s) {
Deque<Character> stack = new ArrayDeque<>(); // top = last surviving char
for (char ch : s.toCharArray()) {
if (!stack.isEmpty() && stack.peek() == ch) {
stack.pop(); // cancel, and expose the char below
} else {
stack.push(ch);
}
}
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()) {
sb.append(stack.pollLast()); // the bottom is the leftmost survivor
}
return sb.toString();
}
The three operations every template below is built from:
| Operation | Python (list) |
Java (Deque<T> via ArrayDeque) |
Cost |
|---|---|---|---|
| push | stack.append(x) |
stack.push(x) |
O(1) amortised |
| pop | stack.pop() — raises on empty |
stack.pop() — throws on empty |
O(1) |
| peek | stack[-1] — guard with if stack |
stack.peek() — returns null on empty |
O(1) |
Complexity
- Time O(n): the
for ch in sloop runs once per character, and each character is pushed at most once and popped at most once. - Space O(n):
stack.append(ch)keeps every survivor;"abcd"cancels nothing.
Dry run
s = "abbaca"
ch action stack
a push a
b push a b
b cancels top a
a cancels top (empty) <- the cancel exposed 'a', which the next 'a' then cancels
c push c
a push c a -> "ca"
Traps
- Re-scanning after each removal (
while "aa" in s: s = s.replace(...)) is O(n²) on"abcddcba"-style inputs, where each removal exposes a new pair. - In Java,
Stack<Character>iterates bottom → top butArrayDequeiterates top → bottom; building the answer by iterating anArrayDequereverses it. Drain from the bottom withpollLast(). - Java’s
stack.peek() == chis safe only becausechis a primitivechar; two boxedCharacters compared with==compare references.
Not this if — the run must reach length k before it disappears (LC 1209) → push
[char, count] pairs instead of single chars (stack_examples.md).
Drill — LC 1047 · LC 844 (backspace: # pops instead of a match) · LC 1544 (cancel
on same letter, opposite case) · LC 232 (two stacks make a queue) · LC 1209 ([char, count] pairs)
Template 2: Bracket Matching — LC 20 Priority 5 of 5 — Must know — expect it in almost every loop
Recognise — “valid parentheses”, “well-formed”, “balanced”, “properly nested”, with more than one bracket or tag type. n ≤ 10⁴ → one O(n) pass.
Key point — the stack holds exactly the openers still owed a closer, innermost on top; a closer is legal only if it matches the top, because the innermost open scope must close first.
# python
# LC 20 - Valid Parentheses
# IDEA: STACK — push openers, pop-and-verify on closers, stack must end empty
# time = O(n), space = O(n)
class Solution(object):
def isValid(self, s):
pairs = {')': '(', ']': '[', '}': '{'}
stack = []
for c in s:
# closer: needs a non-empty stack AND the matching partner on top
if c in pairs:
if not stack or stack.pop() != pairs[c]:
return False
# opener
else:
stack.append(c)
# leftover openers were never closed
return not stack
// java
// LC 20 - Valid Parentheses
// IDEA: STACK — push openers, pop-and-verify on closers, stack must end empty
// time = O(n), space = O(n)
public boolean isValid(String s) {
// closer -> its matching opener
Map<Character, Character> pairs = new HashMap<>();
pairs.put(')', '(');
pairs.put(']', '[');
pairs.put('}', '{');
Deque<Character> st = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (pairs.containsKey(c)) {
if (st.isEmpty()) {
return false; // a closer with nothing open
}
char top = st.pop(); // unbox to char before comparing
if (top != pairs.get(c)) {
return false; // wrong partner
}
} else {
st.push(c);
}
}
return st.isEmpty(); // leftover openers -> invalid
}
Complexity
- Time O(n): the single
for c in sloop; each character is pushed or popped once. - Space O(n):
stack.append(c)can hold every character, e.g."((((((".
Traps
")"alone: popping before checkingnot stackraises / throws. Check empty first."(]": a closer needs both checks — non-empty and the matching partner on top."(((": the loop never fails, so returningTrueafter it is wrong. The stack must end empty.- Java:
st.pop() != pairs.get(c)with both sides boxedCharactercompares references. Unbox one side into acharfirst.
Not this if — only one bracket type (LC 921, LC 1541, LC 1614) → a running balance counter is enough, O(1) space. The position of the offender is needed (LC 1249, LC 32) → push indices, below.
Drill — LC 20 ⭐ · LC 1614 (max depth = max balance) · LC 921 (counter only) ·
LC 1249 (index stack, below) · LC 856 (stack of partial scores) · LC 32 (index stack with a -1 base)
The bracket family — four twists on the same template (where each is worked is linked in the row):
| LC | Twist | Stack holds |
|---|---|---|
| 1249 | repair, not just validate — below | indices of unmatched ( — or no stack: two counter scans, one per direction |
| 921 | one bracket type only, so a counter suffices — O(1) space (greedy_examples § 7) | nothing (the stack degenerates to its size) |
| 32 | length of the longest valid run (stack_examples § LC 32) | indices plus a -1 base sentinel |
| 856 | fold a score out of the nesting (stack_examples § LC 856) | the partial result of each depth |
Bracket Repair Variant — push the index, not the char (LC 1249) Priority 4 of 5 — High value — a gap here costs you rounds
Key point — LC 20 only answers yes / no, so the stack can hold the opener characters and forget where they were. LC 1249 must erase the offenders, so it needs their positions — push the index of each
(. There is only one bracket type, so nothing has to be compared on a pop: any open(matches any).
Reduces to: Template 2 — the stack holds indices instead of characters.
| LC 20 — validate | LC 1249 — repair | |
|---|---|---|
| Stack holds | opener chars | indices of unmatched ( |
) with an empty stack |
return False |
mark index i for removal, keep scanning |
) with a non-empty stack |
pop, check it is the partner | pop — the pair is kept |
| End of scan | stack must be empty | every index left on the stack is an unmatched ( → remove it too |
| Output | a boolean | s minus the marked positions |
Why it is the minimum. A ) with no ( open before it can never be matched by anything to
its right, and an ( still on the stack at the end has no ) after it. Each removal is forced,
and everything else pairs up, so nothing is removed that did not have to be.
The worked solution (Python and Java, mark-then-rebuild, with its traps) is
string.md Template 7;
the stack-free two counter scans (left→right drops the surplus ), right→left drops the
surplus ( — the follow-up an interviewer asks next) are
stack_examples.md § LC 1249.
Template 3: Monotonic Stack — Next Greater / Smaller — LC 739 Priority 4 of 5 — High value — a gap here costs you rounds
Recognise — “next greater / warmer / taller to the right”, “how many days until”, “nearest smaller on each side”. n ≤ 10⁵ → the O(n²) scan-right-for-each-element times out; O(n) expected.
Key point — the stack holds the indices still waiting for an answer, their values decreasing bottom → top with ties allowed; the first value that beats the top is the answer for every index it pops.
This sheet keeps one short form. The full family (previous-smaller, spans, histogram, subarray min/max sums, and the greedy-removal variants) is monotonic_stack.md’s subject, and its worked solutions are there too.
# python
# IDEA: the stack holds indices still waiting for a warmer day; a warmer `val` answers every one it pops
# time = O(n), space = O(n)
# LC 739 - Daily Temperatures (next greater element, as a distance)
def daily_temperatures(temperatures):
n = len(temperatures)
res = [0] * n # 0 = no warmer day ahead
stack = [] # indices, temperatures non-increasing bottom -> top (equal ones stay)
for i, val in enumerate(temperatures):
while stack and temperatures[stack[-1]] < val:
j = stack.pop()
res[j] = i - j # i is the first warmer day after j
stack.append(i)
return res
// java
// IDEA: the stack holds indices still waiting for a warmer day; a warmer value answers every one it pops
// time = O(n), space = O(n)
// LC 739 - Daily Temperatures (next greater element, as a distance)
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n]; // 0 = no warmer day ahead
Deque<Integer> stack = new ArrayDeque<>(); // indices, temperatures non-increasing (equal ones stay)
// Traverse the array from left to right
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && temperatures[stack.peek()] < temperatures[i]) {
int j = stack.pop();
res[j] = i - j; // i is the first warmer day after j
}
stack.push(i);
}
return res;
}
Flip the comparison to flip the direction:
| Want | Pop while | Read the answer | LC |
|---|---|---|---|
| next greater element / warmer day | top < cur |
when cur pops top |
496, 503, 739 |
| next smaller element, or left+right bounds | top > cur |
when cur pops top |
84, 907, 2104 |
| span back to the last bigger value | top <= cur, accumulating the popped spans |
the accumulated count | 901 |
| circular array | same, over nums * 2 with idx % n |
same | 503 |
Complexity
- Time O(n): the inner
whilelooks nested, but every index is pushed once bystack.append(i)and popped at most once, so the pops total at most n across the whole run. - Space O(n): a strictly decreasing input (
[5, 4, 3, 2, 1]) never pops, so the stack holds all n.
Dry run
temperatures = [73, 74, 75, 71, 69, 72, 76, 73]
i val pops (j -> res[j]) stack (indices)
0 73 - [0]
1 74 0 -> 1 [1]
2 75 1 -> 1 [2]
3 71 - [2, 3]
4 69 - [2, 3, 4]
5 72 4 -> 1, 3 -> 2 [2, 5]
6 76 5 -> 1, 2 -> 4 [6]
7 73 - [6, 7] -> [1, 1, 4, 2, 1, 1, 0, 0]
Traps
- Pushing values instead of indices:
[73, 74]can then say that a warmer day exists but not how far. Push the index; read the value through it. <vs<=: with<=, equal temperatures[70, 70, 71]pop the first 70 on the second and report 1 day, though 70 is not warmer. “Strictly greater” means pop on strictly less.- Indices left on the stack at the end have no answer; they keep the default
0(or-1for LC 496 / 503), so initialisereswith it.
Not this if — the answer is a max or min over a sliding window of fixed width (LC 239) → a monotonic deque, which also expires from the front (monotonic_queue.md).
Drill — LC 739 ⭐ · LC 496 · LC 503 (circular: walk 2n) · LC 901 (online spans) ·
LC 907 (contribution counting) · LC 84 (next smaller on both sides)
Template 4: Min Stack — O(1) getMin — LC 155 Priority 4 of 5 — High value — a gap here costs you rounds
Recognise — “design a stack that supports push, pop, top and retrieving the minimum
element in constant time”. Every operation O(1), up to 3·10⁴ calls.
Key point — min_values[i] == min(stack[0..i]): a second array mirrors the stack layer for
layer and holds the answer for everything at or below that layer, so popping a layer pops its
answer and the layer below already knows its own.
The trick is a second array that mirrors the stack layer for layer, holding not the
elements but the answer to the query for everything at or below that layer. For LC 155 the
query is “minimum”, so min_values holds each layer’s minimum. This is the design-problem
face of a prefix aggregate, and the same idea shows up far outside stacks —
see the classics table at the end of this template.
# python
# LC 155 - Min Stack
# IDEA: 2 ARRAYS — stack holds the elements, min_values holds each layer's minimum
# time = O(1) per operation, space = O(n)
class MinStack(object):
def __init__(self):
self.stack = []
# min_values[i] == min(stack[0..i]) — a STATE per layer, not the elements sorted
self.min_values = []
def push(self, val):
self.stack.append(val)
if not self.min_values:
self.min_values.append(val)
else:
self.min_values.append(min(val, self.min_values[-1]))
def pop(self):
# the popped layer's state leaves with it — no comparison needed
self.min_values.pop()
return self.stack.pop()
def top(self):
return self.stack[-1]
def getMin(self):
# the top layer's state IS the current minimum — O(1)
return self.min_values[-1]
// java
// LC 155 - Min Stack
// IDEA: 2 STACKS — stack holds the elements, minValues holds each layer's minimum
// time = O(1) per operation, space = O(n)
class MinStack {
private final Deque<Integer> stack = new ArrayDeque<>();
private final Deque<Integer> minValues = new ArrayDeque<>(); // one entry per layer
public void push(int val) {
stack.push(val);
if (minValues.isEmpty()) {
minValues.push(val);
} else {
minValues.push(Math.min(val, minValues.peek()));
}
}
public void pop() {
stack.pop();
minValues.pop(); // the layer's state leaves with it
}
public int top() {
return stack.peek();
}
public int getMin() {
return minValues.peek(); // the top layer's state is the answer
}
}
Complexity
- Time O(1) per operation:
pushcombines two numbers inmin(val, self.min_values[-1]);getMinreads one cell,self.min_values[-1]. No operation scans. - Space O(n):
self.min_values.append(...)adds one entry per pushed element.
Dry run
push -2, 0, -3 then pop()
stack = [-2, 0, -3] stack = [-2, 0]
min_values = [-2, -2, -3] min_values = [-2, -2] <- getMin() = -2, nothing recomputed
^ ^ ^
| | min of all three
| min of the first two (0 did not beat -2)
min of the first one
Traps
- One
self.minvariable instead of an array: afterpush 1, push 0, pop, the min must go back to 1, and a single variable has already overwritten it. Restoring it is an O(n) rescan. - The space-saving variant that pushes only new minima must compare with
<=:push 0, push 0, popwith<leaves the min-stack empty while the stack still holds a 0 (see the space-saving variant below). The per-layer array above needs no comparison inpopat all. - Java, space-saving variant:
stack.peek() == minStack.peek()on boxedIntegers compares references, so a pushed128never matches itself. Compare with.equals, or unbox first.
Not this if — the minimum must survive deletions from the middle or arbitrary order (not LIFO) → a heap with lazy deletion (heap.md); the window slides from the front (LC 239) → two such stacks as a queue, or a monotonic deque (classics table below).
Drill — LC 155 ⭐ · LC 1381 (lazy increment per layer) · LC 1026 (the recursion stack as
the stack) · LC 716 (max stack, popMax) · LC 895 (one stack per frequency)
| Approach | push |
pop |
getMin |
Why it falls short |
|---|---|---|---|---|
one self.min variable |
O(1) | O(n) | O(1) | after popping the min, the previous min has to be rescanned |
heap (heapq) |
O(log n) | O(log n) with lazy deletion | O(1) | pays a log for an ordering the stack never uses |
{value: count} map |
O(1) | O(1) | O(n) | min(counts) is a scan (this is min-stack.py’s V0-1) |
per-layer min_values |
O(1) | O(1) | O(1) | the answer for the current top was fixed when the top was pushed |
The general form — one aggregate per layer
Invariant: min_values[i] = min(stack[0], stack[1], ..., stack[i])
-> min_values is NOT the elements sorted; it is a STATE per layer
push(v): min_values.append(min(v, min_values[-1])) # new layer, new state
pop(): min_values.pop() # the popped layer's state goes with it
getMin(): min_values[-1] # the top layer's state IS the answer
Three consequences fall straight out of the invariant:
- The two arrays are always the same length — every
pushappends to both, everypopremoves from both. Noifinpop(), no comparing the popped value against the min:min_values[-1]is a state that belongs to the layer, not a copy of an element, so popping the layer is the whole job. That is why the template above does not need thestack.pop() == mins[-1]check that the space-saving variant needs. - Duplicates are free.
push(0); push(0); pop()leavesmin_values = [0], because each0got its own layer. The variant that stores only new minima has to write<=to get this right — the classic LC 155 bug (see the space-saving variant below). - Any aggregate that is a function of the prefix works the same way —
max, a running sum, a running GCD, a pending delta (LC 1381), a frequency count (LC 895). Replacemin(...)inpushand nothing else changes.
The same invariant fits in one stack of (value, min_so_far) tuples — one structure instead
of two, same bounds:
# python
# LC 155 - Min Stack, one stack of (value, min_so_far) tuples
# IDEA: each layer carries its own minimum next to its value
# time = O(1) per operation, space = O(n)
class MinStack(object):
def __init__(self):
self.stack = []
def push(self, x):
if not self.stack:
self.stack.append((x, x))
else:
self.stack.append((x, min(x, self.stack[-1][1])))
def pop(self):
self.stack.pop()
def top(self):
return self.stack[-1][0]
def getMin(self):
return self.stack[-1][1]
Space-saving variant — a second stack of minima only
The minima stack does not need an entry per layer: push onto it only when the new value is a
new minimum or ties it, and pop from it only when the popped value equals its top. Its values
are then non-increasing bottom → top and its top is still the live minimum. It saves space when
most pushes do not lower the minimum, and it is the version interviewers most often see written,
so know its one trap: the comparison on push must be <=.
# python
# LC 155 - Min Stack, auxiliary non-increasing stack
# IDEA: mins holds only the values that were a minimum when pushed; '<=' keeps duplicate minima
# time = O(1) per operation, space = O(n) worst case
class MinStack(object):
def __init__(self):
self.stack = []
self.mins = [] # non-increasing bottom -> top; top = current minimum
def push(self, val):
self.stack.append(val)
if not self.mins or val <= self.mins[-1]:
self.mins.append(val) # '<=': a second copy of the minimum gets its own entry
def pop(self):
if self.stack.pop() == self.mins[-1]:
self.mins.pop() # the minimum is leaving, so its entry leaves too
def top(self):
return self.stack[-1]
def getMin(self):
return self.mins[-1]
// java
// LC 155 - Min Stack, auxiliary non-increasing stack
// IDEA: mins holds only the values that were a minimum when pushed; '<=' keeps duplicate minima
// time = O(1) per operation, space = O(n) worst case
class MinStack {
private final Deque<Integer> stack = new ArrayDeque<>();
private final Deque<Integer> mins = new ArrayDeque<>(); // non-increasing
public void push(int val) {
stack.push(val);
if (mins.isEmpty() || val <= mins.peek()) {
mins.push(val);
}
}
public void pop() {
int v = stack.pop(); // unbox before comparing
if (v == mins.peek()) {
mins.pop();
}
}
public int top() {
return stack.peek();
}
public int getMin() {
return mins.peek();
}
}
The classic bug is pushing a new minimum only on strict <: push(0); push(0); pop() removes
the single stored 0 from mins, and getMin() then fails on an empty stack although a 0 is
still in the stack.
Why O(1) — the state was computed at push time
getMin() reads one array cell. There is no scan, because the work was moved to push,
and push only has to combine two numbers: the new value and the answer for the layer
below. That is the whole argument — each operation touches the top of two arrays and
nothing else.
The point of keeping every layer’s state instead of one self.min variable is pop().
A single variable can be lowered when a smaller value arrives, but cannot be raised
back when that value leaves — the earlier minimum was overwritten. The array remembers it,
because it was never overwritten; it was one layer down the whole time.
The same idea beyond stacks — the classics Priority 4 of 5 — High value — a gap here costs you rounds
The invariant needs a container that only changes at one end, so that “the state one layer down” is still valid when the top goes away. Four things in the LC catalogue behave like that. Read the table by the first column: find what is playing the stack in your problem, and the aggregate you need to carry per layer follows.
| What plays the stack | Per-layer state | Query it answers | Classic LC |
|---|---|---|---|
| an explicit stack | min / max so far |
getMin() / getMax() in O(1) |
155 Min Stack, 716 Max Stack (peekMax) |
| a pending delta for everything below | bulk increment(k, val) in O(1) |
1381 Design a Stack With Increment Operation | |
| one stack per frequency level | pop the most frequent in O(1) | 895 Maximum Frequency Stack | |
| two stacks forming a queue (LC 232) | min / max so far in each stack |
window min / max in amortised O(1), without a monotonic deque | 239 Sliding Window Maximum, 1438 Longest Continuous Subarray With Absolute Diff ≤ Limit |
| an array that only grows — index = layer | prefix min / max / sum / product |
“best value at or before i” for every i |
121 Best Time to Buy and Sell Stock, 2016 Maximum Difference Between Increasing Elements, 42 Trapping Rain Water, 238 Product of Array Except Self, 303 Range Sum Query, 769 Max Chunks To Make Sorted, 915 Partition Array into Disjoint Intervals, 1477 Find Two Non-overlapping Sub-arrays Each With Target Sum |
| the recursion stack — the root-to-node path | (lo, hi) / max / prefix value along the path |
a per-node answer about its ancestors | 1026 Maximum Difference Between Node and Ancestor, 98 Validate Binary Search Tree, 1448 Count Good Nodes in Binary Tree, 129 Sum Root to Leaf Numbers |
Two rules decide how much of the state to keep:
- Keep the whole array when a later query asks about a layer that is not the top.
LC 42 needs
left_max[i]for everyion the second pass; LC 238 needs every prefix product; LC 769 / 915 compare a prefix max against a suffix min at each split. One variable cannot answer for a layer it has already moved past. - Collapse it to one variable when only the top is ever queried and nothing pops.
LC 121 asks “min so far” once per index and never goes back, so
min_valuesis a singlelo— the array’s last cell, kept and the rest thrown away. LC 155 cannot do this becausepop()does go back.
# LC 239 (as a min queue) / LC 1438 - a queue built from two min stacks
# IDEA: a queue is two stacks (LC 232). Give each stack its per-layer min and the
# queue's min is the smaller of the two tops — no monotonic deque needed.
# Swap min -> max for LC 239 itself; run one of each for LC 1438.
# time = O(1) amortised per operation, space = O(n)
class MinQueue(object):
def __init__(self):
self.inbox, self.outbox = [], [] # each entry: (value, min_so_far)
@staticmethod
def _push(st, v):
st.append((v, v if not st else min(v, st[-1][1])))
def push(self, v):
self._push(self.inbox, v)
def pop(self):
if not self.outbox: # refill: each element moves once
while self.inbox:
self._push(self.outbox, self.inbox.pop()[0])
return self.outbox.pop()[0]
def getMin(self):
return min(st[-1][1] for st in (self.inbox, self.outbox) if st)
# LC 1026 - Maximum Difference Between Node and Ancestor
# IDEA: the recursion stack IS the stack; (lo, hi) is that layer's aggregate over the
# root-to-node path. Returning from the call pops it — no bookkeeping at all.
# time = O(n), space = O(h)
def maxAncestorDiff(root):
def dfs(node, lo, hi):
if not node:
return hi - lo
lo, hi = min(lo, node.val), max(hi, node.val)
return max(dfs(node.left, lo, hi), dfs(node.right, lo, hi))
return dfs(root, root.val, root.val)
Where the depth lives for each row: the LC 1381 delta trick in
design_examples.md § LC 1381,
the deque form of LC 239 in
monotonic_queue.md,
prefix arrays in prefix_sum.md, LC 121 in
stock_trading.md, and the (lo, hi) bounds pattern in
bst_advanced.md.
Template 5: Explicit Stack — Iterative Traversal — LC 144, LC 145 Priority 4 of 5 — High value — a gap here costs you rounds
Recognise — “iteratively”, “without recursion”, “follow-up: recursive solution is trivial, could you do it iteratively?”, or a tree / list deep enough (n up to 10⁵ on a skewed tree) to overflow the call stack.
Key point — the stack holds the work not yet done, and a stack hands work back in reverse
order of arrival: push right before left and left is processed first. Postorder is
preorder run as root → right → left, reversed.
The paused form of the same stack — an iterator that must stop between elements — is LC 173 in bst_advanced.md § Template 5b and LC 341 in iterator.md § 2-2, and the wider family is iterator.md.
# python
# LC 144 - Binary Tree Preorder Traversal
# IDEA: EXPLICIT STACK — push RIGHT before LEFT so LEFT is popped first
# time = O(n), space = O(h)
class Solution(object):
def preorderTraversal(self, root):
if not root:
return []
res, stack = [], [root]
while stack:
node = stack.pop()
res.append(node.val)
# right first -> left ends up on TOP
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return res
# LC 145 - Binary Tree Postorder Traversal
# IDEA: preorder variant (root -> RIGHT -> LEFT), then REVERSE => left-right-root
# time = O(n), space = O(h)
class Solution(object):
def postorderTraversal(self, root):
if not root:
return []
res, stack = [], [root]
while stack:
node = stack.pop()
res.append(node.val)
# mirrored push order compared with preorder
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return res[::-1] # root-right-left -> left-right-root
// java
// LC 144 - Binary Tree Preorder Traversal
// IDEA: EXPLICIT STACK — push RIGHT before LEFT so LEFT is popped first
// time = O(n), space = O(h)
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) {
return res;
}
Deque<TreeNode> st = new ArrayDeque<>();
st.push(root);
while (!st.isEmpty()) {
TreeNode node = st.pop();
res.add(node.val);
// right pushed FIRST, so left is popped FIRST
if (node.right != null) {
st.push(node.right);
}
if (node.left != null) {
st.push(node.left);
}
}
return res;
}
// LC 145 - Binary Tree Postorder Traversal
// IDEA: preorder with LEFT/RIGHT swapped (root-right-left), then REVERSE
// time = O(n), space = O(h)
public List<Integer> postorderTraversal(TreeNode root) {
LinkedList<Integer> res = new LinkedList<>();
if (root == null) {
return res;
}
Deque<TreeNode> st = new ArrayDeque<>();
st.push(root);
while (!st.isEmpty()) {
TreeNode node = st.pop();
res.addFirst(node.val); // addFirst == "append, then reverse", done incrementally
if (node.left != null) {
st.push(node.left);
}
if (node.right != null) {
st.push(node.right);
}
}
return res;
}
Complexity
- Time O(n):
while stackpops each node exactly once, and each pop pushes at most two children. - Space O(h) on the stack: it holds at most the pending siblings along one root-to-leaf
path — O(log n) when balanced, O(n) on a skewed tree. The
res[::-1]copy for postorder is output, O(n), not working space.
Traps
- Pushing
leftbeforerightin preorder givesroot → right → left; on[1, 2, 3](2 left, 3 right) it returns[1, 3, 2]. root = Nonepushed onto the stack crashes onnode.val; return[]first.- Postorder’s reversal trick yields the values; it cannot be used when each node must be
processed after its children with side effects in that order (e.g. freeing them) — use a
(node, visited)flag instead.
Not this if — the problem wants nodes level by level (“level order”, “right side view”, “minimum depth”) → a FIFO queue, not a stack (bfs.md).
Drill — LC 144 · LC 145 · LC 94 (inorder: walk left, pop, go right) · LC 173 (paused inorder as an iterator) · LC 341 (paused traversal of nested lists)
Template 6: Scope / Context Ledger — LC 388, LC 636 Priority 5 of 5 — Must know — expect it in almost every loop
Recognise — tab-indented or nested input (“file system as a string”, “tags”), or
start / end event logs (“exclusive time”, “call stack”); any “who is my parent right now?”
question answered in one left-to-right scan. Input length ≤ 10⁴ → O(n).
Key point — stack index == nesting depth: stack[d] holds the context accumulated at depth
d (a path prefix, a running function), so the parent of whatever is being read is always
stack[depth] / stack[-1].
This is the pattern behind the highest-frequency Google stack questions, and it is not bracket matching: the “brackets” are implicit (indentation depth, start/end log events).
# python
# LC 388 - Longest Absolute File Path
# IDEA: SCOPE STACK indexed by DEPTH — stack[d] = length of the path prefix at depth d
# time = O(n), space = O(depth)
class Solution(object):
def lengthLongestPath(self, input):
res = 0
stack = [0] # stack[d] = prefix length at depth d (trailing '/' included)
for line in input.split('\n'):
name = line.lstrip('\t')
depth = len(line) - len(name) # number of leading tabs
# we left deeper scopes -> trim the stack back to this depth
while len(stack) > depth + 1:
stack.pop()
if '.' in name:
# file: measure, never push (a file has no children)
res = max(res, stack[depth] + len(name))
else:
# dir: becomes the prefix for depth+1, +1 for the '/'
stack.append(stack[depth] + len(name) + 1)
return res
// java
// LC 388 - Longest Absolute File Path
// IDEA: SCOPE STACK indexed by DEPTH — stack.get(d) = length of the path prefix at depth d
// time = O(n), space = O(depth)
public int lengthLongestPath(String input) {
int res = 0;
// stack.get(d) = length of "dir1/dir2/.../" for the current branch at depth d,
// trailing '/' included -> the index in the list IS the nesting depth
List<Integer> stack = new ArrayList<>();
stack.add(0); // depth 0 has an empty prefix
for (String line : input.split("\n")) {
// depth = number of leading '\t'
int depth = 0;
while (depth < line.length() && line.charAt(depth) == '\t') {
depth++;
}
String name = line.substring(depth);
// we LEFT the previous deeper scopes -> pop back to `depth`
while (stack.size() > depth + 1) {
stack.remove(stack.size() - 1);
}
if (name.contains(".")) {
// a FILE never becomes a parent -> just measure it
res = Math.max(res, stack.get(depth) + name.length());
} else {
// a DIRECTORY becomes the context of depth+1 (+1 for the '/')
stack.add(stack.get(depth) + name.length() + 1);
}
}
return res;
}
Complexity
- Time O(n):
for line in input.split('\n')touches each character a constant number of times; the trimwhilepops each pushed directory at most once. - Space O(depth):
stack.append(...)grows only while the input nests deeper, one entry per open directory.
Dry run
"dir\n\tsubdir2\n\t\tfile.ext"
line depth stack after res
"dir" 0 [0, 4] ("dir/")
"subdir2" 1 [0, 4, 12] ("dir/subdir2/")
"file.ext" 2 [0, 4, 12] 12 + 8 = 20
Traps
- Forgetting the trim:
"a\n\tb\nc\n\td.txt"would measured.txtundera/b/instead ofc/. - Pushing a file: a file never has children, so a later sibling would read the wrong prefix.
- Forgetting the
+ 1for/:"dir\n\tfile.ext"returns 11 instead of 12. - No file at all (
"a"): the answer is0, not the longest directory path.
Not this if — the nesting is written with explicit brackets and must be validated → Template 2; it must be evaluated (calculators, decode string) → stack_expression_parsing.md.
Drill — LC 388 · LC 71 (.. pops the parent dir) · LC 636 · LC 591 (open-tag stack plus CDATA rules)
Variant: overwrite the depth slot instead of popping — LC 388
Key point — the input is a pre-order listing, so a line at depth
dreads onlypath_len[d], and the last write to that slot was made by its own parent. Deeper stale slots are never read before they are rewritten, so the ledger is really the last write per depth.
Reduces to: Template 6 — with the trim
loop dropped. This is the form README files LC 388 under Hash_table. Same O(n).
# python
# LC 388 - Longest Absolute File Path
# IDEA: HASH MAP by DEPTH — path_len[d] = prefix length for children at depth d; overwrite, never pop
# time = O(n), space = O(depth)
class Solution(object):
def lengthLongestPath(self, input):
res = 0
path_len = {0: 0} # depth-0 entries have an empty prefix
for line in input.split('\n'):
name = line.lstrip('\t')
depth = len(line) - len(name)
if '.' in name:
res = max(res, path_len[depth] + len(name))
else:
# overwrite the slot for depth+1 — a stale deeper slot is rewritten before it is read
path_len[depth + 1] = path_len[depth] + len(name) + 1
return res
// java
// LC 388 - Longest Absolute File Path
// IDEA: HASH MAP by DEPTH — pathLen[d] = prefix length for children at depth d; overwrite, never pop
// time = O(n), space = O(depth)
public int lengthLongestPath(String input) {
int res = 0;
Map<Integer, Integer> pathLen = new HashMap<>();
pathLen.put(0, 0); // depth-0 entries have an empty prefix
for (String line : input.split("\n")) {
int depth = line.lastIndexOf('\t') + 1; // tabs are leading and contiguous
String name = line.substring(depth);
if (name.contains(".")) {
res = Math.max(res, pathLen.get(depth) + name.length());
} else {
// overwrite the slot for depth+1 — a stale deeper slot is rewritten before it is read
pathLen.put(depth + 1, pathLen.get(depth) + name.length() + 1);
}
}
return res;
}
Exclusive Time of Functions — LC 636
Key point — the stack is the call stack the problem describes, so its top is the function on the CPU right now;
previs the first time unit nobody has been credited for yet. Every log line closes the slice[prev, …], credits it to the old top, then moves the cursor.
Reduces to: Template 6 — the context is
“which function is running”, and an end event pops it.
# python
# LC 636 - Exclusive Time of Functions
# IDEA: SCOPE STACK of function ids — the RUNNING function is always the stack top
# time = O(n), space = O(n)
class Solution(object):
def exclusiveTime(self, n, logs):
res = [0] * n
stack = [] # ids of functions currently RUNNING (top = executing now)
prev = 0 # start of the current time slice
for log in logs:
fid, typ, t = log.split(':')
fid, t = int(fid), int(t)
if typ == 'start':
# the caller ran on [prev, t) before being preempted
if stack:
res[stack[-1]] += t - prev
stack.append(fid)
prev = t
else:
# 'end' timestamp is INCLUSIVE -> [prev, t] => + 1
res[stack.pop()] += t - prev + 1
prev = t + 1
return res
// java
// LC 636 - Exclusive Time of Functions
// IDEA: SCOPE STACK of function ids — the RUNNING function is always the stack top
// time = O(n), space = O(n)
public int[] exclusiveTime(int n, List<String> logs) {
int[] res = new int[n];
Deque<Integer> stack = new ArrayDeque<>(); // ids of functions currently RUNNING
int prev = 0; // timestamp where the current "run slice" started
for (String log : logs) {
String[] p = log.split(":"); // {id, "start"|"end", timestamp}
int id = Integer.parseInt(p[0]);
int t = Integer.parseInt(p[2]);
if (p[1].equals("start")) {
// the caller (stack top) ran during [prev, t) -> credit it, then it is PREEMPTED
if (!stack.isEmpty()) {
res[stack.peek()] += t - prev;
}
stack.push(id);
prev = t;
} else {
// "end at t" is INCLUSIVE -> the slice is [prev, t], hence `+ 1`; the caller resumes at t + 1
res[stack.pop()] += t - prev + 1;
prev = t + 1;
}
}
return res;
}
n = 2, logs = ["0:start:0","0:start:2","0:end:5","1:start:6","1:end:6","0:end:7"]
log credit (to stack top) stack prev res
0:start:0 (stack empty) [0] 0 [0, 0]
0:start:2 res[0] += 2 - 0 = 2 [0, 0] 2 [2, 0] <- recursive call, same id
0:end:5 res[0] += 5 - 2 + 1 = 4 [0] 6 [6, 0]
1:start:6 res[0] += 6 - 6 = 0 [0, 1] 6 [6, 0] <- zero-length slice, harmless
1:end:6 res[1] += 6 - 6 + 1 = 1 [0] 7 [6, 1]
0:end:7 res[0] += 7 - 7 + 1 = 1 [] 8 [7, 1] -> answer [7, 1]
Why
+ 1only onend.start:tmeans the beginning of unitt,end:tmeans the end of unitt. So a start boundary sits attand an end boundary sits att + 1. Convertend:tto boundaryt + 1the moment you parse it and both branches become the same half-open[prev, boundary)subtraction — the form below. SameO(n); the reason to know it is that the off-by-one disappears instead of being remembered.
# python
# LC 636 - Exclusive Time of Functions
# IDEA: normalise every log to a half-open BOUNDARY (end:t -> t + 1), so one formula credits both
# time = O(n), space = O(n)
class Solution(object):
def exclusiveTime(self, n, logs):
res = [0] * n
stack = []
prev = 0 # boundary where the current slice began
for log in logs:
fid, typ, t = log.split(':')
fid, t = int(fid), int(t)
boundary = t if typ == 'start' else t + 1
if stack:
# the stack top ran on [prev, boundary)
res[stack[-1]] += boundary - prev
if typ == 'start':
stack.append(fid)
else:
stack.pop()
prev = boundary
return res
Pitfalls specific to LC 636:
- Credit before you push or pop. The slice that just ended belongs to the old top; pushing first hands the caller’s time to the callee.
- One frame per call, not one entry per id. A function can call itself (example 2), so a
{id: start_time}map overwrites the outer call. Two frames with the same id is correct. previs one global cursor, not a per-function start time. The alternative keeps[id, resume_time]frames and rewrites the caller’sresume_time = t + 1after each pop — the same idea with the cursor stored on the frame, and one more line to forget.- A zero-length slice is fine.
1:start:6right afterprev = 6credits0; no special case.
Drill Path
| Stage | Problems (easy → hard) | What it adds |
|---|---|---|
| Core ⭐ | LC 20 ok · LC 155 again · LC 739 ok | bracket matching, a per-layer aggregate, the monotonic stack — one each |
| Practice | LC 1047 no verdict · LC 144 no verdict · LC 232 again · LC 1249 no verdict · LC 636 again · LC 388 again | cancel against the top, explicit traversal, two stacks as a queue, an index stack, the scope ledger twice |
| Stretch | LC 895 no verdict · LC 32 again · LC 591 no verdict | one stack per frequency; the -1 base sentinel; a full tag grammar |
Summary & Quick Reference
Decision Table — Which Stack Pattern?
| Problem Type | Pattern | Key Idea | Examples |
|---|---|---|---|
| Find next greater/smaller element | Monotonic Stack | Maintain increasing/decreasing order | LC 496, 503, 739 |
| Remove adjacent duplicates | Stack with Pair [element, count] | Track counts, pop when k reached | LC 1047, 1209, 1544 |
| Decode strings with brackets | Stack with Count | Use pairs for nested repetitions | LC 394, 726 |
| Arithmetic expressions | Stack with Operators | Handle precedence and evaluation | LC 224, 227 |
| Remove k digits for min number | Greedy + Monotonic | Pop larger digits when beneficial | LC 402 |
| Lexicographically smallest with duplicates | Monotonic + Last Occurrence | Greedy removal with “appears later” check | LC 316, 1081 |
| Streaming/online frequency | Stack with Span Pairs | Accumulate counts in pairs | LC 901 |
| FIFO from LIFO | Two Stacks | Use input/output stacks for queue | LC 232 |
| O(1) min / max alongside push / pop | Per-layer aggregate | min_values[i] = min(stack[0..i]), popped with its layer |
LC 155, 716, 1381 |
| Balanced-bracket validation | Bracket Matching | Push openers, pop-and-verify on closers | LC 20, 1249, 32 |
| Nesting context (indent, start/end events) | Scope / Context Ledger | stack[depth] = the enclosing context |
LC 388, 636, 591 |
| Reverse a forward-only sequence | Push-all, then pop | Popping yields reverse order | LC 445, 234, 143 |
| Iterative traversal / lazy iterator | Explicit Stack | Stack holds work not yet done | LC 144, 145, 173, 341 |
| Postfix / RPN evaluation | Operand Stack | Operator pops two, pushes the result | LC 150, 682 |
How to read: Find your problem goal in the leftmost column, then use the pattern and examples as a starting point.
Pattern Complexity
Per-pattern cost. The per-operation cost of the structure itself is the Time Complexity table at the top.
| Pattern | Time | Space | Why |
|---|---|---|---|
| Bracket matching | O(n) | O(n), or O(1) with one bracket type | one pass, each char pushed at most once |
| Monotonic stack | O(n) | O(n) | each element pushed once, popped at most once |
Greedy removal (k drops) |
O(n) | O(n) | same amortised argument; k bounds the pops |
[element, count] pairs |
O(n) | O(n) | the stack is a run-length encoding of the prefix |
| Min stack | O(1) per op | O(n) | one auxiliary entry per push |
| Explicit-stack traversal | O(n) | O(h) | only the current root-to-node path is pending |
| Scope ledger | O(n) | O(depth) | one entry per open scope |
| Expression parsing | O(n) | O(n) | stack depth = nesting depth |
Common Traps
- Popping on
<=when the problem wants strictly greater (LC 496 / 739): with[2, 2, 3], the second 2 pops the first and hands it the answer 2 instead of 3. Pop only while the top is strictly smaller. - A run count kept outside the stack (LC 1209): with
s = "deeedbbcccbdaa", k = 3, deletingeeemakes the twods adjacent; the count has to live on the stack as[char, count]so it survives the pop. - Leftover
kafter a greedy-removal scan (LC 402):num = "12345", k = 2never pops, so cut the lastkdigits after the loop;num = "10200", k = 1leaves a leading zero to strip ("200"). - Empty-stack check first:
s = "]"(LC 20) pops an empty stack; everypop()/peek()driven by a closer needs!stack.isEmpty()in front of it. Charactervscharin Java:!=on two boxedCharacters compares references. It happens to work for ASCII (values 0–127 are cached) and breaks on the first non-ASCII character — unbox, or use.equals.- Integer division truncates toward zero (LC 150):
-3 / 2must be-1, but Python’s//floors to-2; useint(a / b). - Push children in reverse when a stack must yield left-to-right order: pushing left before right turns the preorder of
[1, 2, 3]into1, 3, 2.
Where the Rest Lives
| Looking for | File |
|---|---|
| Calculators (LC 224 / 227 / 772), decode string (LC 394), postfix (LC 150) | stack_expression_parsing.md |
| Worked solutions for the problems named above | stack_examples.md |
| Next-greater / previous-smaller / histogram theory | monotonic_stack.md |
| Iterator design (LC 173, 341, 284) | iterator.md |
| FIFO, deque, monotonic queue | queue.md, monotonic_queue.md |