Min / Max Stack
Design a stack that supports push, pop, top, and getMin (or getMax) — all in O(1) time. The trick: maintain a second auxiliary stack that tracks the current minimum (or maximum) at every level of the main stack, so the answer is always sitting at the top of the auxiliary stack.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- The code
- Watch it happen, frame by frame
- Space optimization: store only when minimum changes
- Max Stack variant
- Both min and max simultaneously
- Where this pattern appears
- Common traps
- Check yourself
- Practice problems
Before we start
The Min Stack is a classic design question — it appears in nearly every FAANG interview list. The challenge is that a standard stack gives you O(n) getMin (scan all elements). This note shows you why that's bad and exactly how to get O(1) without sacrificing any of the other stack operations. By the end you will be able to:
- Explain the auxiliary stack idea and why it stays synchronized with the main stack.
- Write the full
MinStackclass from memory with all four operations. - Adapt it to a Max Stack and handle a tricky "pop restores previous min" edge case correctly.
Picture this first (no code yet)
A real-life story
A restaurant chef keeps a stack of order tickets. At any moment, the manager wants to instantly know "what is the cheapest dish currently on the stack?" The chef cannot afford to read every ticket every time the manager asks — service would grind to a halt.
Solution: the chef keeps a second, smaller notepad. Every time a new ticket is placed on the stack, the chef writes on the notepad: "current cheapest is ___ (either this new ticket's price or whatever was cheapest before)." Every time a ticket is removed, the chef also tears off the top notepad page.
The notepad's top page always says the current cheapest — O(1) lookup, O(1) update.
The actual problem
Min Stack (LC #155):
Design a stack that supports:
push(val)— push element onto the stack.pop()— remove top element.top()— get the top element.getMin()— retrieve the minimum element in the stack.All operations must run in O(1) time.
First, the slow way (so you feel the pain)
Keep a single list. getMin() scans all n elements: min(self.stack). For 1,000,000 elements and 1,000,000 getMin() calls, that is 10¹² operations — over an hour. Trading a tiny bit of extra space (O(n) for a second stack) brings this to O(1) per operation.
The turning point
Pause & think
The hard part is pop(). Suppose the current minimum is 3 and you push 1 (new minimum is 1). Now you pop() that 1. The minimum should revert to 3. How do you know what the minimum was before you pushed 1?
You need to "remember" the minimum at every level of the stack — not just the global minimum. The auxiliary min_stack stores a snapshot of the minimum at each level: when you push, snapshot the new minimum; when you pop, discard the snapshot.
The one idea to remember
The entire pattern in one sentence
Pair the main stack with a min_stack where each level records the running minimum at that point — push the new minimum alongside each element, pop it alongside each element.
The code
class MinStack:
def __init__(self):
self.stack = [] # main stack: holds actual values
self.min_stack = [] # auxiliary: top is current minimum
def push(self, val: int) -> None:
self.stack.append(val)
# new minimum is: val if min_stack is empty, else min of val and current min
curr_min = val if not self.min_stack else min(val, self.min_stack[-1])
self.min_stack.append(curr_min)
def pop(self) -> None:
self.stack.pop()
self.min_stack.pop() # discard corresponding minimum snapshot
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.min_stack[-1] # always the current min — O(1)
Line-by-line narration:
self.stack— the real stack.self.min_stack— a parallel stack;min_stack[i]is the minimum of all elements instack[0..i].push: append val to main, compute new snapshot min (min of val and previous snapshot), append to min_stack. Both stacks grow in lockstep.pop: discard from both. min_stack automatically reverts to the previous snapshot — the old minimum.top: standard stack top.getMin: justmin_stack[-1]— O(1).
Watch it happen, frame by frame
Operations: push(5), push(3), push(7), push(2), pop(), getMin()
push(5): stack=[5], min_stack=[5]
push(3): stack=[5,3], min_stack=[5,3] (min(3,5)=3)
push(7): stack=[5,3,7], min_stack=[5,3,3] (min(7,3)=3)
push(2): stack=[5,3,7,2], min_stack=[5,3,3,2]
getMin() → min_stack[-1] = 2 ✅
pop(): stack=[5,3,7], min_stack=[5,3,3] (2 popped from both)
getMin() → min_stack[-1] = 3 ✅ (reverted to previous min)
The critical moment: popping 2 correctly reveals that the minimum is now 3 again — no scan needed.
Space optimization: store only when minimum changes
If memory is tight, only push to min_stack when the new value is ≤ the current minimum. Pop from min_stack only when the popped value equals the current minimum:
def push(self, val: int) -> None:
self.stack.append(val)
if not self.min_stack or val <= self.min_stack[-1]:
self.min_stack.append(val)
def pop(self) -> None:
val = self.stack.pop()
if val == self.min_stack[-1]:
self.min_stack.pop()
Trade-off: uses less space when many pushes don't change the minimum, but requires the == comparison on pop. The full parallel approach is simpler and avoids this subtlety.
Max Stack variant
Identical structure, just track the running maximum:
class MaxStack:
def __init__(self):
self.stack = []
self.max_stack = []
def push(self, val):
self.stack.append(val)
curr_max = val if not self.max_stack else max(val, self.max_stack[-1])
self.max_stack.append(curr_max)
def pop(self):
self.stack.pop()
self.max_stack.pop()
def getMax(self):
return self.max_stack[-1]
Both min and max simultaneously
If a problem needs O(1) getMin AND getMax, maintain two auxiliary stacks — min_stack and max_stack — both synchronized with the main stack:
def push(self, val):
self.stack.append(val)
self.min_stack.append(min(val, self.min_stack[-1]) if self.min_stack else val)
self.max_stack.append(max(val, self.max_stack[-1]) if self.max_stack else val)
def pop(self):
self.stack.pop()
self.min_stack.pop()
self.max_stack.pop()
Where this pattern appears
Trigger words:
- "design a stack that supports getMin/getMax in O(1)"
- "sliding window minimum/maximum" — a related but different technique using a deque (Chapter 6.02)
- any problem where you need to query a property of the entire stack's current contents in O(1)
Problems:
- Min Stack (LC #155): direct application.
- Max Stack (LC #716): also requires
popMax()— needs a doubly linked list + heap combination for full O(log n) complexity. - Maximum Frequency Stack (LC #895): design a stack where
popreturns the most frequent element — uses a frequency map and a stack per frequency level.
Common traps
Watch out for these
- Storing the global minimum instead of a snapshot. If you maintain only a single
self.current_minvariable, popping the minimum leaves you with no way to recover the previous minimum without scanning. You need the full parallel stack. - Using
<instead of<=in the space-optimized version. If equal values are pushed, e.g.,push(3), push(3), and you only push tomin_stackon strict<, thenpop()of one 3 would pop the onlymin_stackentry even though another 3 remains. Use<=to push duplicates. - Not keeping both stacks in sync on pop. The main stack and min_stack must always have the same length in the full parallel approach. Popping from one but not the other causes stale minimum values.
Remember this forever
Min Stack
class MinStack:
def __init__(self):
self.s, self.m = [], []
def push(self, val):
self.s.append(val)
self.m.append(min(val, self.m[-1]) if self.m else val)
def pop(self):
self.s.pop(); self.m.pop()
def top(self): return self.s[-1]
def getMin(self): return self.m[-1]
Key: m[i] = min of s[0..i]. Push snapshots the new minimum. Pop discards the snapshot. O(1) all operations, O(n) space.
Check yourself
After `push(5), push(3), push(7), pop()`, what does `getMin()` return and why?
getMin() returns 3. After the three pushes, min_stack = [5, 3, 3] (5 is the min after push(5), 3 after push(3), 3 still after push(7) because min(7,3)=3). After pop() removes 7, both stacks drop their top entry: stack=[5,3], min_stack=[5,3]. min_stack[-1] = 3 — the current minimum. This is correct: only 5 and 3 remain, and 3 is the smallest.
Why does the space-optimized version use `<=` (not `<`) to decide when to push to min_stack?
Consider push(3), push(3), pop(). If we use strict <: first push(3) → min_stack=[3]. Second push(3) → 3 is NOT less than 3, so we don't push → min_stack=[3]. Now pop() removes 3 from the main stack. Should we pop from min_stack? The popped value (3) equals min_stack[-1] (3) → yes, pop → min_stack=[]. But the first 3 is still in the main stack, and getMin() on an empty min_stack crashes! Using <= means both 3s are pushed to min_stack: min_stack=[3,3]. Popping one 3 leaves min_stack=[3] — correct.
Practice problems
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Min Stack | Medium | Parallel auxiliary stack; O(1) all ops | LC #155 |
| Maximum Frequency Stack | Hard | Stack per frequency level; push increments freq map | LC #895 |
| Sliding Window Maximum | Hard | Monotonic deque (not stack) — see Chapter 6.02 | LC #239 |
Next up: Largest Rectangle in Histogram — the most celebrated monotonic stack application, finding the largest rectangle in O(n) by using previous-smaller and next-smaller boundaries simultaneously.