title: Min Stack slug: min-stack difficulty: medium leetcode_id: 155 leetcode_url: https://leetcode.com/problems/min-stack/ categories: - stack patterns: - slug: monotonic-stack is_optimal: true function_signature: "class MinStack" test_cases: visible: - input: operations: ["MinStack", "push", "push", "push", "getMin", "pop", "top", "getMin"] args: [[], [-2], [0], [-3], [], [], [], []] expected: [null, null, null, null, -3, null, 0, -2] - input: operations: ["MinStack", "push", "push", "getMin", "pop", "getMin"] args: [[], [1], [2], [], [], []] expected: [null, null, null, 1, null, 1] hidden: - input: operations: ["MinStack", "push", "push", "push", "top", "pop", "getMin", "pop", "getMin", "pop", "push", "top", "getMin", "push", "top", "getMin", "pop", "getMin"] args: [[], [2147483646], [2147483646], [2147483647], [], [], [], [], [], [], [2147483647], [], [], [-2147483648], [], [], [], []] expected: [null, null, null, null, 2147483647, null, 2147483646, null, 2147483646, null, null, 2147483647, 2147483647, null, -2147483648, -2147483648, null, 2147483647] - input: operations: ["MinStack", "push", "push", "push", "getMin", "top", "pop", "getMin"] args: [[], [0], [1], [0], [], [], [], []] expected: [null, null, null, null, 0, 0, null, 0] - input: operations: ["MinStack", "push", "getMin", "push", "getMin", "push", "getMin"] args: [[], [5], [], [3], [], [7], []] expected: [null, null, 5, null, 3, null, 3] 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   **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`   **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   **Step 4: Implement top and getMin** - `top()`: Return `stack[-1]` - `getMin()`: Return `min_stack[-1]`   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.