Stack & monotonic stack¶
At a glance
Priority: P0 · Complexity: ⅖ · Est. time: 3 h · Phase: 1 · Prereqs: Arrays & hashing You're done when: you can write the "next greater element" monotonic stack from memory, solve Largest Rectangle in Histogram in < 30 min, and use the contribution technique (prev-less / next-less) to count subarray minimums.
Why it matters¶
Stacks model nesting and "most recent unresolved" state: parsers, expression evaluation, undo, call stacks, and DFS. Monotonic stacks answer "nearest greater/smaller to the left/right" for all elements in O(n). That one primitive solves histogram, stock span, temperature and "sum of subarray minimums" problems, which are some of the most common Medium/Hard interview problems. The contribution technique behind them also underpins interval-dominance analytics.
Core concepts¶
Pattern recognition signals¶
| When you see… | Think… |
|---|---|
| Matching brackets, nested structures, "decode k[...]" | Stack of context |
| Expression evaluation (RPN, calculator) | Operand stack (+ operator stack) |
| "next greater / warmer / higher to the right" | Monotonic decreasing stack |
| "next smaller", histogram, "largest rectangle" | Monotonic increasing stack |
| "remove k digits to make smallest", "smallest subsequence" | Greedy + monotonic stack |
| "sum over all subarrays of min/max" | Contribution: prev-less and next-less per element |
| "collisions" (asteroids, cars merging into fleets) | Stack simulating survivors |
| Min in O(1) with push/pop | Stack of (val, current_min) |
Template 1: next greater element (monotonic decreasing), O(n)¶
def next_greater(a):
res = [-1] * len(a)
st = [] # indices; a[st] strictly decreasing
for i, x in enumerate(a):
while st and a[st[-1]] < x: # x resolves everyone smaller
res[st.pop()] = i # (or x, or i - j for distance)
st.append(i)
return res
Flip < to > for next smaller. Iterate right-to-left for "previous" variants, or read the stack top after popping. Circular arrays: iterate 2n indices with i % n.
Template 2: largest rectangle in histogram, O(n)¶
def largest_rectangle(h):
st, best = [], 0 # (start_index, height), heights increasing
for i, x in enumerate(h + [0]): # sentinel 0 flushes the stack
start = i
while st and st[-1][1] >= x:
idx, height = st.pop()
best = max(best, height * (i - idx))
start = idx # current bar can extend back to idx
st.append((start, x))
return best
Template 3: contribution technique (sum of subarray minimums), O(n)¶
def sum_subarray_mins(a, MOD=10**9 + 7):
n = len(a)
left, right = [0] * n, [0] * n
st = []
for i in range(n): # previous strictly less
while st and a[st[-1]] >= a[i]:
st.pop()
left[i] = i - st[-1] if st else i + 1
st.append(i)
st = []
for i in range(n - 1, -1, -1): # next less-or-equal (tie-break!)
while st and a[st[-1]] > a[i]:
st.pop()
right[i] = st[-1] - i if st else n - i
st.append(i)
return sum(a[i] * left[i] * right[i] for i in range(n)) % MOD
Asymmetric tie-break (strict on one side, non-strict on the other) prevents double-counting equal minima.
Template 4: min stack¶
class MinStack:
def __init__(self): self.st = []
def push(self, x): self.st.append((x, min(x, self.st[-1][1]) if self.st else x))
def pop(self): self.st.pop()
def top(self): return self.st[-1][0]
def getMin(self): return self.st[-1][1]
Comparison¶
| Problem | Stack order | What is popped | What pop computes |
|---|---|---|---|
| Daily Temperatures (739) | decreasing temps | colder days | days waited = i − j |
| Online Stock Span (901) | decreasing prices | smaller prices (absorb spans) | span |
| Largest Rectangle (84) | increasing heights | taller bars | area with right boundary i |
| Remove K Digits (402) | increasing digits | larger digits while k > 0 | greedy smallest number |
| Car Fleet (853) | arrival times | faster cars that catch up | number of fleets = stack size |
| Asteroid Collision (735) | survivors | smaller right-movers | survivors |
flowchart TD
A[Need nearest element with property to each index?] -- yes --> B{Greater or smaller?}
B -- "next greater" --> C[Decreasing stack; pop while top < x]
B -- "next smaller / rectangle" --> D[Increasing stack; pop while top > x]
A -- no --> E{Nested structure / undo / parse?}
E -- yes --> F[Plain stack of context frames]
E -- no --> G{Sum over all subarrays of min/max?}
G -- yes --> H[Contribution: left span x right span x value]
Common bugs¶
- Storing values instead of indices, so you can't compute widths or distances.
- Missing the sentinel in histogram, leaving the stack unflushed.
- Tie handling in contribution problems (double counting).
- RPN:
a // bfloors toward −∞ in Python, but the problem wants truncation, so useint(a / b). - Decode String: multi-digit repeat counts (
12[a]) need accumulatingk = k*10 + d.
Resources¶
| Resource | Type | Why this one | Level | Cost |
|---|---|---|---|---|
| NeetCode roadmap: Stack | video | Clear histogram and car-fleet walkthroughs | intermediate | freemium |
| Tech Interview Handbook: Stack | article | Corner cases and must-do list | intermediate | free |
| cp-algorithms: stack/queue modification |
article | Min-stack and min-queue in O(1), with proofs | advanced | free |
| labuladong: monotonic stack |
article | One template for all next-greater variants | intermediate | freemium |
| Hello Interview: coding patterns | interactive | Animated monotonic stack | intermediate | freemium |
| VisuAlgo: linked list / stack / queue | interactive | Visual stack/queue/deque operations | intermediate | free |
Hands-on lab¶
| # | LC | Problem | Difficulty | Set | Key insight |
|---|---|---|---|---|---|
| 1 | 20 | Valid Parentheses | Easy | NC150 | Push opens; closing must match top. |
| 2 | 682 | Baseball Game | Easy | NC250+ | Stack simulation warm-up. |
| 3 | 225 | Implement Stack using Queues | Easy | NC250+ | Rotate the queue after each push. |
| 4 | 155 | Min Stack | Medium | NC150 | Store (val, min_so_far) pairs. |
| 5 | 150 | Evaluate Reverse Polish Notation | Medium | NC150 | Operand stack; int(a / b) truncates toward zero. |
| 6 | 22 | Generate Parentheses | Medium | NC150 | Backtrack with open < n and close < open. |
| 7 | 739 | Daily Temperatures | Medium | NC150 | Monotonic decreasing stack of indices; pop resolves answers. |
| 8 | 853 | Car Fleet | Medium | NC150 | Sort by position desc; stack of arrival times, merge if <= top. |
| 9 | 71 | Simplify Path | Medium | NC250+ | Split on '/', stack for '..'. |
| 10 | 394 | Decode String | Medium | NC250+ | Stack of (prev_string, repeat) on '['. |
| 11 | 735 | Asteroid Collision | Medium | NC250+ | Only right-moving top vs left-moving incoming collide. |
| 12 | 901 | Online Stock Span | Medium | NC250+ | Stack of (price, span); absorb smaller spans. |
| 13 | 402 | Remove K Digits | Medium | NC250+ | Greedy monotonic increasing stack; strip leading zeros. |
| 14 | 907 | Sum of Subarray Minimums | Medium | NC250+ | Contribution technique: prev-less and next-less-or-equal. |
| 15 | 84 | Largest Rectangle in Histogram | Hard | NC150 | Increasing stack; on pop, width spans to new left boundary. |
| 16 | 85 | Maximal Rectangle | Hard | NC250+ | Row-by-row histogram + problem 84. |
Stretch exercise: implement a basic calculator supporting + - * / ( ) with two stacks (shunting-yard). Compare it with a recursive-descent version and note which is easier to extend with operator precedence.
Questions¶
L1 — Recall¶
Q1. Why is a monotonic stack O(n) overall?
Answer
Each index is pushed once and popped at most once, so total pops ≤ n regardless of the inner while. That is aggregate amortised analysis.
Q2. Increasing vs decreasing monotonic stack: which answers 'next greater'?
Answer
Decreasing. Elements wait on the stack until a larger element arrives and pops them. That arrival is their next greater element.
Q3. How does Min Stack achieve O(1) getMin?
Answer
Each entry stores the minimum of the stack at the time of its push. Popping restores the previous minimum automatically. Alternative: an auxiliary stack of minimums pushed only when ≤ the current min.
L2 — Apply¶
Q4. Implement Daily Temperatures.
Q5. Trace Largest Rectangle on [2,1,5,6,2,3].
Answer
Push (0,2). At i=1 (h=1): pop (0,2) giving area 2·1=2, push (0,1). Push (2,5), (3,6). At i=4 (h=2): pop (3,6) giving 6·1=6, pop (2,5) giving 5·2=10, push (2,2). Push (5,3). Sentinel at i=6: pop (5,3) giving 3, pop (2,2) giving 2·4=8, pop (0,1) giving 1·6=6. Best 10.
Q6. Implement Car Fleet.
Answer
A stack of times collapses to a single "slowest ahead" value. O(n log n) for the sort.L3 — Design & trade-offs¶
Q7. Maximal Rectangle in a binary matrix: why reduce to histograms and what is the complexity?
Answer
For each row, compute heights[c] = consecutive 1s ending at this row. Any all-1 rectangle has a bottom row, and in that row's histogram it's a rectangle under the bars. Running LC 84 per row gives O(R·C) total, versus O(R2·C2) or worse for brute force.
Q8. Remove K Digits: why is greedy + monotonic stack optimal?
Answer
Comparing numbers of equal length is decided by the leftmost differing digit. If a digit is larger than its right neighbour, removing it strictly decreases the number at the most significant position possible. The stack keeps digits non-decreasing, removing a "peak" whenever one appears while k > 0. Leftover k removes from the end. Strip leading zeros and return "0" if empty.
Q9. Recursion vs explicit stack for Decode String / nested parsing?
Answer
Recursion mirrors the grammar (each [ recurses) and is concise, but depth equals nesting depth, so the Python recursion limit matters for adversarial input. An explicit stack of (prefix, k) frames is iterative and handles arbitrary depth. In production parsers, prefer iterative or a parser generator with explicit depth limits (a DoS protection, as JSON parsers do).
L4 — Staff-level ambiguity¶
Q10. Stock span over a real-time price feed with 50k symbols. Productionise the monotonic stack.
Answer
Per symbol, keep a stack of (price, span). It's amortised O(1) per tick, but memory per symbol is O(n) in the worst case (monotonically decreasing prices). Bound it by time horizon: evict entries older than the window (the stack becomes a deque). Partition symbols across workers by hash. State must survive restarts: snapshot the stacks plus replay from a Kafka offset. Latency per tick is O(1) amortised, but a single tick can pop many entries, so monitor p99. The algorithm is the same; the issues are memory bounds, partitioning and recovery.
Q11. An engineer proposes computing 'for each shipment, the next shipment with a higher priority' with a nightly O(n^2) SQL self-join over 20M rows. What do you suggest?
Answer
Order by time and run a monotonic stack pass: O(n) in a single-threaded job, or per partition if the "next" relation is scoped (per lane or customer). In SQL, window functions can't express it directly, but a recursive CTE or a UDF/Spark mapPartitions running the stack works. Quantify it: 20M^2/2 = 2·10^14 comparisons versus 2·10^7 operations. Offer to pair on it. Leading with the algorithmic reframe plus a cost estimate is how you influence.
Real-world use cases¶
- Parsers and validators: JSON/XML bracket matching, expression engines in rule platforms.
- Undo/redo in editors (two stacks), and browser history.
- Finance/telemetry: stock span and "time until metric exceeded threshold" (Daily Temperatures) in alerting.
- Skyline and capacity views: largest rectangle under a utilisation curve, meaning the longest window with at least X capacity available.
Pitfalls & anti-patterns¶
- Values vs indices on the stack.
- Missing sentinels or final flush.
- Double counting ties in contribution problems.
- Using recursion for deep nesting without limits.
Checklist¶
- I can write next-greater, histogram and contribution templates from memory
- I can explain the amortised O(n) argument in one sentence
- I solved 84 unaided within 30 min
- I answered all L3 questions out loud in < 3 min each