You already know stacks. Push, pop, peek — all O(1), all touching a single end of the array. Nothing to think about. But now someone changes the requirements. You're building a stock trading dashboard. At any given moment, a user wants to see the lowest price in the current window of recent transactions. They push new prices, pop expired ones, and between every operation they ask: “What's the minimum right now?”
Your first instinct: keep a variable called min. When you push a value, compare it to min and update if smaller. Easy. O(1) on push. O(1) on getMin — just read the variable. Done?
Not quite. Pop a value. Was it the minimum? If so, what's the new minimum? You don't know. The old minimum is gone, and you have no record of what the second-smallest value was. You'd have to scan the entire remaining stack to find it — O(n). Every pop that removes the minimum triggers a full scan.
Maybe you decide that's acceptable. But consider: in a worst-case sequence of interleaved pushes and pops, you might hit that O(n) scan on half your operations. For a dashboard processing thousands of transactions per second, “occasionally O(n)” is not the same as “always O(1).”
So here's the open question: how would you remember the previous minimum without scanning?
Given the constraint 'O(1) push and O(1) getMin, no scanning allowed', which approach would actually work?
A history of values where the most recent one comes off first — that's a stack. You need a stack to track your stack. The idea sounds circular, but it's the trick. Now go feel it work.
Two stacks, side by side: the data stack on the left and the min stack on the right. Push and pop values from a scripted sequence and, before each operation, predict how the min stack responds.
Pay attention to when the min stack doesn't change — that's where the insight lives. Not every push creates a new minimum, and not every pop disturbs the old one. The min stack is selective.
The min stack is empty. After pushing 3, what will the min stack top be?
Now name what you just discovered. The rule is precise: when you push a value that's less than or equal to the current minimum, push it onto the min stack too. When you pop a value that equals the min stack's top, pop the min stack. At any point, the top of the min stack IS the current minimum. No scanning, no sorting, no variables going stale. O(1) getMin, always.
The elegance is that the min stack mirrors the data stack's lifecycle but only stores the moments when the minimum changed. It's a compressed timeline of minimums. The data stack changed on every operation; the min stack was quieter — it only moved when the minimum itself changed. That selectivity is what makes the whole structure efficient.
The visual stacks you just operated on map directly to two arrays in code: this.stack and this.minStack. Below is a MinStack class with push, pop, and getMin methods, plus a usage sequence. Step through each operation, watch the active line, and predict each getMin() result before the trace reveals it.
The conditional in push is the “≤ current min” check you discovered. The conditional in pop is the “equals min top” check. And getMin is a single peek at the min stack — the same move you made by eye three slides ago.
class MinStack { stack: number[] = [] minStack: number[] = [] push(val: number): void { this.stack.push(val) if (!this.minStack.length || val <= this.top(this.minStack)) { this.minStack.push(val) } } pop(): void { const val = this.stack.pop()! if (val === this.top(this.minStack)) { this.minStack.pop() } } getMin(): number { return this.top(this.minStack) } private top(s: number[]): number { return s[s.length - 1] }}// Usage:const ms = new MinStack()ms.push(5)ms.push(3)ms.push(7)const a = ms.getMin() // → ?ms.pop() // removes 7ms.push(1)const b = ms.getMin() // → ?ms.pop() // removes 1const c = ms.getMin() // → ?Three getMin() calls, three O(1) lookups. The first returned 3 (the running minimum after pushing 5, 3, 7). After popping 7 and pushing 1, the second returned 1 — the new minimum. After popping 1, the third returned 3 — the min stack remembered the old minimum, sitting there all along. No scanning, no recomputation. Every answer was already at the top of the min stack, waiting.
You've watched the min stack in action and traced the code. Now construct it. Fill in the three critical blanks: the push condition that decides when a value joins the min stack, the pop condition that decides when the min stack shrinks, and the getMin expression that reads the current minimum.
Each blank has exactly one correct answer — and the wrong options represent real misconceptions. The first wrong push condition checks the wrong end of the min stack. The second wrong pop condition would destroy the min history on every pop. And the naive getMin scans the whole stack, defeating the purpose of the data structure entirely.
Three blanks, three lines of real logic. The push condition handles both the empty min stack and the comparison. The pop condition fires only when the removed value IS the current minimum. And getMin peeks at the top of the min stack — constant time, always correct, because the structure maintains the invariant on every push and pop.
The time guarantee is absolute: every operation runs in O(1). But what about space? The min stack uses at most O(n) extra memory in the worst case — and that worst case is concrete: push a monotonically decreasing sequence like 10, 9, 8, 7, 6, 5. Every value is a new minimum, so every value gets pushed onto the min stack too. The min stack becomes a full copy of the data stack. In practice, with random data, the min stack grows much more slowly because most values aren't new minimums.
There's an alternative design worth knowing: instead of only pushing new minimums, push the current minimum onto the min stack on every push. This simplifies the pop logic — you always pop both stacks, no comparison needed — at the cost of guaranteed O(n) space. The version you built is the standard one because it's more space-efficient on average, but both are valid O(1)-time solutions. The tradeoff is between conditional logic (our version) and unconditional space usage (the simpler variant). Interviewers may ask you to discuss both.