πŸ₯ž Stack & Monotonic Stack

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.

🧠 1 · How to think (and invent it)

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).

decreasing stack (top = smallest) 75 71 69 ← bottom (kept) ← these pop: smaller than 72 72 newcomer 72 > 69 β†’ pop, resolve 69 72 > 71 β†’ pop, resolve 71 72 < 75 β†’ stop, push 72
The newcomer is the answer ("next greater") for everything smaller it pops. Push-once, pop-once β‡’ linear.
Mental checklist (monotonic) (1) increasing or decreasing stack? (2) pop on < 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.

Watch it get invented β€” Daily Temperatures

For each day, how many days until a warmer one? temps = [73,74,75,71,69,72,76,73].

  1. Start brute. For each i, scan right for the first warmer day β†’ O(nΒ²), re-scanning the same cold stretch over and over.
  2. Spot the waste. Keep a stack of days still waiting for a warmer one β€” naturally in decreasing temperature. A warm newcomer resolves all the colder days on top at once, and a day is resolved exactly once.
  3. The leap. Decreasing stack of indices. While temps[top] < temps[i]: pop, set ans[top] = i βˆ’ top. Then push i.
  4. Payoff. Each index pushed once, popped once β‡’ O(n).
itemppops (resolve)stack (indices) after
275pop 1 β†’ ans[1]=1[2]
572pop 4 (ans=1), pop 3 (ans=2)[2, 5]
676pop 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.

🎯 2 · Attack plan

AskIf yes β†’Pattern
Nesting/matching β€” brackets, balanced, undo?push openers, pop on closersMatching (Β§4.1)
Need to resume context after a sub-part (multipliers, expressions)?stack of partial stateParse / evaluate (Β§4.2)
For each element, the nearest larger/smaller on one side?monotonic stackMonotonic Β· next (Β§4.3)
Span/area bounded by nearest smaller/larger (histogram, rain)?monotonic + width mathMonotonic Β· area (Β§4.4)
Greedily build an optimal sequence, dropping earlier worse picks?monotonic greedyMonotonic Β· greedy (Β§4.5)
When a stack is the WRONG tool Β· You need random / middle access β€” a stack only sees the top.
Β· The "resolve order" isn't LIFO and isn't "nearest on one side" β€” you may want a queue, heap, or two pointers.
Β· You're tempted to pop based on a non-monotonic rule (pop, then a later element should have un-popped it) β€” the push-once/pop-once guarantee breaks, and so does O(n).

πŸ”Ž 3 Β· Recognition map

Words in the promptReach 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 · The five core patterns

Β§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.

4.1 Β· Matching & nesting

β€œ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.

( [ { } ] ) ( [ { '}' pops the matching '{'
Each closer must equal the popped opener; an empty stack at the end means perfectly balanced.
πŸ”‘ Invariant β€” why it's safe The stack always holds the still-open brackets in the exact order they must close. So the next valid closer is uniquely determined by the top β€” a single comparison decides validity.
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
}
🎀 Interview lens Probed: the two empty-stack checks (pop on empty = bad closer; non-empty at end = unclosed). Β· Escalation: valid parens β†’ "min add/remove to make valid" β†’ "longest valid parentheses" (stack of indices, or DP).

4.2 Β· Parse / evaluate β€” a stack of context

β€œ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.

πŸ”‘ Invariant β€” why it's safe At any moment the stack holds, from bottom to top, the unfinished work of each enclosing level. Popping restores exactly the parent's state, so nesting of any depth resolves correctly.
// 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;
}
🎀 Interview lens Probed: multi-digit numbers and arbitrary nesting depth. Β· Escalation: decode string β†’ "Basic Calculator I/II/III" (operators + precedence with a stack) β†’ evaluate RPN.

4.3 Β· Monotonic stack β€” nearest greater / smaller

β€œ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.

πŸ”‘ Invariant β€” why it's safe The stack holds exactly the elements whose "next greater" is still unknown, in strictly decreasing value order. So the first element a newcomer fails to beat blocks everything beneath it β€” no need to look deeper. Each index is pushed and popped once β‡’ 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
}
🎀 Interview lens Probed: increasing vs decreasing, < 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.

4.4 Β· Monotonic stack β€” spans & areas

β€œ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.

2 1 5 6 2 3 best = height 5 Γ— width 2 = 10
Bar 5 pops when bar 2 arrives; its width spans until the previous shorter bar β€” that's where the area is computed.
πŸ”‘ Invariant β€” why it's safe The stack holds bars in increasing height; the bar below any bar is its nearest shorter on the left, and the bar that pops it is its nearest shorter on the right. Those two bounds give the exact maximal width for that height β€” computed precisely once, at the pop.
// 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;
}
🎀 Interview lens Probed: the width formula 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).

4.5 Β· Monotonic stack β€” greedy lexicographic build

β€œ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.

πŸ”‘ Invariant β€” why it's safe The stack is always the lexicographically best prefix achievable from what's been read, given how many removals you've spent. Popping a larger digit before a smaller newcomer strictly improves the prefix and can never be regretted.
// 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
}
🎀 Interview lens Probed: the leftover-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."

⏱️ 5 · Complexity

PatternTimeSpaceNote
Matching / ParseO(n)O(n)stack depth ≀ nesting depth
Monotonic (next / area / greedy)O(n)O(n)each element pushed once, popped once
The amortized argument The inner 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.

⚠️ 6 Β· Pitfalls β€” and the test that catches each

BugThe 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".
πŸ”— This pattern, elsewhere The monotonic stack is the monotonic deque from sliding-window.html β€” a deque is just a stack open at both ends, evicting from the front when the window slides. The recursion call stack (recursion.html) is a stack you make explicit here β€” every recursion can be rewritten with one. Stacks drive iterative DFS and tree traversal (see the Graph / Trees sheets). Histogram area β†’ "Maximal Rectangle" turns a 2D matrix into a histogram per row.
βž• Three more stack shapes β€” Visible People (1944) Β· Exclusive Time (636) Β· Max Chunks II (768)
Same monotonic-stack machinery, three new roles: count while you pop (1944), the stack is the call stack (636), and each stack element summarises one group (768).
// 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;
}
⚠️ Traps 1944: pop on strictly shorter; the trailing +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.

πŸ“Š Coverage β€” pattern β†’ LeetCode (medium & hard)

Own each pattern and these are in range. Bold = Hard tier.

PatternRepresentative LeetCodeTier
Matching20 Valid Parentheses Β· 1047 Remove Adjacent Dups Β· 636 Exclusive Time (call-stack sim) Β· 32 Longest Valid ParensEasy β†’ Hard
Parsing150 Eval RPN Β· 394 Decode String Β· 224 Basic CalculatorMed β†’ Hard
Monotonic stack496/503 Next Greater Element Β· 739 Daily Temperatures Β· 901 Stock Span Β· 1944 Visible People Β· 768 Max Chunks IIMed
Histogram / area84 Largest Rectangle in Histogram Β· 85 Maximal Rectangle Β· 42 Trapping RainHard
Greedy with a stack316 Remove Duplicate Letters Β· 402 Remove K Digits Β· 1673Med β†’ Hard

βœ… 7 Β· Self-check

Cover the right column (blurred β€” hover/tap to reveal). Say your answer out loud first.

PromptAnswer
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.

Write these cold (no peeking)

For each monotonic one: write "increasing/decreasing, pop on < or ≀, what I compute on pop" as a comment first.