A learning sheet β a stack remembers what's unresolved and settles it in reverse. The monotonic variant turns O(nΒ²) "nearest larger/smaller" scans into one pass.
The plain idea: a stack is "things I've seen but can't finish yet," resolved last-in-first-out. Openers wait for closers; a sub-expression waits until its ]; the call stack waits for each return.
The powerful idea β the monotonic stack: keep the stack sorted (always increasing, or always decreasing). When a newcomer would break that order, pop the elements it dominates and resolve each one against the newcomer. Because every element is pushed once and popped once, an apparently nested loop is actually O(n).
< or <= (how to treat ties)? (3) store indices (if you need distance/width) or values? (4) what do you compute at the moment of popping β that pop is when an element's answer becomes known.For each day, how many days until a warmer one? temps = [73,74,75,71,69,72,76,73].
i, scan right for the first warmer day β O(nΒ²), re-scanning the same cold stretch over and over.temps[top] < temps[i]: pop, set ans[top] = i β top. Then push i.O(n).| i | temp | pops (resolve) | stack (indices) after |
|---|---|---|---|
| 2 | 75 | pop 1 β ans[1]=1 | [2] |
| 5 | 72 | pop 4 (ans=1), pop 3 (ans=2) | [2, 5] |
| 6 | 76 | pop 5 (ans=1), pop 2 (ans=4) | [6] |
Answer: [1,1,4,2,1,1,0,0]. Days never resolved (no warmer ahead) keep their default 0.
| Ask | If yes β | Pattern |
|---|---|---|
| Nesting/matching β brackets, balanced, undo? | push openers, pop on closers | Matching (Β§4.1) |
| Need to resume context after a sub-part (multipliers, expressions)? | stack of partial state | Parse / evaluate (Β§4.2) |
| For each element, the nearest larger/smaller on one side? | monotonic stack | Monotonic Β· next (Β§4.3) |
| Span/area bounded by nearest smaller/larger (histogram, rain)? | monotonic + width math | Monotonic Β· area (Β§4.4) |
| Greedily build an optimal sequence, dropping earlier worse picks? | monotonic greedy | Monotonic Β· greedy (Β§4.5) |
O(n).| Words in the prompt | Reach for |
|---|---|
| "valid/balanced parentheses", "matching brackets", "nested", "undo" | Matching Β§4.1 |
| "decode string", "basic calculator", "evaluate expression / RPN", "simplify path" | Parse Β§4.2 |
| "next greater / smaller element", "days until warmer", "stock span" | Monotonic next Β§4.3 |
| "largest rectangle", "maximal area", "trapping rain water" | Monotonic area Β§4.4 |
| "remove k digits", "smallest/largest subsequence", "remove duplicate letters" | Monotonic greedy Β§4.5 |
Β§4.3β4.5 are the monotonic family β same engine (push-once/pop-once), different "what you compute on pop." Each pattern: a one-liner, a π invariant, a template, and a π€ interview lens.
βPush what's open; a closer must match the most recent opener.β
Setup: the only thing that can close an opener is the nearest unclosed one β pure LIFO.
function isValid(s: string): boolean {
const stack: string[] = [];
const match: Record<string, string> = { ')': '(', ']': '[', '}': '{' };
for (const c of s) {
if (c in match) { if (stack.pop() !== match[c]) return false; } // closer: must match top
else stack.push(c); // opener: wait
}
return stack.length === 0; // nothing left unclosed
}
βHit a sub-part β stash what you were doing; finish it; pop and resume.β
Setup: when you descend into a bracketed/nested piece, push the surrounding state; when it ends, pop and splice the result back.
// Decode String: "3[a2[c]]" -> "accaccacc"
function decodeString(s: string): string {
const numStack: number[] = [], strStack: string[] = [];
let cur = '', num = 0;
for (const c of s) {
if (c >= '0' && c <= '9') num = num * 10 + (+c); // multi-digit
else if (c === '[') { numStack.push(num); strStack.push(cur); num = 0; cur = ''; } // descend
else if (c === ']') { const k = numStack.pop()!; cur = strStack.pop()! + cur.repeat(k); } // resume
else cur += c;
}
return cur;
}
βA newcomer resolves everyone it dominates, all at once.β
Setup: maintain a stack of indices whose answers aren't known yet, kept monotonic. The newcomer pops (and answers) every waiting element it beats.
O(n).// Next Greater Element β res[i] = next value to the right greater than a[i], else -1
function nextGreater(a: number[]): number[] {
const res = new Array(a.length).fill(-1);
const stack: number[] = []; // indices; a[stack] strictly decreasing
for (let i = 0; i < a.length; i++) {
while (stack.length && a[stack[stack.length - 1]] < a[i]) {
res[stack.pop()!] = a[i]; // a[i] is the answer for the popped index
}
stack.push(i);
}
return res; // anything left has no greater β stays -1
}
< vs <= (ties), indices vs values, and the amortized O(n). Β· Escalation: next greater β Daily Temperatures β "Next Greater Element II" (circular: iterate 2n with i % n) β online Stock Span.βWhen a bar pops, that's the instant its full width is known.β
Setup: an increasing stack of indices. When a shorter bar arrives, each taller bar it pops can't extend further right β so compute its rectangle at the pop: width = (current index) β (index below it on the stack) β 1.
// Largest Rectangle in Histogram
function largestRectangle(h: number[]): number {
const stack: number[] = []; // indices; heights increasing
let best = 0;
for (let i = 0; i <= h.length; i++) {
const cur = i === h.length ? 0 : h[i]; // sentinel 0 flushes the stack
while (stack.length && h[stack[stack.length - 1]] >= cur) {
const height = h[stack.pop()!];
const left = stack.length ? stack[stack.length - 1] : -1; // nearest shorter on left
best = Math.max(best, height * (i - left - 1)); // width spans (left, i)
}
stack.push(i);
}
return best;
}
i β left β 1 and the trailing sentinel that flushes. Β· Escalation: histogram β "Maximal Rectangle" in a 0/1 matrix (run this per row) β "Trapping Rain Water" (bounded by nearest taller on both sides).βBuild the result greedily; pop a worse predecessor while you still can.β
Setup: construct an output by pushing characters; before pushing, pop earlier characters that are "worse" than the newcomer if your removal budget allows. The stack is the answer-in-progress.
// Remove K Digits β smallest number after deleting k digits
function removeKdigits(num: string, k: number): string {
const stack: string[] = [];
for (const d of num) {
while (k > 0 && stack.length && stack[stack.length - 1] > d) { stack.pop(); k--; } // drop bigger predecessors
stack.push(d);
}
while (k-- > 0) stack.pop(); // leftover budget β trim the tail (digits ascending)
return stack.join('').replace(/^0+/, '') || '0'; // strip leading zeros
}
k tail trim, and leading zeros. Β· Escalation: remove k digits β "Remove Duplicate Letters / Smallest Subsequence" (pop only if the char reappears later, and isn't already in the stack) β "Create Maximum Number."| Pattern | Time | Space | Note |
|---|---|---|---|
| Matching / Parse | O(n) | O(n) | stack depth β€ nesting depth |
| Monotonic (next / area / greedy) | O(n) | O(n) | each element pushed once, popped once |
while can pop many elements in one step, which looks like O(nΒ²) β but across the whole run there are exactly n pushes, so at most n pops total. Total work is O(n). This "push-once, pop-once" line is the proof every monotonic-stack answer must state.| Bug | The input that exposes it |
|---|---|
Monotonic < vs <= β wrong tie handling. | Equal heights [2,2,2] in histogram β off-by-one width if the comparison is wrong. |
| Storing values when you need distance/width. | Any area/span problem β can't compute i β left β 1 without the index. |
| Forgetting the trailing sentinel/flush. | Strictly increasing histogram [1,2,3] β nothing ever pops; the answer is never computed. |
| Matching: popping an empty stack. | ")(" or "]" β pop() on empty must count as invalid, not crash. |
| Matching: not checking the stack is empty at the end. | "(" β unclosed opener should fail. |
| removeK: not trimming the leftover budget from the tail. | "12345", k=2 (already ascending) β must drop the last two β "123". |
| removeK: leaving leading zeros. | "10200", k=1 β "200", not "0200". |
// 1944 Number of Visible People in a Queue β scan RIGHTβLEFT, decreasing stack
function canSeePersonsCount(h: number[]): number[] {
const res: number[] = new Array(h.length).fill(0), st: number[] = [];
for (let i = h.length - 1; i >= 0; i--) {
while (st.length && h[st[st.length - 1]] < h[i]) { st.pop(); res[i]++; } // every shorter person is visible
if (st.length) res[i]++; // plus the first person >= me
st.push(i);
}
return res;
}
// 636 Exclusive Time of Functions β simulate the call stack
function exclusiveTime(n: number, logs: string[]): number[] {
const res: number[] = new Array(n).fill(0), st: number[] = [];
let prev = 0;
for (const log of logs) {
const [id, type, t] = log.split(':'); const ts = +t;
if (type === 'start') {
if (st.length) res[st[st.length - 1]] += ts - prev; // caller runs until the callee starts
st.push(+id); prev = ts;
} else {
res[st.pop()!] += ts - prev + 1; // end timestamp is INCLUSIVE
prev = ts + 1;
}
}
return res;
}
// 768 Max Chunks To Make Sorted II β each stack element = the MAX of one chunk
function maxChunksToSorted(arr: number[]): number {
const st: number[] = [];
for (const x of arr) {
if (!st.length || x >= st[st.length - 1]) { st.push(x); continue; } // >= keeps duplicates in separate chunks
const cur = st[st.length - 1]; // the merged chunk's max
while (st.length && st[st.length - 1] > x) st.pop(); // absorb every chunk whose max exceeds x
st.push(cur);
}
return st.length;
}
+1 counts the first person β₯ you. 636: the +1 on end and prev = ts + 1. 768: push with >=; push back the old top (cur), not x.Own each pattern and these are in range. Bold = Hard tier.
| Pattern | Representative LeetCode | Tier |
|---|---|---|
| Matching | 20 Valid Parentheses Β· 1047 Remove Adjacent Dups Β· 636 Exclusive Time (call-stack sim) Β· 32 Longest Valid Parens | Easy β Hard |
| Parsing | 150 Eval RPN Β· 394 Decode String Β· 224 Basic Calculator | Med β Hard |
| Monotonic stack | 496/503 Next Greater Element Β· 739 Daily Temperatures Β· 901 Stock Span Β· 1944 Visible People Β· 768 Max Chunks II | Med |
| Histogram / area | 84 Largest Rectangle in Histogram Β· 85 Maximal Rectangle Β· 42 Trapping Rain | Hard |
| Greedy with a stack | 316 Remove Duplicate Letters Β· 402 Remove K Digits Β· 1673 | Med β Hard |
Cover the right column (blurred β hover/tap to reveal). Say your answer out loud first.
| Prompt | Answer |
|---|---|
| What does a monotonic stack actually hold at any moment? | Exactly the elements whose answer (nearest greater/smaller) is still unknown β kept in sorted order. |
Why is a monotonic stack O(n) despite the inner while? | Push-once, pop-once: across the run there are n pushes, so β€ n pops total. Amortized linear. |
| In the histogram, what is the width when a bar pops? | i β left β 1, where left is the index now on top (its nearest shorter on the left) and i is the shorter bar that triggered the pop. |
| For "next greater," which direction is the stack sorted, and why store indices? | Strictly decreasing; store indices to compute distances (e.g. days) and to write into the result array. |
| Remove K Digits: what are the two cleanup steps after the loop? | Trim leftover k from the tail, then strip leading zeros (and return "0" if empty). |
| When should you not use a stack? | When you need random/middle access, or the resolution order isn't LIFO / nearest-on-one-side. |
| How does a monotonic stack relate to the sliding-window deque? | Same idea; the deque also evicts from the front when elements leave the window β a stack open at both ends. |
| Visible People in a Queue β why scan right to left, and what do you add per person? | Visibility looks to the right. Each shorter person popped is visible (+1 each), plus +1 for the first person at least as tall who remains on the stack. |
| Max Chunks II β what does each stack element represent? | The maximum of one chunk. A smaller incoming value merges every chunk with a bigger max into one (push the old top back); the stack size is the chunk count. |
isValid (brackets)decodeStringnextGreater + Daily TemperatureslargestRectangle (with the sentinel)removeKdigitsFor each monotonic one: write "increasing/decreasing, pop on < or β€, what I compute on pop" as a comment first.