How to Explain Min Stack in a Coding Interview (Step-by-Step)

How to Explain Min Stack in a Coding Interview (Step-by-Step)

Min Stack is a data structure design problem that looks deceptively simple — until you realise that a standard stack loses minimum information the moment you pop. The key is maintaining an auxiliary min-stack that tracks the running minimum at every level, ensuring getMin() is always O(1) no matter what gets pushed or popped.

In this post, you’ll learn exactly how to walk an interviewer through LeetCode 155: the problem with naïve approaches, the dual-stack design, the synchronisation rule between stacks, a full operation trace, and every edge case worth naming.


Problem Statement

LeetCode 155 Medium

Design a stack that supports push, pop, top, and retrieving the minimum element — all in O(1) time.

Implement the MinStack class with the following methods:

push(val)
Push element val onto the stack.
O(1)
pop()
Remove the element on top of the stack.
O(1)
top()
Get the top element.
O(1)
getMin()
Retrieve the minimum element in the stack.
O(1)

Example

Operations
MinStack minStack = new MinStack();
minStack.push(-2); // stack: [-2]
minStack.push(0); // stack: [-2, 0]
minStack.push(-3); // stack: [-2, 0, -3]
minStack.getMin(); // → -3
minStack.pop(); // stack: [-2, 0]
minStack.top(); // → 0
minStack.getMin(); // → -2 ← minimum restored after pop

Constraints

  • -231 <= val <= 231 - 1
  • pop(), top(), getMin() will always be called on a non-empty stack
  • At most 3 * 104 operations

What Is the Problem?

“I need to design a stack where every operation — including retrieving the current minimum — runs in O(1). A standard stack already gives O(1) for push, pop, and top. The hard part is getMin(). If I scan the whole stack for the minimum each time, that’s O(n). I need to track the minimum incrementally as elements are pushed and popped.”

Identifying that getMin() is the only challenging operation — and that the other three are trivially O(1) on a normal stack — tells the interviewer you’ve precisely located the difficulty before reaching for a solution.


Step 1: Compare Approaches Before Jumping to Code

✗ Too Slow — O(n) getMin
Scan on Every getMin
Keep a plain stack. Scan all elements to find the minimum on each getMin() call. Violates the O(1) constraint immediately.
~ Broken — O(1) but incorrect
Single Running Minimum
Track one min_val variable. Works for pushes, but fails on pop — if the minimum is popped, you can’t recover the previous minimum in O(1).
✓ Optimal — all O(1)
Auxiliary Min-Stack
A second stack shadows the main stack, storing the current minimum at every level. Push and pop both stacks in sync. getMin() is always the top of the min-stack.
“The single-variable approach breaks the moment you pop the current minimum — you’d need to scan the remaining stack to find the new minimum, which is O(n). The auxiliary stack solves this by remembering, at every depth, what the minimum was at that point in time. When you pop the main stack, you also pop the min-stack — and the new top of the min-stack is the correct minimum for whatever is left.”

The Key Insight — Track the Minimum at Every Level

💡 Core Insight

The minimum changes as elements are pushed and popped. What we need is not just the current minimum, but the minimum that was in effect at every stack depth. When we pop to depth k, we need the minimum of all elements at depth ≤ k — not the global minimum of everything ever pushed.

The auxiliary min-stack encodes exactly this. Entry min_stack[k] is the minimum of all elements in the main stack from index 0 through k. Popping both stacks simultaneously keeps them in sync, and the top of the min-stack is always the correct current minimum.


The Dual-Stack Design

📐 The Synchronisation Rule

On push(val): Push val to the main stack. Push min(val, min_stack[-1]) to the min-stack — the new minimum is either the incoming value or whatever was already the minimum.

On pop(): Pop both stacks simultaneously. The main stack loses its top element; the min-stack loses the minimum that was valid at that depth. The new min_stack[-1] is automatically correct for the remaining stack.

On getMin(): Return min_stack[-1]. No scan, no extra work — the answer is always sitting at the top.

Key invariant: The two stacks always have the same size. Every push adds to both; every pop removes from both.


Step 2: Explain the Algorithm in Plain English

1 Initialise two stacks

self.stack — the main stack holding all values. self.min_stack — the auxiliary stack where each entry is the minimum of the main stack at that depth. Both start empty.

2 push(val)

Append val to self.stack. Append min(val, self.min_stack[-1]) to self.min_stack — or just val if the min-stack is empty (first push). Both stacks grow together.

3 pop()

Pop from both self.stack and self.min_stack. The min-stack entry for this depth is no longer needed. After the pop, self.min_stack[-1] is the correct minimum for the remaining elements.

4 top() and getMin()

top() returns self.stack[-1]. getMin() returns self.min_stack[-1]. Both are single array lookups — O(1) with no computation.


Operation Trace — push(-2), push(0), push(-3), getMin, pop, top, getMin

Tracing every operation. Blue = main stack, Purple = min-stack. The top of the min-stack is always the current minimum.

push −2 Min-stack empty → push −2 to both stacks
Main Stack
−2
Min Stack
−2
current min = −2
push 0 0 > min(−2) → min stays −2 → push min(0, −2) = −2 to min-stack
Main Stack
0
−2
Min Stack
−2
−2
current min = −2
push −3 −3 < min(−2) → new minimum → push min(−3, −2) = −3 to min-stack
Main Stack
−3
0
−2
Min Stack
−3
−2
−2
current min = −3
getMin Return top of min-stack — no scan needed
Main Stack
−3
0
−2
Min Stack
−3
−2
−2
✓ getMin() → −3
pop Pop from both stacks — −3 removed from main, −3 removed from min-stack
↩ Both stacks lose their top entry simultaneously
Main Stack
0
−2
Min Stack
−2
−2
min restored to −2 automatically
top Return top of main stack
✓ top() → 0
getMin Return top of min-stack — correctly reflects the minimum after popping −3
Main Stack
0
−2
Min Stack
−2
−2
✓ getMin() → −2  (minimum correctly restored after pop)

Step 3: Talk Through the Code

class MinStack:

    def __init__(self):
        self.stack     = []   # Main stack — stores all values
        self.min_stack = []   # Auxiliary stack — top is always the current minimum

    def push(self, val: int) -> None:
        self.stack.append(val)
        # Current minimum is the smaller of val and whatever minimum was before
        if self.min_stack:
            self.min_stack.append(min(val, self.min_stack[-1]))
        else:
            self.min_stack.append(val)   # First push — val is trivially the minimum

    def pop(self) -> None:
        self.stack.pop()
        self.min_stack.pop()   # Always pop both — stacks are always the same size

    def top(self) -> int:
        return self.stack[-1]

    def getMin(self) -> int:
        return self.min_stack[-1]   # O(1) — answer is always at the top

Three things to narrate explicitly: (1) the two stacks are always kept the same size — every push adds to both, every pop removes from both; (2) the min-stack entry on push is min(val, min_stack[-1]) — this is what makes the minimum “sticky” even when the incoming value isn’t a new minimum; (3) getMin() is a single array lookup — no scan, no sort, no computation.


Step 4: Highlight Edge Cases

1 Popping the current minimum
“This is the whole point of the design. When the current minimum is popped, both stacks lose their top. The new top of the min-stack is the minimum of the remaining elements — exactly what was stored there when that element was pushed. No scan needed.”
2 Duplicate values — pushing the same value twice
“If we push 3, then push 3 again, the min-stack gets 3 twice. This is intentional — each push needs its own min-stack entry. If we popped the second 3 and only stored it once, the min-stack would go out of sync with the main stack.”
3 Pushing a value larger than all current values
“The minimum doesn’t change. We push min(val, min_stack[-1]) = min_stack[-1] — the same value as the current top. The min-stack grows in size but the minimum stays the same until this element is popped.”
4 Pushing a value smaller than all current values
“A new minimum is established. We push min(val, min_stack[-1]) = val to the min-stack. getMin() now correctly returns this new value.”
5 Negative values and integer boundaries
“The constraints allow values down to -2^31. The algorithm is value-agnostic — it uses Python’s min() which handles any integer correctly. No special casing for negatives.”

Step 5: State Time and Space Complexity

OperationTimeReason
push O(1) One append to each stack — both are O(1) amortised
pop O(1) One pop from each stack — O(1)
top O(1) Array index lookup — O(1)
getMin O(1) Top of min-stack — O(1), no scan
Space O(n) Two stacks each of size n — O(2n) = O(n)
“Every operation is O(1). The cost is space — we use O(n) for the auxiliary min-stack on top of the O(n) main stack. The follow-up question is often ‘can you do it with less space?’ You can optimise slightly by only pushing to the min-stack when the new value is ≤ the current minimum, and only popping the min-stack when the popped value equals the current minimum — but this complicates the code. The simpler always-in-sync approach is preferred unless the interviewer pushes for the optimisation.”

Interview Checklist for Min Stack

  • Restate — “all four operations must be O(1), including getMin”
  • Identify getMin as the only non-trivial operation — others are standard stack
  • Explain why scanning fails (O(n)) and why a single variable fails (can’t recover after pop)
  • Name the auxiliary min-stack pattern and state its invariant: top always holds the current minimum
  • Describe push: append to both stacks, min-stack gets min(val, min_stack[-1])
  • Describe pop: pop both stacks simultaneously — stacks are always the same size
  • Describe getMin: single O(1) lookup at min_stack[-1]
  • Explain why duplicate values must be pushed to the min-stack — keeps stacks in sync
  • Mention the space-optimised variant (only push to min-stack on new minimums) as a follow-up
  • State O(1) per operation and O(n) total space

If you can explain Min Stack cleanly, try these next:

💡 The follow-up interviewers love: “Can you reduce the space used by the min-stack?” Yes — instead of always pushing to the min-stack, only push when val <= min_stack[-1], and only pop when stack[-1] == min_stack[-1]. This means the min-stack only stores values that actually changed the minimum. Fewer entries, but the code is harder to reason about. Most interviewers prefer the always-in-sync approach for its clarity — but knowing the optimisation and being able to trade off the two designs cold is a strong signal.

Scroll to Top