title: Min Stack
slug: min-stack
difficulty: medium
leetcode_id: 155
leetcode_url: https://leetcode.com/problems/min-stack/
categories:
  - stack
patterns:
  - monotonic-stack

description: |
  Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.

  Implement the `MinStack` class:

  - `MinStack()` initialises 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.

  You must implement a solution with **O(1) time complexity** for each function.

constraints: |
  - `-2^31 <= val <= 2^31 - 1`
  - Methods `pop`, `top` and `getMin` operations will always be called on **non-empty** stacks.
  - At most `3 * 10^4` calls will be made to `push`, `pop`, `top`, and `getMin`.

examples:
  - input: |
      ["MinStack","push","push","push","getMin","pop","top","getMin"]
      [[],[-2],[0],[-3],[],[],[],[]]
    output: "[null,null,null,null,-3,null,0,-2]"
    explanation: |
      MinStack minStack = new MinStack();
      minStack.push(-2);
      minStack.push(0);
      minStack.push(-3);
      minStack.getMin(); // return -3
      minStack.pop();
      minStack.top();    // return 0
      minStack.getMin(); // return -2

explanation:
  intuition: |
    Think of this problem like maintaining two parallel records: one for the actual items on the stack, and one for "what's the smallest item from here down?"

    A regular stack gives us O(1) for push, pop, and top — those operations only touch the top element. But finding the minimum normally requires scanning the entire stack, which is O(n).

    The key insight is that we can **precompute the minimum at each level** of the stack. When we push a new element, we know the current minimum (from the previous level) and the new value — the new minimum is simply the smaller of the two. When we pop, the minimum "reverts" to whatever it was before we pushed that element.

    Imagine each element carrying a "badge" that says: "When I'm on top, the minimum is X." This badge never changes once assigned, because elements below me will never change while I exist on the stack.

  approach: |
    We solve this using **two synchronised stacks**: one for values, one for minimums.

    **Step 1: Design the data structure**

    - `stack`: A standard stack storing all pushed values
    - `min_stack`: A parallel stack where each entry holds the minimum value at that level

    &nbsp;

    **Step 2: Implement push**

    - Push `val` onto `stack`
    - Compute the new minimum: `min(val, current_min)` where `current_min` is `min_stack[-1]` (or `val` if empty)
    - Push this minimum onto `min_stack`

    &nbsp;

    **Step 3: Implement pop**

    - Pop from both `stack` and `min_stack` simultaneously
    - This maintains the synchronisation — when the top value leaves, so does its associated minimum

    &nbsp;

    **Step 4: Implement top and getMin**

    - `top()`: Return `stack[-1]`
    - `getMin()`: Return `min_stack[-1]`

    &nbsp;

    Both stacks always have the same length, so `getMin()` always reflects the minimum considering all elements currently in the stack.

  common_pitfalls:
    - title: Scanning for Minimum on Every Call
      description: |
        A naive approach is to iterate through the entire stack each time `getMin()` is called:

        ```python
        def getMin(self):
            return min(self.stack)  # O(n) - too slow!
        ```

        This violates the O(1) requirement. With up to `3 * 10^4` operations, repeatedly scanning could result in O(n²) total time.
      wrong_approach: "Scanning the stack on each getMin() call"
      correct_approach: "Track minimum at each stack level"

    - title: Single Variable for Minimum
      description: |
        Storing only a single `self.min_value` variable seems efficient, but fails on pop:

        ```python
        def pop(self):
            if self.stack[-1] == self.min_value:
                # What's the new minimum? We don't know!
                self.min_value = ???
        ```

        When you pop the current minimum, you'd need to scan the remaining stack to find the new minimum — back to O(n).
      wrong_approach: "Single variable tracking current minimum"
      correct_approach: "Stack of minimums to restore previous min on pop"

    - title: Forgetting Empty Stack Edge Case in Push
      description: |
        When the stack is empty, there's no previous minimum to compare against:

        ```python
        def push(self, val):
            # This crashes if min_stack is empty!
            new_min = min(val, self.min_stack[-1])
        ```

        Handle the empty case by treating the first element as the minimum.
      wrong_approach: "Always accessing min_stack[-1]"
      correct_approach: "Check if min_stack is empty first, or use infinity as initial comparison"

  key_takeaways:
    - "**Auxiliary data structures**: When one operation is too slow, consider storing precomputed information in a parallel structure"
    - "**State at each level**: The minimum-at-each-level technique extends to other 'aggregate' queries (max, sum, etc.)"
    - "**Space-time tradeoff**: We use O(n) extra space to achieve O(1) time for all operations"
    - "**Design problems**: Often require thinking about invariants — here, `min_stack[i]` always equals `min(stack[0:i+1])`"

  time_complexity: "O(1) for all operations. Push, pop, top, and getMin each perform a constant number of operations."
  space_complexity: "O(n) where `n` is the number of elements in the stack. We store each element twice — once in the main stack and once in the min stack."

solutions:
  - approach_name: Two Stacks
    is_optimal: true
    code: |
      class MinStack:
          def __init__(self):
              # Main stack for all values
              self.stack = []
              # Parallel stack tracking minimum at each level
              self.min_stack = []

          def push(self, val: int) -> None:
              self.stack.append(val)
              # New minimum is smaller of val and current min (or just val if empty)
              if self.min_stack:
                  new_min = min(val, self.min_stack[-1])
              else:
                  new_min = val
              self.min_stack.append(new_min)

          def pop(self) -> None:
              # Remove from both stacks to stay synchronised
              self.stack.pop()
              self.min_stack.pop()

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

          def getMin(self) -> int:
              # min_stack top always holds current minimum
              return self.min_stack[-1]
    explanation: |
      **Time Complexity:** O(1) for all operations — each method performs constant-time stack operations.

      **Space Complexity:** O(n) — we store each element twice.

      The min_stack maintains an invariant: `min_stack[i]` equals the minimum value among `stack[0]` through `stack[i]`. When we pop, both stacks shrink together, automatically restoring the correct minimum.

  - approach_name: Single Stack with Pairs
    is_optimal: true
    code: |
      class MinStack:
          def __init__(self):
              # Each entry is (value, min_at_this_level)
              self.stack = []

          def push(self, val: int) -> None:
              # Calculate minimum including this new value
              if self.stack:
                  current_min = min(val, self.stack[-1][1])
              else:
                  current_min = val
              # Store both the value and the running minimum
              self.stack.append((val, current_min))

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

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

          def getMin(self) -> int:
              return self.stack[-1][1]
    explanation: |
      **Time Complexity:** O(1) for all operations.

      **Space Complexity:** O(n) — each stack entry stores two values.

      This variation combines both pieces of information into a single stack. Each entry is a tuple of `(value, minimum_at_this_level)`. Functionally equivalent to the two-stack approach, but with slightly cleaner code.

  - approach_name: Min Stack with Optimised Space
    is_optimal: false
    code: |
      class MinStack:
          def __init__(self):
              self.stack = []
              # Only store min when it changes
              self.min_stack = []

          def push(self, val: int) -> None:
              self.stack.append(val)
              # Only push to min_stack if val is <= current minimum
              if not self.min_stack or val <= self.min_stack[-1]:
                  self.min_stack.append(val)

          def pop(self) -> None:
              val = self.stack.pop()
              # Only pop from min_stack if we're removing the current minimum
              if val == self.min_stack[-1]:
                  self.min_stack.pop()

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

          def getMin(self) -> int:
              return self.min_stack[-1]
    explanation: |
      **Time Complexity:** O(1) for all operations.

      **Space Complexity:** O(n) worst case, but often less in practice.

      This optimisation only adds to `min_stack` when we see a new minimum (or equal value). The space savings depend on the input — if values are strictly increasing, `min_stack` holds only one element. Note the `<=` instead of `<`: we must track duplicates of the minimum value, otherwise popping one copy would incorrectly update the minimum.
