8.2 Min Stack
Source:
src/main/kotlin/stack/MinStack.ktPattern: dual-stack design · Core page
The Problem
Design a stack that supports push, pop, top, and getMin — all in $O(1)$ time.
- Constraints: up to $3 \times 10^4$ operations; $-2^{31} \le val \le 2^{31} - 1$.
Examples
push(-2), push(0), push(-3)
getMin() -> -3
pop() (pops -3)
top() -> 0
getMin() -> -2
Intuition — the minimum is a history, so it deserves its own stack
getMin() in $O(1)$ is the crux. Scanning the main stack for the minimum is $O(n)$; keeping a single cached min value breaks on pop (what was the minimum before the popped value was pushed?). The fix: the running minimum is itself a stack. Every time you push a value, push the current global minimum onto a second stack; every time you pop, pop the second stack too. The two stacks move in lockstep — minStack.top is always “the minimum of everything currently on the main stack”.
Two equivalent bookkeeping styles:
- Push min every time —
minStackalways has the same height asstack;getMinjust reads its top. Wasteful when minima don’t change (pushing 5 then 5 then 5 repeats 5 three times). - Push only when it changes (the repo’s version) — push
valontominStackonly ifval <= minStack.top; onpop, popminStackonly if the popped value was that minimum.minStackcan be shorter thanstack, but its top is still exactly the current minimum. This is the interview-grade version: less memory, same $O(1)$.
The subtle correctness point in style 2: why <= and not <? Duplicates. If two equal values equal the current minimum are pushed, both must land on minStack — otherwise the first pop removes the only copy and the true minimum is lost. <= keeps one copy per duplicate.
Approach 1 — Scan for the min on demand (too slow)
getMin walks the whole stack: $O(n)$ per call, up to $O(n^2)$ total. The problem’s $O(1)$ requirement exists precisely to forbid this.
Approach 2 — Parallel min stack (the repo’s version, optimal)
class MinStack() {
private val stack = ArrayDeque<Int>()
private val minStack = ArrayDeque<Int>() // history of running minimums
/**
* @param `val` value to push
*/
fun push(`val`: Int) {
stack.add(`val`)
if (minStack.isEmpty() || `val` <= minStack.last()) { // new minimum (or duplicate)
minStack.add(`val`)
}
}
fun pop() {
if (stack.isNotEmpty()) {
val poppedValue = stack.removeLast()
if (poppedValue == minStack.last()) { // did the minimum itself leave?
minStack.removeLast()
}
}
}
fun top(): Int {
return stack.last()
}
fun getMin(): Int {
return minStack.last()
}
}
import java.util.*;
public class MinStack {
private Deque<Integer> stack = new ArrayDeque<>();
private Deque<Integer> minStack = new ArrayDeque<>();
/** @param val value to push */
public void push(int val) {
stack.push(val);
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val); // new minimum (or duplicate)
}
}
public void pop() {
if (!stack.isEmpty() && stack.peek().equals(minStack.peek())) {
minStack.pop(); // the minimum itself left
}
stack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
#include <stack>
class MinStack {
std::stack<int> st;
std::stack<int> minSt; // history of running minimums
public:
/** @param val value to push */
void push(int val) {
st.push(val);
if (minSt.empty() || val <= minSt.top()) {
minSt.push(val); // new minimum (or duplicate)
}
}
void pop() {
if (!st.empty() && st.top() == minSt.top()) {
minSt.pop(); // the minimum itself left
}
st.pop();
}
int top() { return st.top(); }
int getMin() { return minSt.top(); }
};
class MinStack:
"""@param val: value to push"""
def __init__(self):
self.stack = []
self.min_stack = [] # history of running minimums
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) # new minimum (or duplicate)
def pop(self) -> None:
if self.stack.pop() == self.min_stack[-1]:
self.min_stack.pop() # the minimum itself left
def top(self) -> int:
return self.stack[-1]
def get_min(self) -> int:
return self.min_stack[-1]
#![allow(unused)]
fn main() {
struct MinStack {
stack: Vec<i32>,
min_stack: Vec<i32>, // history of running minimums
}
impl MinStack {
fn new() -> Self {
MinStack { stack: Vec::new(), min_stack: Vec::new() }
}
/// @param val value to push
fn push(&mut self, val: i32) {
self.stack.push(val);
if self.min_stack.is_empty() || val <= *self.min_stack.last().unwrap() {
self.min_stack.push(val); // new minimum (or duplicate)
}
}
fn pop(&mut self) {
if let (Some(v), Some(&m)) = (self.stack.pop(), self.min_stack.last()) {
if v == m {
self.min_stack.pop(); // the minimum itself left
}
}
}
fn top(&self) -> i32 {
*self.stack.last().unwrap()
}
fn get_min(&self) -> i32 {
*self.min_stack.last().unwrap()
}
}
}
Dry run
Input: the example sequence.
push(-2): stack=[-2], minStack empty -> push. minStack=[-2]
push(0): stack=[-2,0], 0 <= -2? no. minStack=[-2]
push(-3): stack=[-2,0,-3], -3 <= -2 -> push. minStack=[-2,-3]
getMin() -> minStack.top = -3 ✓
pop(): pops -3; -3 == minStack.top(-3) -> pop minStack. stack=[-2,0], minStack=[-2]
top() -> 0 ✓
getMin() -> -2 ✓
Now the duplicate case — the reason for <=:
push(1): stack=[1], minStack=[1]
push(1): stack=[1,1], 1 <= 1 -> push. minStack=[1,1]
pop(): pops 1; 1 == minStack.top(1) -> pop. stack=[1], minStack=[1]
getMin() -> 1 ✓ (with `<` instead of `<=`, the second 1 would never have been recorded,
and this pop would have emptied minStack — getMin would crash)
Complexity
Time. Every operation is $O(1)$ (amortized for the stacks’ growth):
$$ T_{\text{push/pop/top/getMin}}(n) = O(1) $$
Space. Two stacks:
$$ S(n) = O(n) $$
Variants & follow-ups
- MinStackShort (
src/main/kotlin/stack/MinStackShort.kt) — a one-stack trick storing2*val - minencoded values; the repo’s compact alternative. - Design A Stack With Increment Operations (
src/main/kotlin/stack/DesignAStackWithIncrementOperations.kt) — a lazyinc[]array plus a final pass; the same “sidecar state” reflex applied to range updates. - Max Stack / monotonic variants — the same dual-stack skeleton with
>instead of<=. - Interview follow-up: “Can you do it with one stack?” Yes — encode
2*val - currentMinon push and decode on pop (the repo’sMinStackShort), trading a little overflow care for one less structure. Worth mentioning as the O(1)-space curiosity, with the dual-stack as the primary answer.