242 lines
9.5 KiB
YAML
242 lines
9.5 KiB
YAML
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
|
|
|
|
|
|
|
|
**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.
|