Push and pop in O(1) is free. The trick is answering "what's the minimum right now?" instantly — without ever scanning.
Build a stack with three operations, all in O(1): push a value, pop the top, and return the
current minimum. Push and pop are trivial on any stack. The hard constraint is findMin() in
constant time.
Scanning the stack for its smallest value is O(N) — banned by the requirement. So keep a running
min variable? Push handles it fine with min(oldMin, x). But pop breaks it:
Push 5, 3 → min is 3. Pop the 3 and the stack is [5], so the min should be 5 — but a
single variable never recorded it. The real need is minimum history: what was the minimum
before each element arrived.
Every push is a save point that records the stack and its minimum. Popping loads the previous save — the min it had back then comes back automatically.
A second stack walks in lockstep with the first. Each slot remembers the smallest value seen up to that depth, so its top is always today's minimum.
Because pops reverse pushes in LIFO order, the minimums are needed back in reverse too — which is exactly what a stack gives you.
Push values and pop them. The main stack holds your
values; the min stack pushes
min(x, currentMin) alongside every value. Watch how the top of the min stack is always the answer
to findMin() — and how popping restores the previous minimum for free.
Push 2, 3, 1, 4 and the minimum after each push is 2, 2, 1, 1. The two stacks line up
slot for slot:
| Depth | Main stack | Min stack | findMin |
|---|---|---|---|
| 1 | 2 |
2 |
2 |
| 2 | 2 3 |
2 2 |
2 |
| 3 | 2 3 1 |
2 2 1 |
1 |
| 4 | 2 3 1 4 |
2 2 1 1 |
1 |
Because each value carries its own paired minimum, popping never requires a recompute — remove the top of both stacks and the previous minimum is sitting right there.
import java.util.*; class MinStack { private final Deque<Integer> stack = new ArrayDeque<>(); private final Deque<Integer> minStack = new ArrayDeque<>(); void push(int x) { stack.push(x); if (minStack.isEmpty()) minStack.push(x); else minStack.push(Math.min(x, minStack.peek())); } void pop() { if (stack.isEmpty()) return; stack.pop(); minStack.pop(); // stay in lockstep } int findMin() { return stack.isEmpty() ? -1 : minStack.peek(); } }
Every operation is a constant number of stack calls, so push, pop, and findMin are all O(1).
Both stacks hold up to N items, giving O(N) space — the classic trade of memory for query speed. Prefer
ArrayDeque over the legacy Stack class.
<=A space-saving variant pushes to the min stack only when a value is a new minimum, and pops it only when the
popped value equals the current min. It works — but the comparison must be <=, not
<.
2, then 2 again. With <, the second 2 never reaches the min
stack, so min = [2]. Pop one 2, the rule removes that lone 2, and now the min stack is empty while
a 2 still sits in main. Using <= records both copies, so each pop retires exactly one.
// sparse variant — note the <= void push(int x) { stack.push(x); if (minStack.isEmpty() || x <= minStack.peek()) minStack.push(x); } void pop() { int removed = stack.pop(); if (removed == minStack.peek()) minStack.pop(); }
It's still O(N) in the worst case — a strictly decreasing input makes every value a new minimum. For learning, the push-to-both version is safer; the compression just trades a little clarity for occasional space.
Two parallel stacks and one stack of (value, minSoFar) pairs are the same idea wearing different
clothes. Each entry carries the minimum as of when it was pushed, so the top always knows the current minimum.
record Entry(int value, int minSoFar) {} Deque<Entry> stack = new ArrayDeque<>(); void push(int x) { int m = stack.isEmpty() ? x : Math.min(x, stack.peek().minSoFar()); stack.push(new Entry(x, m)); } int findMin() { return stack.isEmpty() ? -1 : stack.peek().minSoFar(); }
2*x − currentMin to smuggle the previous minimum into the
stored number, using just one min variable and no second stack. It works but overflows easily (use
long) and is easy to get wrong — reach for it only after the two-stack version is second nature.
This isn't really about minimums. It's a template: whenever a data structure needs an extra aggregate query in
O(1), store that aggregate at every depth so it updates incrementally. The recurrence is always
state[i] = combine(state[i−1], newValue).
The same instinct powers prefix sums, segment trees, and augmented trees: store extra information when maintaining it beats recomputing it later.