Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°30

Min Stack

Easy StackData StructuresDesign

A stack that also returns its minimum in O(1) — no scanning. The trick is to carry the running minimum alongside each element. Replay a sequence of push/pop/top/getMin operations and return the outputs. Solve it in Python or TypeScript, with hidden tests.

The problem

Design a stack that supports push(x), pop(), top(), and getMin() — each in O(1) time. You are given a list of ops such as ["push",5], ["pop"], ["top"], ["getMin"]. Apply them in order and return the list of values produced by the read operations (top and getMin), in order.

SOLVE IT YOURSELF

Your turn — write it

Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.

YOUR TASK

Implement min_stack_ops(ops): maintain a stack plus a parallel "min so far" stack. On push, record min(x, current_min); on pop, drop both. Collect the outputs of every top and getMin.

CPython · WebAssembly
Examples and constraints 4 examples
EXAMPLE 1
Input ops = [["push",5],["push",3],["getMin"],["pop"],["getMin"],["top"]]
Output [3, 5, 5]
min is 3, then after pop the min is 5 and top is 5
EXAMPLE 2
Input ops = [["push",2],["push",2],["getMin"],["pop"],["getMin"]]
Output [2, 2]
duplicate mins must be tracked correctly
EXAMPLE 3
Input ops = [["push",3],["push",1],["pop"],["getMin"]]
Output [3]
popping the minimum brings the old minimum back; a single min variable would still say 1
EXAMPLE 4
Input ops = [["push",5],["pop"],["push",7],["getMin"],["top"]]
Output [7, 7]
edge case: the stack empties and refills, so the next push starts a fresh minimum
CONSTRAINTS
  • Every pop/top/getMin is called on a non-empty stack.
  • All four operations must be O(1) — do not scan the stack to find the minimum.
  • Duplicate minimum values must survive a single pop (the min only rises once all copies are gone).
Hints 4 ideas
HINTS — 4 IDEAS
  1. The naive getMin scans the stack — O(n). To get O(1), store the minimum as of each push next to the value.
  2. Keep a second stack mins: when you push x, push min(x, mins[-1]) (or just x if empty).
  3. On pop, pop both stacks together so mins[-1] always reflects the current stack.
  4. This is why duplicates matter: pushing the min twice puts it on mins twice, so one pop does not lose it.
Approach, complexity & discussion — open after you solve

Start from brute force

The obvious approach keeps one plain stack and answers getMin with min(stack). It is correct, because the scan sees every value currently on the stack, and it needs no space beyond the stack itself: O(1) extra. But each getMin costs O(n) for n values on the stack, so a sequence of q operations can cost O(n · q); with 105 pushes followed by 105 reads that is 1010 comparisons, and the statement requires O(1). The other tempting shortcut, a single current_min variable, is fast but wrong: after push 3, push 1, pop it still says 1, because nothing remembers what the minimum was before the 1 arrived.

The approach

A stack only changes at its top, so the minimum of everything below the top cannot change while those values stay buried. That means the minimum can be computed once, at push time, and stored with the value. The invariant is mins[i] == min(stack[0..i]) for every level i, so mins[-1] is always the minimum of the whole stack, and popping a level restores exactly the minimum that held before it was pushed.

The reference solution keeps stack and mins the same length. On "push" it appends x to stack and x if not mins else min(x, mins[-1]) to mins; the empty check covers both the first push and any push after the stack has been emptied. On "pop" it pops both lists in lockstep. "top" appends stack[-1] to out and "getMin" appends mins[-1], so every operation is a constant number of list operations. Duplicates need no special case: pushing a second 2 appends another 2 to mins, so one pop cannot lose it.

Walk through an example

Trace the first example, [["push",5],["push",3],["getMin"],["pop"],["getMin"],["top"]], watching mins mirror stack level for level.

stepopstackminsout
1push 5[5][5][]
2push 3[5, 3][5, 3][]
3getMin[5, 3][5, 3][3]
4pop[5][5][3]
5getMin[5][5][3, 5]
6top[5][5][3, 5, 5]

Complexity

Optimal: O(1) time per operation, so O(q) for q operations, and O(n) space for the two parallel stacks of up to n values, plus the output. Brute force (scan on every getMin): O(n) per getMin, up to O(n · q) overall, with O(1) extra space.

Edge cases to test

  • Duplicate minimums: [["push",2],["push",2],["getMin"],["pop"],["getMin"]] → [2, 2]. A space-saving variant that pushes onto mins only when x is strictly smaller loses the second 2 and has nothing left after the pop.
  • Popping the minimum: [["push",3],["push",1],["pop"],["getMin"]] → [3]. The previous minimum comes back, which a single current_min variable cannot do.
  • Ascending pushes: push 1, 2, 3 gives mins = [1, 1, 1], so after a pop, getMin is still 1.
  • Emptying and refilling: [["push",5],["pop"],["push",7],["getMin"],["top"]] → [7, 7]. The if not mins branch starts a fresh minimum instead of comparing with a value that is gone.
  • Negative values and zero: [["push",-1],["push",0],["getMin"],["top"]] → [-1, 0]. Never use 0 as an “empty” sentinel for the minimum.
  • An empty-stack pop, top or getMin is ruled out by the constraints; the Python reference would raise IndexError and the TypeScript one would return undefined.

Common mistakes

  • Recomputing the minimum by scanning the stack — that makes getMin O(n) instead of O(1).
  • Forgetting to pop the min-stack alongside the main stack.
  • Mishandling duplicate minimums — push the running min on every push (or handle ties explicitly) so a pop cannot lose the current min.
  • Tracking the minimum in one variable, which cannot be restored after the minimum is popped.

Follow-ups an interviewer asks next

Can you do it with O(1) extra space?
Yes, by encoding. Keep one variable m. When pushing x below m, push 2x − m instead and set m = x; the stored value is below the new minimum, which marks it. On pop, a stored value y below m means the previous minimum was 2m − y. top returns m for a marked value. In fixed-width integers, 2x − m can overflow.
How would you save space when the minimum rarely changes?
Push onto mins only when x <= mins[-1], and pop it only when the popped value equals mins[-1]. Pushing 5, 6, 7, 8 then stores one entry in mins instead of four. The comparison must be <=, not <, or a duplicate minimum is popped away while a copy is still on the stack.
Can you build a queue that reports its minimum?
Use two min-stacks: push onto an inbox, and pop from an outbox that is refilled by moving the whole inbox over when it runs empty. The queue’s minimum is the smaller of the two stacks’ minimums. Each element moves at most once, so every operation is amortized O(1). For a fixed sliding window, a monotonic deque does the same job.
What if you also need the maximum?
Add a third parallel stack, maxs, maintained the same way with max. The idea generalizes to any aggregate that can be combined from the value below: minimum, maximum, sum, and greatest common divisor all work. A median does not, because it cannot be computed from the previous median and one new value.

Where this shows up

“Carry an auxiliary aggregate alongside the structure” is the transferable idea — it scales to a max-stack, to min/max over a sliding window (a monotonic deque), and to running statistics. Min-stack is the smallest example of augmenting a data structure so an otherwise O(n) query becomes O(1).

Reference solution

Reference solution — Python
def min_stack_ops(ops):
    stack = []
    mins = []          # mins[i] = minimum of stack[0..i]
    out = []
    for op in ops:
        kind = op[0]
        if kind == "push":
            x = op[1]
            stack.append(x)
            mins.append(x if not mins else min(x, mins[-1]))
        elif kind == "pop":
            stack.pop()
            mins.pop()          # keep the two stacks in lock-step
        elif kind == "top":
            out.append(stack[-1])
        elif kind == "getMin":
            out.append(mins[-1])
    return out


print(min_stack_ops([["push",5],["push",3],["getMin"],["pop"],["getMin"],["top"]]))  # -> [3, 5, 5]
Reference solution — TypeScript
function minStackOps(ops: any[][]): number[] {
  const stack: number[] = [];
  const mins: number[] = [];        // mins[i] = min of stack[0..i]
  const out: number[] = [];
  for (const op of ops) {
    const kind = op[0];
    if (kind === "push") {
      const x = op[1] as number;
      stack.push(x);
      mins.push(mins.length ? Math.min(x, mins[mins.length - 1]) : x);
    } else if (kind === "pop") {
      stack.pop();
      mins.pop();                    // lock-step with the value stack
    } else if (kind === "top") {
      out.push(stack[stack.length - 1]);
    } else if (kind === "getMin") {
      out.push(mins[mins.length - 1]);
    }
  }
  return out;
}

console.log(minStackOps([["push",5],["push",3],["getMin"],["pop"],["getMin"],["top"]])); // -> [3,5,5]

Questions about Min Stack

How does a min stack return the minimum in O(1) time?

It stores the answer ahead of time. Alongside every value it pushes, it records the minimum of the stack at that moment, which is the smaller of the new value and the previous recorded minimum. Because a stack only ever changes at the top, those recorded minimums stay correct for every level below the top. Reading the minimum is then just reading the most recent record, and popping a value also pops its record, which brings back the minimum from before that value was pushed. Each operation is a constant number of list operations, so none of them depends on the stack size.

Why not just keep one variable for the minimum?

Because a single variable cannot be restored after the minimum is popped. Push 3 and then 1, and the variable correctly says 1. Pop the 1, and the true minimum is 3 again, but nothing in the variable remembers that 3 was the minimum before the 1 arrived; finding it would mean scanning the stack, which is O(n). The min stack solves this by keeping the minimum as of every level, so each pop hands back the previous answer. The space cost is one extra number per element, or less with the optimizations covered in the follow-ups.

Can a min stack be implemented with O(1) extra space?

Yes, with an encoding trick. Keep the current minimum m in one variable. When you push a value x smaller than m, store 2x minus m instead of x and set m to x. That stored number is smaller than the new minimum, which marks it as special. When you later pop a stored value y that is smaller than m, you know a minimum change happened at that level, and the previous minimum was 2m minus y. The top operation returns m whenever the stored value is marked. The catch is overflow in fixed-width integers, which Python avoids.

How does a min stack handle duplicate minimum values?

In the parallel-stack version shown here, duplicates need no special handling: every push records a minimum, so pushing 2 twice records 2 twice, and popping one copy leaves the other record in place. Duplicates become a trap in the space-saving variant that only records a new minimum when it changes. If that variant records only values strictly smaller than the current minimum, the second 2 is never recorded, and popping the first copy removes the only record while another 2 is still on the stack. Using less than or equal when deciding to record fixes it.

▶  Watch it explained
Finished this one? 0 / 76 Challenges done
Next3Sum Next on the “Interview classics” ladder · rung 5 of 10

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Challenges