Conceptual Foundation & Intuitive Analogy
Checking future days for each day's "next warmer day" is O(N²). A monotonic decreasing stack resolves all days in a single linear pass by storing unresolved cooler days until a warmer day arrives.
🧭 Conceptual Foundation & Pattern Intuition
For each day in an array of temperatures, we want to know how many days we must wait until a warmer temperature arrives. A brute-force nested loop compares each day with all future days , costing time and timing out on . The key observation is that colder days cannot resolve each other:
- If Day 2 is colder than Day 1, Day 2 cannot be Day 1's answer, but both Day 1 and Day 2 are now waiting for some future warmer day.
- A Monotonic Decreasing Stack stores the indices of days that are still waiting for a warmer temperature.
- When we iterate to day with temperature :
- While the stack is non-empty and : day is the very first warmer day for the day at the top of the stack! We pop
prev_idx = stack.pop()and record the waiting duration:answer[prev_idx] = i - prev_idx. - We push index onto the stack.
- While the stack is non-empty and : day is the very first warmer day for the day at the top of the stack! We pop
- Any day remaining on the stack when the loop terminates never found a warmer day, correctly remaining
0.
🎫 The Analogy: The Waiting Room for Outdoor Cafe Seating
Imagine people waiting for outdoor patio seating at a mountain cafe. Customer A arrives on a 60° day and says "Call me when it gets warmer." Customer B arrives on a 55° day and sits next to Customer A. Customer C arrives on a 50° day. The waiting room is ordered from warmest to coldest. Suddenly, an 65° sunny afternoon arrives! The hostess walks into the waiting room: 65° is warmer than Customer C (50°), so C is seated. It's warmer than Customer B (55°), so B is seated. It's warmer than Customer A (60°), so A is seated! One single warm day clears the entire waiting room in one sweep.
🔑 Why It Works
Even though there is a while loop inside a for loop, every index is pushed onto the stack exactly once and popped at most once. Total operations across the entire array are strictly bounded by , guaranteeing amortized linear time.
Everything on my stack is still waiting for an answer, and it is ordered. When a new element arrives, it answers every waiting element it beats.
A monotonic stack never goes the wrong way, so a pop answers a question about the popped index. A nesting stack holds one saved state per open bracket.
Monotonic: while stack and nums[i] > nums[stack[-1]]: stack.pop(). Nesting: push on an opening token, pop on the matching close.
The 4 Mental Models of Stack
Monotonic stacks, nesting stacks and stack design: name the model before coding.
Length-Prefix Encoding
| 1 | class Codec: |
| 2 | """Encode and Decode Strings: write each string's length and a "#" in front of it. |
| 3 | Time: O(N + L) | Space: O(N + L) |
| 4 | """ |
| 5 | |
| 6 | def encode(self, strs: list[str]) -> str: |
| 7 | parts: list[str] = [] # One size-prefixed piece per string |
| 8 | for s in strs: |
| 9 | # 1. Write Each String's Size Before Its Data: |
| 10 | # Any character may appear inside a string, so no separator is safe; the reader learns where |
| 11 | # a piece ends before it reads a single character of it. |
| 12 | parts.append(str(len(s)) + "#" + s) |
| 13 | return "".join(parts) |
| 14 | |
| 15 | def decode(self, s: str) -> list[str]: |
| 16 | strs: list[str] = [] |
| 17 | i = 0 # Always the first digit of a length |
| 18 | while i < len(s): |
| 19 | # 1. Find Where The Size Ends: |
| 20 | # The first "#" from i must end the length, because digits written by encode never contain a "#". |
| 21 | j = s.index("#", i) |
| 22 | length = int(s[i:j]) |
| 23 | |
| 24 | # 2. Take Exactly That Many Characters And Jump Past Them: |
| 25 | # Counting instead of searching means a "#" or a digit inside the text is never read as part |
| 26 | # of the format. |
| 27 | strs.append(s[j + 1 : j + 1 + length]) |
| 28 | i = j + 1 + length |
| 29 | return strs |
Any character can appear inside a string, so no separator can mark where one ends: write the size before the data instead, str(len(s)) + "#" + s in encode. Digits never contain a "#", so in decode the first "#" from i, j = s.index("#", i), is exactly where the length ends, and the length says where the text ends: take s[j + 1 : j + 1 + length] and jump i = j + 1 + length without reading the text, so the next piece starts on a length again. The trap is reading the text by searching for the next "#" or splitting the message on "#": the strings may hold "#" and digits themselves, as in ["4#ab", "", "#"], and only counting length characters gets past them safely.
Gotcha 1 of 4
- The trap
Gotcha 1: Splitting the message on
"#"Mistakedef encode(self, strs): return "#".join(strs) def decode(self, s): return s.split("#")Fixstrs.append(s[j + 1 : j + 1 + length]) i = j + 1 + lengthWhat happens: Any character can appear inside a string,
"#"included. A split on"#"cuts such a string into pieces. Reading up to the next"#"cuts it in the same way.decodemust take exactlylengthcharacters and jump over them withi = j + 1 + length.Example: strs = ["4#ab", "", "#"]Mistake: returns ['4', 'ab', '', '', '']Fix: returns ['4#ab', '', '#']
2Nesting Stack
nested input ➔ push the outer context on open ➔ pop and combine on closeStack for Parsing & Nesting (Decode String, Basic Calculator)
| 1 | def decode_string(s: str) -> str: |
| 2 | """Decode String: save the outer text and count at each '[', restore them at ']'. |
| 3 | Time: O(N * L) | Space: O(N + L) |
| 4 | """ |
| 5 | stack: list[tuple[str, int]] = [] # One saved (cur, num) per '[' still open |
| 6 | cur = "" # Text of the innermost open bracket, built so far |
| 7 | num = 0 # Repeat count being read, digit by digit |
| 8 | |
| 9 | for ch in s: |
| 10 | if ch.isdigit(): |
| 11 | # 1. Build The Repeat Count Digit By Digit: |
| 12 | # A count can have several digits, as in "12[a]", so each new digit shifts the earlier |
| 13 | # ones left by one place. |
| 14 | num = num * 10 + int(ch) |
| 15 | elif ch == "[": |
| 16 | # 2. Opener: Save The Outer State, Then Start The Inner One Empty: |
| 17 | # The outer text waits on stack with the count this bracket will use; resetting cur and num |
| 18 | # keeps the inner text from starting with the outer text already in it. |
| 19 | stack.append((cur, num)) |
| 20 | cur, num = "", 0 |
| 21 | elif ch == "]": |
| 22 | # 3. Closer: Restore The Outer State And Fold The Finished Inner Result In: |
| 23 | # The ']' closes the most recent '[', whose saved pair sits on top of stack; the inner text |
| 24 | # goes back after the outer text, repeated k times. |
| 25 | prev, k = stack.pop() |
| 26 | cur = prev + cur * k |
| 27 | else: |
| 28 | # 4. Plain Content Extends The Current Text: |
| 29 | # A letter belongs to whichever bracket is open right now. |
| 30 | cur += ch |
| 31 | |
| 32 | return cur |
A [ starts a smaller problem inside the current one, and the outer text has to wait until that inner problem is finished: so at [ push the outer state (cur, num) onto stack and start the inner text empty, and at ] pop it back and fold the finished inner text in with cur = prev + cur * k. Brackets close in the reverse order they open, so the ] you meet always closes the most recent [ still open, and the top of stack is exactly what that [ saved; everything below it waits untouched for its own ]. That is the whole model for any nesting (brackets, parentheses, folders): last opened, first finished, one saved context per open level. The trap is the reset: stack.append((cur, num)) must be followed by cur, num = "", 0. Keep the old cur and the outer text is repeated inside the bracket ("3[a2[c]]" turns into "aacacaacacaacac"); keep the old num and the next count's digits are glued onto it (the inner 2 is read as 32).
Gotcha 1 of 5
- The trap
Gotcha 1: Keeping the old
curafter the pushMistakenum = 0Fixcur, num = "", 0What happens: At
[, the outer text is saved onstackwithstack.append((cur, num)). The inner text must then start empty. Ifcurkeeps the outer text, that text is also repeated inside the bracket. At], the outer text is added once more fromstack.Example: s = "3[a2[c]]"Mistake: returns 'aacacaacacaacac'Fix: returns 'accaccacc'
3Min-Stack Trick
push / pop stream ➔ each entry stores the running min with it ➔ O(1) queryAugmented Entries (Store a Running Min/Max/Count With Each Item)
O(1) per call while items are pushed and popped.| 1 | class MinStack: |
| 2 | """Min Stack: each entry carries the minimum of itself and every entry below it. |
| 3 | Time: O(1) per call | Space: O(N) |
| 4 | """ |
| 5 | |
| 6 | def __init__(self) -> None: |
| 7 | # self.stack: one (val, current_min) pair per pushed value; current_min is the minimum of val |
| 8 | # and every entry below it, so the top entry always knows the whole stack's minimum. |
| 9 | self.stack: list[tuple[int, int]] = [] |
| 10 | |
| 11 | def push(self, val: int) -> None: |
| 12 | # 1. Compute This Entry's Running Minimum: |
| 13 | # Extend the minimum carried by the entry below with val, or start fresh when the stack is empty. |
| 14 | if self.stack: |
| 15 | current_min = min(val, self.stack[-1][1]) |
| 16 | else: |
| 17 | current_min = val |
| 18 | |
| 19 | # 2. Store The Value Together With Its Running Minimum: |
| 20 | # Pairing them means nothing below this entry has to change while it stays on the stack. |
| 21 | self.stack.append((val, current_min)) |
| 22 | |
| 23 | def pop(self) -> None: |
| 24 | self.stack.pop() # The new top already carries the minimum of what is left |
| 25 | |
| 26 | def top(self) -> int: |
| 27 | return self.stack[-1][0] |
| 28 | |
| 29 | def getMin(self) -> int: |
| 30 | return self.stack[-1][1] # Read it off the top entry: no search |
A stack only changes at its top, so store with each entry the answer for itself and everything below it: push appends (val, current_min) with current_min = min(val, self.stack[-1][1]), and getMin just reads self.stack[-1][1]. While an entry sits on the stack nothing below it can change, so its current_min stays true until the entry itself is popped; pop removes exactly one entry, and the new top already carries the minimum of what is left, so nothing is ever recomputed. The same move carries any value you can build from the entry below: a maximum, a count or a pending increment. The trap is keeping one self.min for the whole stack: it is right after every push, but when pop removes the smallest value it can't know the next smallest without a scan.
Gotcha 1 of 4
- The trap
Gotcha 1: Keeping one
self.minfor the whole stackMistakedef getMin(self) -> int: return self.minFixdef getMin(self) -> int: return self.stack[-1][1]What happens: One variable
self.minis correct after everypush. Whenpopremoves the smallest value,self.minstill holds it. Finding the next smallest value would need a scan of the whole stack. Each entry's owncurrent_minalready holds that value.Example: push(-2), push(0), push(-3), pop(), getMin()Mistake: returns -3Fix: returns -2
4Next Greater Element
stream of values ➔ stack of strictly decreasing indices ➔ pop on violationMonotonic Stack (Next Greater Element)
| 1 | from typing import Callable, List, Sequence, TypeVar |
| 2 | |
| 3 | Item = TypeVar("Item") |
| 4 | |
| 5 | |
| 6 | def next_greater(values: Sequence[Item], pops: Callable[[Item, Item], bool]) -> List[int]: |
| 7 | """Next Greater Element: for each index, the first later index whose value answers it, or -1. |
| 8 | pops(top, value) is True when value answers the waiting value top, for example top < value. |
| 9 | Precondition: when value does not pop the top of the stack, it pops no index below the top either. |
| 10 | This holds for the comparisons <, <=, > and >=. |
| 11 | 132 Pattern and Online Stock Span (a class that answers one value at a time) do not fit. |
| 12 | N = len(values). T = the cost of one pops call. |
| 13 | Time: O(N * T) | Space: O(N) |
| 14 | """ |
| 15 | n = len(values) |
| 16 | answer = [-1] * n # -1: no later value answers this index |
| 17 | stack: List[int] = [] # Holds INDICES of values still waiting for an answer, newest on top |
| 18 | |
| 19 | # 1. Visit Each Value Left To Right: |
| 20 | # Scanning once suffices because an index can only be answered by a later index, so by the time value is reached, every index that could answer it has already arrived. |
| 21 | for i, value in enumerate(values): |
| 22 | # 2. Resolve Every Waiting Index That `pops` Answers: |
| 23 | # While value answers the index on top of the stack, pop it and record i as its answer. The precondition guarantees that once value stops answering the top, it cannot answer anything buried below it either, so the while loop can stop there. |
| 24 | while stack and pops(values[stack[-1]], value): |
| 25 | prev_idx = stack.pop() # Answered by the current value |
| 26 | answer[prev_idx] = i |
| 27 | # 3. Push The Current Index As Still Waiting: |
| 28 | # Whatever remains unanswered after the pops above is now the newest index waiting for its own answer, so it goes on top. |
| 29 | stack.append(i) |
| 30 | |
| 31 | # 4. Return The Collected Answers: |
| 32 | # Any index left on the stack when the scan ends never found a later value that answers it, so it keeps its -1. |
| 33 | return answer |
Keep stack as the indices still waiting for a warmer day, and when temp beats the temperature on top, i is that index's answer: pop it and write answer[prev_idx] = i - prev_idx. Every index below the top has a temperature at least as high as the top's, so if temp does not beat the top it cannot beat anything below it; each index is therefore answered by the first warmer day to its right, and never earlier. The trap is storing temperatures instead of indices: the answer is a distance written into its own slot of answer, so push i, not temp.
Gotcha 1 of 4
- The trap
Gotcha 1: Pushing
tempinstead of the indexiMistakestack.append(temp)Fixstack.append(i)What happens: The answer is a distance,
i - prev_idx, written intoanswer[prev_idx]. Both need the day's index, and a temperature cannot give it. Withtemponstack, the loop readstemperatures[stack[-1]]with a temperature as the index. That index is past the end of the list, so Python raises an IndexError.Example: temperatures = [30, 40, 50, 60]Mistake: raises IndexErrorFix: returns [1, 1, 1, 0]
Built on the mental models above
- Valley WaterMonotonic Stack (Trapping Rain Water)
A pop closes a valley: the popped bar is the floor, the new top is the left wall and the arriving bar is the right wall.
- Histogram StackMonotonic Stack (Largest Rectangle in Histogram)
A pop also needs the previous smaller bar, so the width is known at the moment the bar is popped.
- Contribution TechniqueContribution Technique (Sum over All Subarrays via Each Element's Share)
Each popped item is the minimum of the range between its two smaller neighbours, so it counts value times left times right.
- Lexicographic StackMonotonic Stack Greedy (Remove K Digits, Remove Duplicate Letters)
A pop is paid for by a budget of removals, or blocked because the popped item appears again later.
The Stack Decision Rule
When facing an unseen stack problem, ask in this order:
Store indices in decreasing stack and pop when larger element arrives.
Compute area with popped height and left/right stack boundaries.
Element i is the minimum of (i - left[i]) * (right[i] - i) subarrays; find left and right with a monotonic stack, breaking ties on one side only.
While deletions remain and the stack top is bigger than the current character, pop it; then push.
Push (value, running min) pairs, so each entry remembers the answer for the stack below it.
On an opening token push the outer context; on the matching close pop it and combine it with the finished inner result.
Write each string as its length, a delimiter, then the string; the decoder reads the length first.
Pop each valley floor when a taller bar arrives; the new top is the left wall, so add width times the lower wall minus the floor.
Pattern Architecture & Invariant Design Masterclass
One idea runs through every card here: keep a stack of items that are still waiting, and let each new item answer the ones it can.
Monotonic stack. Asked "for each element, what is the next bigger one to its right?", the instinct is again O(n²). Instead keep a stack of elements still waiting for their answer, deliberately maintained so it is always increasing (or always decreasing). When a new element arrives, it answers every waiting element it beats — those get popped, permanently. Each element is pushed once and popped once, so the whole thing is O(n) despite the inner loop.
Nesting stack. Some inputs nest: brackets inside brackets, a sub-expression inside parentheses. Keep one saved state for each bracket that is still open, with the innermost on top. An opening token pushes the outer state. The matching close pops it and combines it with the inner result.
The Duplicate Asymmetry Invariant (Strict vs. Non-Strict Inequalities)
When using a monotonic stack to compute span boundaries over arrays that may contain duplicate elements and every subarray must be counted exactly once (e.g. Sum of Subarray Minimums), a subtle correctness bug emerges if both boundaries use the same comparison:
- If both left and right boundaries use strict inequality (
<), any subarray containing equal minimum values is counted twice. - If both boundaries use non-strict inequality (
<=), equal elements pop each other prematurely, leaving duplicate spans uncounted. - The Invariant: Break symmetry deterministically. Set one side to strictly greater/smaller (
<) and the opposite side to greater/smaller or equal (<=). This guarantees every contiguous subarray has a single, uniquely designated representative element (e.g. the leftmost minimum). - When nothing is counted: for a maximum such as Largest Rectangle in Histogram, letting a bar extend over equal heights on both sides is also correct; only stopping at equal heights on both sides is wrong, because the widest rectangle of that height is then never measured.
The invariants to say out loud:
- Monotonic stack: "Everything on my stack is still waiting for an answer, and it is ordered — so the first element that fails the pop test means every one below it fails too. When duplicates exist, enforce asymmetric boundaries to partition spans uniquely."
- Nesting stack: "The stack holds one saved state for every bracket that is still open, and the innermost is on top."
How this plays out in actual problems:
🟣 Scenario C: Monotonic Stack Pending Query Resolution (Indices Stack)
- In nearest greater/smaller element queries (like Daily Temperatures LC 739 or Largest Rectangle in Histogram LC 84), pairwise lookups are .
- The Rule: Maintain an index stack maintaining strict monotonic order. An incoming element serves as the answer ("next greater/smaller") for every element it violates on top of the stack. Pop and record results in amortized time.
- The Code:
while stack and val > arr[stack[-1]]: prev = stack.pop(); ans[prev] = i - prev
💡 The Ultimate Takeaway Decision Rule:
When analyzing a sequence query problem, ask: "Does each item need the nearest bigger or smaller item, does the input nest, or must a stack answer a question about all of its items?"
- Nearest Greater / Smaller Element ➔ Monotonic stack of indices, pop on monotonic violation ().
- Max Rectangular Bounded Area ➔ Monotonic increasing stack with sentinel zero padding ().
- Nested Brackets or Expressions ➔ Push the saved state on an open token, pop on the close ().
- Running Minimum of a Stack ➔ Store the minimum with every entry ( per call).
🌡️ 4. Monotonic Decreasing Stack for Next Greater Element (LC 739)
- Mental Model: The stack stores indices of elements waiting for a higher temperature. Incoming higher temperature resolves all smaller elements below it.
- Code: see the Next Greater Element card.
📊 5. Monotonic Increasing Stack for Histogram Bounded Area (LC 84)
- Mental Model: A rectangle's height is determined by the shortest bar inside it. Popping a bar from an increasing stack identifies its left smaller boundary (
stack[-1]) and right smaller boundary (i). - Code: see the Histogram Stack card.
Understanding Invariants: Monotonic Stack Pending Query Resolution
Everything on my stack is still waiting for an answer, and the stack never goes the wrong way.
Monotonic stacks maintain strict extremum order, so an incoming violator resolves every pending query it beats in amortized O(1) per element. Nesting stacks save one state per open level and restore it on the matching close.
Incoming element acts as the closest boundary or answer for all dominated waiting elements on the stack.
The popped entry is exactly the state that was saved when this bracket opened, so the inner result combines with the right outer state.
Common Questions
"My monotonic stack solution has a while loop nested inside a for loop — doesn't that make it O(n^2), the same as the brute force it's supposed to replace?"
It looks that way from the code's shape, but shape is misleading here — you have to track what each individual element does across the entire run, not what the inner loop does in one iteration of the outer loop.
The
forloop runsntimes, no argument there. Thewhileloop's total iteration count across the whole algorithm is the part that's easy to miscount: every single element is pushed onto the stack exactly once (that happens once perforiteration, unconditionally) and popped at most once, ever — once an element is popped, it's answered and gone for good, it never goes back on the stack. So the grand total of allwhile-loop pops, summed over every iteration of theforloop, cannot exceedn— the samenelements that were pushed.That gives at most
npushes plus at mostnpops,2ntotal stack operations, however unevenly they're distributed across iterations (one iteration might pop five elements, the next might pop zero). This is amortized analysis: thewhileloop isn'tO(n)per outer iteration, it'sO(n)for the entire algorithm, which is what makes the whole thingO(n)overall rather thanO(n²). The tell an interviewer is listening for: "each element is pushed once and popped at most once."
Coach discussion points
- 1.
"Your monotonic stack has a
whileinside afor. Convince me it is notO(n²)." — The amortisation argument again. Follow up with "how many times in total can an index be popped?" if they stall. - 2.
"Trapping Rain Water also has a two-pointer solution. Walk me through why the monotonic-stack version resolves a bar's water differently, and which one you'd actually reach for in an interview." — Tests whether the candidate understands both lenses on the same problem rather than having memorized one path, and whether they can articulate the trade-off (stack needs
O(n)space; two-pointer needsO(1)). - 3.
"Your Trapping Rain Water stack popped a valley and the stack is now empty. What do you do, and why is that not a bug?" — Forces the candidate to state the empty-stack guard out loud: that valley has no left wall yet, contributes zero for now, and is correctly not pushed back.
- 4.
"For Maximal Rectangle, why can you call the exact same
largestRectangleAreafunction you wrote for the histogram problem, completely unchanged, once per row?" — Tests whether the candidate sees the row-reduction insight as a genuine algorithmic reduction (histogram problem solved once, reused rows times) rather than coincidence.
Complexity analysis
Daily Temperatures — Time O(n), Space O(n).
This one is worth defending carefully, because it contains a while inside a for and looks quadratic. Each index is pushed exactly once and popped at most once, so the total number of stack operations across the whole run is at most 2n. The inner loop is not O(n) per iteration — it is O(n) for the entire algorithm. That is the same amortisation argument as the sliding window in two-pointers.md, and interviewers ask for it in exactly the same way. Space is the stack, O(n) when temperatures are strictly decreasing and nothing ever resolves.
Trapping Rain Water (Monotonic Stack) — Time O(n), Space O(n).
Same amortisation argument as Daily Temperatures: each bar index is pushed exactly once and popped at most once across the entire run, so total stack operations are bounded by 2n regardless of the nested while inside the for. Space is the stack, worst case O(n) for a strictly decreasing elevation profile where nothing ever resolves.
Maximal Rectangle (Row-Reduced Histogram) — Time O(rows * cols), Space O(cols).
Each of the rows rows costs O(cols) to update the running heights array, plus another amortised O(cols) to run the Largest Rectangle in Histogram (LC 84) solver on it — so the total is rows * O(cols) = O(rows * cols). Space is O(cols) for the persisted heights array plus the per-row monotonic stack (discarded after each row), independent of rows.
A weather app shows, for each day, how many days until it gets warmer. A trading dashboard shows how long ago the price was last higher. Both need the nearest bigger value on one side of every item. Checking every later item for every item costs O(n²) and is too slow for a year of daily data. One pass with a stack costs O(n).
💡 Don't worry if popping from a stack feels unintuitive right now! A stack turns quadratic searches into one linear pass because each item is pushed once and popped at most once. As you work through the cards, the same three questions repeat: what do I store, when do I pop, and what does a pop answer.
Pushing values when you also need positions: this problem only needs values, because all values are distinct. Problems with repeated values or distances need indices on the stack (Daily Temperatures).
Scanning nums2 once per query: looking up each
nums1[i]with a fresh scan costs O(n1 * n2). Build the whole answer table once, then read it.Forgetting the default: a value that stays on the stack has no larger value to its right, so its answer is
-1. Usedict.get(x, -1).
Staff+ Systems Architecture & Hardware Considerations
Triggers: "next greater or smaller element", "largest rectangle in a histogram", "remove k digits to make the smallest number", "decode nested brackets". Each one needs the most recent unfinished item, and a stack gives exactly that.
Arrays of up to items. Every item is pushed once and popped at most once, so the cost is . Checking every later item for every item costs and is too slow.
Pushing values when the answer needs positions (push indices); using < where <= is needed when values repeat; forgetting the items left on the stack after the loop; popping while the stack is empty.
Conceptual Foundation & Intuitive Analogy
Checking future days for each day's "next warmer day" is O(N²). A monotonic decreasing stack resolves all days in a single linear pass by storing unresolved cooler days until a warmer day arrives.
🧭 Conceptual Foundation & Pattern Intuition
For each day in an array of temperatures, we want to know how many days we must wait until a warmer temperature arrives. A brute-force nested loop compares each day with all future days , costing time and timing out on . The key observation is that colder days cannot resolve each other:
- If Day 2 is colder than Day 1, Day 2 cannot be Day 1's answer, but both Day 1 and Day 2 are now waiting for some future warmer day.
- A Monotonic Decreasing Stack stores the indices of days that are still waiting for a warmer temperature.
- When we iterate to day with temperature :
- While the stack is non-empty and : day is the very first warmer day for the day at the top of the stack! We pop
prev_idx = stack.pop()and record the waiting duration:answer[prev_idx] = i - prev_idx. - We push index onto the stack.
- While the stack is non-empty and : day is the very first warmer day for the day at the top of the stack! We pop
- Any day remaining on the stack when the loop terminates never found a warmer day, correctly remaining
0.
🎫 The Analogy: The Waiting Room for Outdoor Cafe Seating
Imagine people waiting for outdoor patio seating at a mountain cafe. Customer A arrives on a 60° day and says "Call me when it gets warmer." Customer B arrives on a 55° day and sits next to Customer A. Customer C arrives on a 50° day. The waiting room is ordered from warmest to coldest. Suddenly, an 65° sunny afternoon arrives! The hostess walks into the waiting room: 65° is warmer than Customer C (50°), so C is seated. It's warmer than Customer B (55°), so B is seated. It's warmer than Customer A (60°), so A is seated! One single warm day clears the entire waiting room in one sweep.
🔑 Why It Works
Even though there is a while loop inside a for loop, every index is pushed onto the stack exactly once and popped at most once. Total operations across the entire array are strictly bounded by , guaranteeing amortized linear time.
Everything on my stack is still waiting for an answer, and it is ordered. When a new element arrives, it answers every waiting element it beats.
A monotonic stack never goes the wrong way, so a pop answers a question about the popped index. A nesting stack holds one saved state per open bracket.
Monotonic: while stack and nums[i] > nums[stack[-1]]: stack.pop(). Nesting: push on an opening token, pop on the matching close.
The 4 Mental Models of Stack
Monotonic stacks, nesting stacks and stack design: name the model before coding.
Length-Prefix Encoding
| 1 | class Codec: |
| 2 | """Encode and Decode Strings: write each string's length and a "#" in front of it. |
| 3 | Time: O(N + L) | Space: O(N + L) |
| 4 | """ |
| 5 | |
| 6 | def encode(self, strs: list[str]) -> str: |
| 7 | parts: list[str] = [] # One size-prefixed piece per string |
| 8 | for s in strs: |
| 9 | # 1. Write Each String's Size Before Its Data: |
| 10 | # Any character may appear inside a string, so no separator is safe; the reader learns where |
| 11 | # a piece ends before it reads a single character of it. |
| 12 | parts.append(str(len(s)) + "#" + s) |
| 13 | return "".join(parts) |
| 14 | |
| 15 | def decode(self, s: str) -> list[str]: |
| 16 | strs: list[str] = [] |
| 17 | i = 0 # Always the first digit of a length |
| 18 | while i < len(s): |
| 19 | # 1. Find Where The Size Ends: |
| 20 | # The first "#" from i must end the length, because digits written by encode never contain a "#". |
| 21 | j = s.index("#", i) |
| 22 | length = int(s[i:j]) |
| 23 | |
| 24 | # 2. Take Exactly That Many Characters And Jump Past Them: |
| 25 | # Counting instead of searching means a "#" or a digit inside the text is never read as part |
| 26 | # of the format. |
| 27 | strs.append(s[j + 1 : j + 1 + length]) |
| 28 | i = j + 1 + length |
| 29 | return strs |
Any character can appear inside a string, so no separator can mark where one ends: write the size before the data instead, str(len(s)) + "#" + s in encode. Digits never contain a "#", so in decode the first "#" from i, j = s.index("#", i), is exactly where the length ends, and the length says where the text ends: take s[j + 1 : j + 1 + length] and jump i = j + 1 + length without reading the text, so the next piece starts on a length again. The trap is reading the text by searching for the next "#" or splitting the message on "#": the strings may hold "#" and digits themselves, as in ["4#ab", "", "#"], and only counting length characters gets past them safely.
Gotcha 1 of 4
- The trap
Gotcha 1: Splitting the message on
"#"Mistakedef encode(self, strs): return "#".join(strs) def decode(self, s): return s.split("#")Fixstrs.append(s[j + 1 : j + 1 + length]) i = j + 1 + lengthWhat happens: Any character can appear inside a string,
"#"included. A split on"#"cuts such a string into pieces. Reading up to the next"#"cuts it in the same way.decodemust take exactlylengthcharacters and jump over them withi = j + 1 + length.Example: strs = ["4#ab", "", "#"]Mistake: returns ['4', 'ab', '', '', '']Fix: returns ['4#ab', '', '#']
2Nesting Stack
nested input ➔ push the outer context on open ➔ pop and combine on closeStack for Parsing & Nesting (Decode String, Basic Calculator)
| 1 | def decode_string(s: str) -> str: |
| 2 | """Decode String: save the outer text and count at each '[', restore them at ']'. |
| 3 | Time: O(N * L) | Space: O(N + L) |
| 4 | """ |
| 5 | stack: list[tuple[str, int]] = [] # One saved (cur, num) per '[' still open |
| 6 | cur = "" # Text of the innermost open bracket, built so far |
| 7 | num = 0 # Repeat count being read, digit by digit |
| 8 | |
| 9 | for ch in s: |
| 10 | if ch.isdigit(): |
| 11 | # 1. Build The Repeat Count Digit By Digit: |
| 12 | # A count can have several digits, as in "12[a]", so each new digit shifts the earlier |
| 13 | # ones left by one place. |
| 14 | num = num * 10 + int(ch) |
| 15 | elif ch == "[": |
| 16 | # 2. Opener: Save The Outer State, Then Start The Inner One Empty: |
| 17 | # The outer text waits on stack with the count this bracket will use; resetting cur and num |
| 18 | # keeps the inner text from starting with the outer text already in it. |
| 19 | stack.append((cur, num)) |
| 20 | cur, num = "", 0 |
| 21 | elif ch == "]": |
| 22 | # 3. Closer: Restore The Outer State And Fold The Finished Inner Result In: |
| 23 | # The ']' closes the most recent '[', whose saved pair sits on top of stack; the inner text |
| 24 | # goes back after the outer text, repeated k times. |
| 25 | prev, k = stack.pop() |
| 26 | cur = prev + cur * k |
| 27 | else: |
| 28 | # 4. Plain Content Extends The Current Text: |
| 29 | # A letter belongs to whichever bracket is open right now. |
| 30 | cur += ch |
| 31 | |
| 32 | return cur |
A [ starts a smaller problem inside the current one, and the outer text has to wait until that inner problem is finished: so at [ push the outer state (cur, num) onto stack and start the inner text empty, and at ] pop it back and fold the finished inner text in with cur = prev + cur * k. Brackets close in the reverse order they open, so the ] you meet always closes the most recent [ still open, and the top of stack is exactly what that [ saved; everything below it waits untouched for its own ]. That is the whole model for any nesting (brackets, parentheses, folders): last opened, first finished, one saved context per open level. The trap is the reset: stack.append((cur, num)) must be followed by cur, num = "", 0. Keep the old cur and the outer text is repeated inside the bracket ("3[a2[c]]" turns into "aacacaacacaacac"); keep the old num and the next count's digits are glued onto it (the inner 2 is read as 32).
Gotcha 1 of 5
- The trap
Gotcha 1: Keeping the old
curafter the pushMistakenum = 0Fixcur, num = "", 0What happens: At
[, the outer text is saved onstackwithstack.append((cur, num)). The inner text must then start empty. Ifcurkeeps the outer text, that text is also repeated inside the bracket. At], the outer text is added once more fromstack.Example: s = "3[a2[c]]"Mistake: returns 'aacacaacacaacac'Fix: returns 'accaccacc'
3Min-Stack Trick
push / pop stream ➔ each entry stores the running min with it ➔ O(1) queryAugmented Entries (Store a Running Min/Max/Count With Each Item)
O(1) per call while items are pushed and popped.| 1 | class MinStack: |
| 2 | """Min Stack: each entry carries the minimum of itself and every entry below it. |
| 3 | Time: O(1) per call | Space: O(N) |
| 4 | """ |
| 5 | |
| 6 | def __init__(self) -> None: |
| 7 | # self.stack: one (val, current_min) pair per pushed value; current_min is the minimum of val |
| 8 | # and every entry below it, so the top entry always knows the whole stack's minimum. |
| 9 | self.stack: list[tuple[int, int]] = [] |
| 10 | |
| 11 | def push(self, val: int) -> None: |
| 12 | # 1. Compute This Entry's Running Minimum: |
| 13 | # Extend the minimum carried by the entry below with val, or start fresh when the stack is empty. |
| 14 | if self.stack: |
| 15 | current_min = min(val, self.stack[-1][1]) |
| 16 | else: |
| 17 | current_min = val |
| 18 | |
| 19 | # 2. Store The Value Together With Its Running Minimum: |
| 20 | # Pairing them means nothing below this entry has to change while it stays on the stack. |
| 21 | self.stack.append((val, current_min)) |
| 22 | |
| 23 | def pop(self) -> None: |
| 24 | self.stack.pop() # The new top already carries the minimum of what is left |
| 25 | |
| 26 | def top(self) -> int: |
| 27 | return self.stack[-1][0] |
| 28 | |
| 29 | def getMin(self) -> int: |
| 30 | return self.stack[-1][1] # Read it off the top entry: no search |
A stack only changes at its top, so store with each entry the answer for itself and everything below it: push appends (val, current_min) with current_min = min(val, self.stack[-1][1]), and getMin just reads self.stack[-1][1]. While an entry sits on the stack nothing below it can change, so its current_min stays true until the entry itself is popped; pop removes exactly one entry, and the new top already carries the minimum of what is left, so nothing is ever recomputed. The same move carries any value you can build from the entry below: a maximum, a count or a pending increment. The trap is keeping one self.min for the whole stack: it is right after every push, but when pop removes the smallest value it can't know the next smallest without a scan.
Gotcha 1 of 4
- The trap
Gotcha 1: Keeping one
self.minfor the whole stackMistakedef getMin(self) -> int: return self.minFixdef getMin(self) -> int: return self.stack[-1][1]What happens: One variable
self.minis correct after everypush. Whenpopremoves the smallest value,self.minstill holds it. Finding the next smallest value would need a scan of the whole stack. Each entry's owncurrent_minalready holds that value.Example: push(-2), push(0), push(-3), pop(), getMin()Mistake: returns -3Fix: returns -2
4Next Greater Element
stream of values ➔ stack of strictly decreasing indices ➔ pop on violationMonotonic Stack (Next Greater Element)
| 1 | from typing import Callable, List, Sequence, TypeVar |
| 2 | |
| 3 | Item = TypeVar("Item") |
| 4 | |
| 5 | |
| 6 | def next_greater(values: Sequence[Item], pops: Callable[[Item, Item], bool]) -> List[int]: |
| 7 | """Next Greater Element: for each index, the first later index whose value answers it, or -1. |
| 8 | pops(top, value) is True when value answers the waiting value top, for example top < value. |
| 9 | Precondition: when value does not pop the top of the stack, it pops no index below the top either. |
| 10 | This holds for the comparisons <, <=, > and >=. |
| 11 | 132 Pattern and Online Stock Span (a class that answers one value at a time) do not fit. |
| 12 | N = len(values). T = the cost of one pops call. |
| 13 | Time: O(N * T) | Space: O(N) |
| 14 | """ |
| 15 | n = len(values) |
| 16 | answer = [-1] * n # -1: no later value answers this index |
| 17 | stack: List[int] = [] # Holds INDICES of values still waiting for an answer, newest on top |
| 18 | |
| 19 | # 1. Visit Each Value Left To Right: |
| 20 | # Scanning once suffices because an index can only be answered by a later index, so by the time value is reached, every index that could answer it has already arrived. |
| 21 | for i, value in enumerate(values): |
| 22 | # 2. Resolve Every Waiting Index That `pops` Answers: |
| 23 | # While value answers the index on top of the stack, pop it and record i as its answer. The precondition guarantees that once value stops answering the top, it cannot answer anything buried below it either, so the while loop can stop there. |
| 24 | while stack and pops(values[stack[-1]], value): |
| 25 | prev_idx = stack.pop() # Answered by the current value |
| 26 | answer[prev_idx] = i |
| 27 | # 3. Push The Current Index As Still Waiting: |
| 28 | # Whatever remains unanswered after the pops above is now the newest index waiting for its own answer, so it goes on top. |
| 29 | stack.append(i) |
| 30 | |
| 31 | # 4. Return The Collected Answers: |
| 32 | # Any index left on the stack when the scan ends never found a later value that answers it, so it keeps its -1. |
| 33 | return answer |
Keep stack as the indices still waiting for a warmer day, and when temp beats the temperature on top, i is that index's answer: pop it and write answer[prev_idx] = i - prev_idx. Every index below the top has a temperature at least as high as the top's, so if temp does not beat the top it cannot beat anything below it; each index is therefore answered by the first warmer day to its right, and never earlier. The trap is storing temperatures instead of indices: the answer is a distance written into its own slot of answer, so push i, not temp.
Gotcha 1 of 4
- The trap
Gotcha 1: Pushing
tempinstead of the indexiMistakestack.append(temp)Fixstack.append(i)What happens: The answer is a distance,
i - prev_idx, written intoanswer[prev_idx]. Both need the day's index, and a temperature cannot give it. Withtemponstack, the loop readstemperatures[stack[-1]]with a temperature as the index. That index is past the end of the list, so Python raises an IndexError.Example: temperatures = [30, 40, 50, 60]Mistake: raises IndexErrorFix: returns [1, 1, 1, 0]
Built on the mental models above
- Valley WaterMonotonic Stack (Trapping Rain Water)
A pop closes a valley: the popped bar is the floor, the new top is the left wall and the arriving bar is the right wall.
- Histogram StackMonotonic Stack (Largest Rectangle in Histogram)
A pop also needs the previous smaller bar, so the width is known at the moment the bar is popped.
- Contribution TechniqueContribution Technique (Sum over All Subarrays via Each Element's Share)
Each popped item is the minimum of the range between its two smaller neighbours, so it counts value times left times right.
- Lexicographic StackMonotonic Stack Greedy (Remove K Digits, Remove Duplicate Letters)
A pop is paid for by a budget of removals, or blocked because the popped item appears again later.
The Stack Decision Rule
When facing an unseen stack problem, ask in this order:
Store indices in decreasing stack and pop when larger element arrives.
Compute area with popped height and left/right stack boundaries.
Element i is the minimum of (i - left[i]) * (right[i] - i) subarrays; find left and right with a monotonic stack, breaking ties on one side only.
While deletions remain and the stack top is bigger than the current character, pop it; then push.
Push (value, running min) pairs, so each entry remembers the answer for the stack below it.
On an opening token push the outer context; on the matching close pop it and combine it with the finished inner result.
Write each string as its length, a delimiter, then the string; the decoder reads the length first.
Pop each valley floor when a taller bar arrives; the new top is the left wall, so add width times the lower wall minus the floor.
Pattern Architecture & Invariant Design Masterclass
One idea runs through every card here: keep a stack of items that are still waiting, and let each new item answer the ones it can.
Monotonic stack. Asked "for each element, what is the next bigger one to its right?", the instinct is again O(n²). Instead keep a stack of elements still waiting for their answer, deliberately maintained so it is always increasing (or always decreasing). When a new element arrives, it answers every waiting element it beats — those get popped, permanently. Each element is pushed once and popped once, so the whole thing is O(n) despite the inner loop.
Nesting stack. Some inputs nest: brackets inside brackets, a sub-expression inside parentheses. Keep one saved state for each bracket that is still open, with the innermost on top. An opening token pushes the outer state. The matching close pops it and combines it with the inner result.
The Duplicate Asymmetry Invariant (Strict vs. Non-Strict Inequalities)
When using a monotonic stack to compute span boundaries over arrays that may contain duplicate elements and every subarray must be counted exactly once (e.g. Sum of Subarray Minimums), a subtle correctness bug emerges if both boundaries use the same comparison:
- If both left and right boundaries use strict inequality (
<), any subarray containing equal minimum values is counted twice. - If both boundaries use non-strict inequality (
<=), equal elements pop each other prematurely, leaving duplicate spans uncounted. - The Invariant: Break symmetry deterministically. Set one side to strictly greater/smaller (
<) and the opposite side to greater/smaller or equal (<=). This guarantees every contiguous subarray has a single, uniquely designated representative element (e.g. the leftmost minimum). - When nothing is counted: for a maximum such as Largest Rectangle in Histogram, letting a bar extend over equal heights on both sides is also correct; only stopping at equal heights on both sides is wrong, because the widest rectangle of that height is then never measured.
The invariants to say out loud:
- Monotonic stack: "Everything on my stack is still waiting for an answer, and it is ordered — so the first element that fails the pop test means every one below it fails too. When duplicates exist, enforce asymmetric boundaries to partition spans uniquely."
- Nesting stack: "The stack holds one saved state for every bracket that is still open, and the innermost is on top."
How this plays out in actual problems:
🟣 Scenario C: Monotonic Stack Pending Query Resolution (Indices Stack)
- In nearest greater/smaller element queries (like Daily Temperatures LC 739 or Largest Rectangle in Histogram LC 84), pairwise lookups are .
- The Rule: Maintain an index stack maintaining strict monotonic order. An incoming element serves as the answer ("next greater/smaller") for every element it violates on top of the stack. Pop and record results in amortized time.
- The Code:
while stack and val > arr[stack[-1]]: prev = stack.pop(); ans[prev] = i - prev
💡 The Ultimate Takeaway Decision Rule:
When analyzing a sequence query problem, ask: "Does each item need the nearest bigger or smaller item, does the input nest, or must a stack answer a question about all of its items?"
- Nearest Greater / Smaller Element ➔ Monotonic stack of indices, pop on monotonic violation ().
- Max Rectangular Bounded Area ➔ Monotonic increasing stack with sentinel zero padding ().
- Nested Brackets or Expressions ➔ Push the saved state on an open token, pop on the close ().
- Running Minimum of a Stack ➔ Store the minimum with every entry ( per call).
🌡️ 4. Monotonic Decreasing Stack for Next Greater Element (LC 739)
- Mental Model: The stack stores indices of elements waiting for a higher temperature. Incoming higher temperature resolves all smaller elements below it.
- Code: see the Next Greater Element card.
📊 5. Monotonic Increasing Stack for Histogram Bounded Area (LC 84)
- Mental Model: A rectangle's height is determined by the shortest bar inside it. Popping a bar from an increasing stack identifies its left smaller boundary (
stack[-1]) and right smaller boundary (i). - Code: see the Histogram Stack card.
Understanding Invariants: Monotonic Stack Pending Query Resolution
Everything on my stack is still waiting for an answer, and the stack never goes the wrong way.
Monotonic stacks maintain strict extremum order, so an incoming violator resolves every pending query it beats in amortized O(1) per element. Nesting stacks save one state per open level and restore it on the matching close.
Incoming element acts as the closest boundary or answer for all dominated waiting elements on the stack.
The popped entry is exactly the state that was saved when this bracket opened, so the inner result combines with the right outer state.
Common Questions
"My monotonic stack solution has a while loop nested inside a for loop — doesn't that make it O(n^2), the same as the brute force it's supposed to replace?"
It looks that way from the code's shape, but shape is misleading here — you have to track what each individual element does across the entire run, not what the inner loop does in one iteration of the outer loop.
The
forloop runsntimes, no argument there. Thewhileloop's total iteration count across the whole algorithm is the part that's easy to miscount: every single element is pushed onto the stack exactly once (that happens once perforiteration, unconditionally) and popped at most once, ever — once an element is popped, it's answered and gone for good, it never goes back on the stack. So the grand total of allwhile-loop pops, summed over every iteration of theforloop, cannot exceedn— the samenelements that were pushed.That gives at most
npushes plus at mostnpops,2ntotal stack operations, however unevenly they're distributed across iterations (one iteration might pop five elements, the next might pop zero). This is amortized analysis: thewhileloop isn'tO(n)per outer iteration, it'sO(n)for the entire algorithm, which is what makes the whole thingO(n)overall rather thanO(n²). The tell an interviewer is listening for: "each element is pushed once and popped at most once."
Coach discussion points
- 1.
"Your monotonic stack has a
whileinside afor. Convince me it is notO(n²)." — The amortisation argument again. Follow up with "how many times in total can an index be popped?" if they stall. - 2.
"Trapping Rain Water also has a two-pointer solution. Walk me through why the monotonic-stack version resolves a bar's water differently, and which one you'd actually reach for in an interview." — Tests whether the candidate understands both lenses on the same problem rather than having memorized one path, and whether they can articulate the trade-off (stack needs
O(n)space; two-pointer needsO(1)). - 3.
"Your Trapping Rain Water stack popped a valley and the stack is now empty. What do you do, and why is that not a bug?" — Forces the candidate to state the empty-stack guard out loud: that valley has no left wall yet, contributes zero for now, and is correctly not pushed back.
- 4.
"For Maximal Rectangle, why can you call the exact same
largestRectangleAreafunction you wrote for the histogram problem, completely unchanged, once per row?" — Tests whether the candidate sees the row-reduction insight as a genuine algorithmic reduction (histogram problem solved once, reused rows times) rather than coincidence.
Complexity analysis
Daily Temperatures — Time O(n), Space O(n).
This one is worth defending carefully, because it contains a while inside a for and looks quadratic. Each index is pushed exactly once and popped at most once, so the total number of stack operations across the whole run is at most 2n. The inner loop is not O(n) per iteration — it is O(n) for the entire algorithm. That is the same amortisation argument as the sliding window in two-pointers.md, and interviewers ask for it in exactly the same way. Space is the stack, O(n) when temperatures are strictly decreasing and nothing ever resolves.
Trapping Rain Water (Monotonic Stack) — Time O(n), Space O(n).
Same amortisation argument as Daily Temperatures: each bar index is pushed exactly once and popped at most once across the entire run, so total stack operations are bounded by 2n regardless of the nested while inside the for. Space is the stack, worst case O(n) for a strictly decreasing elevation profile where nothing ever resolves.
Maximal Rectangle (Row-Reduced Histogram) — Time O(rows * cols), Space O(cols).
Each of the rows rows costs O(cols) to update the running heights array, plus another amortised O(cols) to run the Largest Rectangle in Histogram (LC 84) solver on it — so the total is rows * O(cols) = O(rows * cols). Space is O(cols) for the persisted heights array plus the per-row monotonic stack (discarded after each row), independent of rows.
A weather app shows, for each day, how many days until it gets warmer. A trading dashboard shows how long ago the price was last higher. Both need the nearest bigger value on one side of every item. Checking every later item for every item costs O(n²) and is too slow for a year of daily data. One pass with a stack costs O(n).
💡 Don't worry if popping from a stack feels unintuitive right now! A stack turns quadratic searches into one linear pass because each item is pushed once and popped at most once. As you work through the cards, the same three questions repeat: what do I store, when do I pop, and what does a pop answer.
Pushing values when you also need positions: this problem only needs values, because all values are distinct. Problems with repeated values or distances need indices on the stack (Daily Temperatures).
Scanning nums2 once per query: looking up each
nums1[i]with a fresh scan costs O(n1 * n2). Build the whole answer table once, then read it.Forgetting the default: a value that stays on the stack has no larger value to its right, so its answer is
-1. Usedict.get(x, -1).
Staff+ Systems Architecture & Hardware Considerations
Triggers: "next greater or smaller element", "largest rectangle in a histogram", "remove k digits to make the smallest number", "decode nested brackets". Each one needs the most recent unfinished item, and a stack gives exactly that.
Arrays of up to items. Every item is pushed once and popped at most once, so the cost is . Checking every later item for every item costs and is too slow.
Pushing values when the answer needs positions (push indices); using < where <= is needed when values repeat; forgetting the items left on the stack after the loop; popping while the stack is empty.