155. Min Stack - Explanation

Problem Link

Description

Design a stack class that supports the push, pop, top, and getMin operations.

  • MinStack() initializes the stack object.
  • void push(int val) pushes the element val onto the stack.
  • void pop() removes the element on the top of the stack.
  • int top() gets the top element of the stack.
  • int getMin() retrieves the minimum element in the stack.

Each function should run in O(1)O(1) time.

Example 1:

Input: ["MinStack", "push", 1, "push", 2, "push", 0, "getMin", "pop", "top", "getMin"]

Output: [null,null,null,null,0,null,2,1]

Explanation:
MinStack minStack = new MinStack();
minStack.push(1);
minStack.push(2);
minStack.push(0);
minStack.getMin(); // return 0
minStack.pop();
minStack.top();    // return 2
minStack.getMin(); // return 1

Constraints:

  • -2^31 <= val <= 2^31 - 1.
  • pop, top and getMin will always be called on non-empty stacks.


Topics

Recommended Time & Space Complexity

You should aim for a solution with O(1) time for each function call and O(n) space, where n is the maximum number of elements present in the stack.


Hint 1

A brute force solution would be to always check for the minimum element in the stack for the getMin() function call. This would be an O(n) approach. Can you think of a better way? Maybe O(n) extra space to store some information.


Hint 2

We can use a stack to maintain the elements. But how can we find the minimum element at any given time? Perhaps we should consider a prefix approach.


Hint 3

We use an additional stack to maintain the prefix minimum element. When popping elements from the main stack, we should also pop from this extra stack. However, when pushing onto the extra stack, we should push the minimum of the top element of the extra stack and the current element onto this extra stack.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Stack Data Structure - Understanding LIFO (Last In First Out) operations including push, pop, and peek
  • Auxiliary Data Structures - Using additional stacks or variables to track extra state alongside the main data structure
  • Space-Time Tradeoffs - Trading extra memory for faster operations (e.g., O(n) space for O(1) getMin)

1. Brute Force

Intuition

To get the minimum value, this approach simply looks through all elements in the stack.
Since a normal stack does not store any extra information about the minimum, the only way to find it is to temporarily remove every element, track the smallest one, and then put everything back.
It's easy to understand but slow because each getMin call scans the entire stack.

Algorithm

  1. To push a value, append it to the stack.
  2. To pop, remove the top element of the stack.
  3. To top, return the last element.
  4. To getMin:
    • Create a temporary list.
    • Pop all elements from the stack while tracking the smallest value.
    • Push all elements back from the temporary list to restore the stack.
    • Return the smallest value found.
class MinStack:

    def __init__(self):
        self.stack = []

    def push(self, val: int) -> None:
        self.stack.append(val)

    def pop(self) -> None:
        self.stack.pop()

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

    def getMin(self) -> int:
        tmp = []
        mini = self.stack[-1]

        while len(self.stack):
            mini = min(mini, self.stack[-1])
            tmp.append(self.stack.pop())

        while len(tmp):
            self.stack.append(tmp.pop())

        return mini

Time & Space Complexity

  • Time complexity: O(n)O(n) for getMin()getMin() and O(1)O(1) for other operations.
  • Space complexity: O(n)O(n) for getMin()getMin() and O(1)O(1) for other operations.

2. Two Stacks

Intuition

Instead of searching the whole stack to find the minimum every time, we can keep a second stack that always stores the minimum value up to that point.
So whenever we push a new value, we compare it with the current minimum and store the smaller one on the minStack.
This guarantees that the top of minStack is always the minimum of the entire stack — allowing getMin() to work in constant time.

Algorithm

  1. Maintain two stacks:
    • stack → stores all pushed values.
    • minStack → stores the minimum so far at each level.
  2. On push(val):
    • Push val onto stack.
    • Compute the new minimum (between val and the current minimum on minStack).
    • Push this minimum onto minStack.
  3. On pop():
    • Pop from both stack and minStack to keep them aligned.
  4. On top():
    • Return the top of stack.
  5. On getMin():
    • Return the top of minStack, which is the current minimum.
class MinStack:
    def __init__(self):
        self.stack = []
        self.minStack = []

    def push(self, val: int) -> None:
        self.stack.append(val)
        val = min(val, self.minStack[-1] if self.minStack else val)
        self.minStack.append(val)

    def pop(self) -> None:
        self.stack.pop()
        self.minStack.pop()

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

    def getMin(self) -> int:
        return self.minStack[-1]

Time & Space Complexity

  • Time complexity: O(1)O(1) for all operations.
  • Space complexity: O(n)O(n)

3. One Stack

Intuition

This approach keeps only one stack and stores encoded values instead of the actual numbers.
The trick is to record the difference between the pushed value and the current minimum.
Whenever a new minimum is pushed, we store a negative encoded value, which signals that the minimum has changed.
Later, when popping such a value, we can decode it to restore the previous minimum.

This way, the stack internally keeps track of all minimum updates without needing a second stack — giving constant-time operations with minimal space.

Algorithm

  1. Maintain:
    • stack → stores encoded values (not the actual numbers)
    • min → stores the current minimum in the stack
  2. Push(val):
    • If the stack is empty:
      • Push 0 (difference) and set min = val.
    • Otherwise:
      • Push val - min.
      • If val is the new minimum, update min.
  3. Pop():
    • Pop the encoded value.
    • If this value is negative, it means the popped element was the minimum.
      • Restore the previous minimum using the encoded difference.
  4. Top():
    • If the encoded value is positive, return encoded + min.
    • If it's negative, the top actual value is simply min.
  5. getMin():
    • Return the current min.
class MinStack:
    def __init__(self):
        self.min = float('inf')
        self.stack = []

    def push(self, val: int) -> None:
        if not self.stack:
            self.stack.append(0)
            self.min = val
        else:
            self.stack.append(val - self.min)
            if val < self.min:
                self.min = val

    def pop(self) -> None:
        if not self.stack:
            return

        pop = self.stack.pop()

        if pop < 0:
            self.min = self.min - pop

    def top(self) -> int:
        top = self.stack[-1]
        if top > 0:
            return top + self.min
        else:
            return self.min

    def getMin(self) -> int:
        return self.min

Time & Space Complexity

  • Time complexity: O(1)O(1) for all operations.
  • Space complexity: O(n)O(n)

Common Pitfalls

Not Synchronizing the Two Stacks

When using the two-stack approach, some implementations only push to minStack when the new value is smaller than the current minimum. This optimization requires careful handling during pop(): you must only pop from minStack when the popped value equals the current minimum. Forgetting this synchronization causes minStack to become misaligned with the main stack, returning incorrect minimums.

Integer Overflow in the Encoded Value Approach

The one-stack solution stores val - min as encoded values. When dealing with extreme integer values (e.g., Integer.MIN_VALUE and Integer.MAX_VALUE), this subtraction can overflow. Using long instead of int for the encoded values and the minimum prevents this issue in languages with fixed-width integers.