LeetCode 155 - Min Stack
The problem asks us to design a custom stack data structure that behaves like a normal stack, while also supporting an additional operation called getMin(). This operation must return the minimum value currently stored in the stack, and it must do so in constant time, O(1).
Difficulty: 🟡 Medium
Topics: Stack, Design
Solution
Problem Understanding
The problem asks us to design a custom stack data structure that behaves like a normal stack, while also supporting an additional operation called getMin(). This operation must return the minimum value currently stored in the stack, and it must do so in constant time, O(1).
A normal stack already supports these standard operations efficiently:
push(val)inserts an element onto the toppop()removes the top elementtop()returns the top element
The challenge is adding getMin() while preserving constant time complexity for every operation.
The input in LeetCode is represented as a sequence of method calls. For example:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
This means:
- Create a new
MinStack - Push
-2 - Push
0 - Push
-3 - Retrieve the minimum
- Pop the top
- Retrieve the top
- Retrieve the minimum again
The expected output is:
[null,null,null,null,-3,null,0,-2]
The constraints are important:
- Up to
3 * 10^4operations - Values can range from
-2^31to2^31 - 1 pop,top, andgetMinare always called on non-empty stacks
These constraints tell us that efficiency matters. A solution that scans the entire stack every time getMin() is called would become too slow in the worst case.
Several edge cases are important:
- Multiple identical minimum values
- Popping the current minimum
- Negative numbers
- A stack with only one element
- Alternating pushes and pops that frequently change the minimum
The problem guarantees that invalid operations will never occur, so we do not need to handle popping from an empty stack.
Approaches
Brute Force Approach
The simplest implementation uses a single stack.
push()appends the valuepop()removes the last valuetop()returns the last valuegetMin()scans the entire stack and returns the smallest element
This approach is correct because scanning every element always finds the true minimum. However, the problem specifically requires constant time operations. Scanning the stack takes O(n) time, which violates the requirement.
If there are many getMin() calls on a large stack, the performance becomes inefficient.
Optimal Approach
The key insight is that we should not recompute the minimum repeatedly. Instead, we should store enough information during each push() operation so that the minimum is always immediately available.
A common technique is to maintain a second stack called min_stack.
- The main stack stores all values
- The min stack stores the minimum value seen so far at each depth
For every push:
- Push the value onto the main stack
- Push the new minimum onto the min stack
For every pop:
- Pop from both stacks
This ensures:
- The top of the main stack is always the current top element
- The top of the min stack is always the current minimum
Every operation becomes O(1).
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | push/pop/top: O(1), getMin: O(n) |
O(n) |
Uses one stack and scans for minimum |
| Optimal | O(1) for all operations |
O(n) |
Uses an additional stack to track minimums |
Algorithm Walkthrough
- Initialize two stacks.
The first stack stores all pushed values normally. The second stack stores the minimum value corresponding to each stack depth.
2. During push(val):
Push val onto the main stack.
Then determine the new minimum:
- If the min stack is empty,
valbecomes the minimum - Otherwise, compare
valwith the current minimum - Push the smaller value onto the min stack
This guarantees that the top of the min stack always represents the minimum value of the stack at that moment.
3. During pop():
Remove the top element from both stacks.
Since both stacks always have the same size, removing from both keeps them synchronized.
4. During top():
Return the last element from the main stack.
5. During getMin():
Return the last element from the min stack.
Since the min stack stores the minimum at every depth, this operation is constant time.
Why it works
The core invariant is:
At every stack depth
i,min_stack[i]stores the minimum value among all elements instack[0:i+1].
Because this invariant is maintained during every push and pop, the top of the min stack always represents the current minimum element of the entire stack.
Python Solution
class MinStack:
def __init__(self):
self.stack = []
self.min_stack = []
def push(self, val: int) -> None:
self.stack.append(val)
if not self.min_stack:
self.min_stack.append(val)
else:
current_min = self.min_stack[-1]
self.min_stack.append(min(val, current_min))
def pop(self) -> None:
self.stack.pop()
self.min_stack.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.min_stack[-1]
# Your MinStack object will be instantiated and called as such:
# obj = MinStack()
# obj.push(val)
# obj.pop()
# param_3 = obj.top()
# param_4 = obj.getMin()
The implementation uses two Python lists as stacks.
self.stack stores the actual elements pushed by the user.
self.min_stack stores the minimum value corresponding to every stack position. This means both stacks always have the same length.
Inside push(), we first append the value to the main stack. Then we compare the new value with the current minimum. The smaller value is appended to min_stack.
Inside pop(), both stacks remove their top elements together so they remain synchronized.
top() simply returns the last element of the main stack.
getMin() returns the last element of the min stack, which is always the minimum value currently present in the stack.
Go Solution
type MinStack struct {
stack []int
minStack []int
}
func Constructor() MinStack {
return MinStack{
stack: []int{},
minStack: []int{},
}
}
func (this *MinStack) Push(val int) {
this.stack = append(this.stack, val)
if len(this.minStack) == 0 {
this.minStack = append(this.minStack, val)
} else {
currentMin := this.minStack[len(this.minStack)-1]
if val < currentMin {
this.minStack = append(this.minStack, val)
} else {
this.minStack = append(this.minStack, currentMin)
}
}
}
func (this *MinStack) Pop() {
this.stack = this.stack[:len(this.stack)-1]
this.minStack = this.minStack[:len(this.minStack)-1]
}
func (this *MinStack) Top() int {
return this.stack[len(this.stack)-1]
}
func (this *MinStack) GetMin() int {
return this.minStack[len(this.minStack)-1]
}
/**
* Your MinStack object will be instantiated and called as such:
* obj := Constructor();
* obj.Push(val);
* obj.Pop();
* param_3 := obj.Top();
* param_4 := obj.GetMin();
*/
The Go solution follows the same logic as the Python version but uses slices instead of Python lists.
Popping from a stack in Go is done by slicing:
stack = stack[:len(stack)-1]
Unlike Python, Go does not provide built-in stack operations, so slices are commonly used to simulate stacks efficiently.
The constraints guarantee valid operations, so no additional empty-stack checks are required.
Worked Examples
Example 1
Input:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
Step-by-step Trace
| Operation | Main Stack | Min Stack | Returned Value |
|---|---|---|---|
| Initialize | [] |
[] |
null |
| push(-2) | [-2] |
[-2] |
null |
| push(0) | [-2, 0] |
[-2, -2] |
null |
| push(-3) | [-2, 0, -3] |
[-2, -2, -3] |
null |
| getMin() | [-2, 0, -3] |
[-2, -2, -3] |
-3 |
| pop() | [-2, 0] |
[-2, -2] |
null |
| top() | [-2, 0] |
[-2, -2] |
0 |
| getMin() | [-2, 0] |
[-2, -2] |
-2 |
Notice how the min stack stores the minimum value at every level:
- After pushing
0, the minimum remains-2 - After pushing
-3, the minimum becomes-3 - After popping
-3, the previous minimum-2is automatically restored
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(1) |
Every operation uses only direct stack access |
| Space | O(n) |
Two stacks store up to n elements |
Each operation performs only constant-time stack operations such as append, pop, and indexing.
The additional min stack requires extra memory proportional to the number of elements in the main stack. Therefore, the total space complexity is linear.
Test Cases
# Basic example from problem statement
stack = MinStack()
stack.push(-2)
stack.push(0)
stack.push(-3)
assert stack.getMin() == -3 # minimum after multiple pushes
stack.pop()
assert stack.top() == 0 # top after pop
assert stack.getMin() == -2 # minimum restored after pop
# Single element stack
stack = MinStack()
stack.push(5)
assert stack.top() == 5 # only element is top
assert stack.getMin() == 5 # only element is minimum
# Increasing values
stack = MinStack()
stack.push(1)
stack.push(2)
stack.push(3)
assert stack.getMin() == 1 # first element remains minimum
# Decreasing values
stack = MinStack()
stack.push(3)
stack.push(2)
stack.push(1)
assert stack.getMin() == 1 # newest element becomes minimum
# Duplicate minimum values
stack = MinStack()
stack.push(2)
stack.push(1)
stack.push(1)
stack.push(3)
assert stack.getMin() == 1 # duplicate minimum exists
stack.pop()
assert stack.getMin() == 1 # minimum still exists
stack.pop()
assert stack.getMin() == 1 # second duplicate still exists
stack.pop()
assert stack.getMin() == 2 # minimum restored
# Negative numbers
stack = MinStack()
stack.push(-1)
stack.push(-5)
stack.push(-3)
assert stack.getMin() == -5 # negative minimum
# Alternating push and pop
stack = MinStack()
stack.push(10)
stack.push(5)
assert stack.getMin() == 5 # minimum updated
stack.pop()
assert stack.getMin() == 10 # minimum restored
stack.push(2)
assert stack.getMin() == 2 # new minimum
# Large integer boundaries
stack = MinStack()
stack.push(-2**31)
stack.push(2**31 - 1)
assert stack.getMin() == -2**31 # minimum handles extreme values
| Test | Why |
|---|---|
| Problem example | Verifies standard behavior |
| Single element | Tests smallest valid stack |
| Increasing values | Ensures minimum remains unchanged |
| Decreasing values | Ensures minimum updates correctly |
| Duplicate minimums | Verifies proper handling of repeated minimums |
| Negative numbers | Confirms support for negative values |
| Alternating operations | Tests synchronization between stacks |
| Integer boundaries | Confirms extreme values are handled correctly |
Edge Cases
Duplicate Minimum Values
One subtle issue occurs when the minimum value appears multiple times.
For example:
push(2)
push(1)
push(1)
If we only tracked unique minimums, popping one 1 could incorrectly remove the minimum even though another 1 still exists.
The implementation avoids this problem by storing the minimum value at every stack depth. Each push records the current minimum again, even if it is unchanged.
Popping the Current Minimum
Another common source of bugs occurs when the top element is also the current minimum.
For example:
push(3)
push(2)
push(1)
pop()
After removing 1, the minimum should revert to 2.
Because the min stack stores historical minimum values at every depth, removing the top automatically restores the previous minimum correctly.
Negative Numbers and Integer Limits
The constraints allow values as small as -2^31 and as large as 2^31 - 1.
Some implementations accidentally assume only positive numbers or use incorrect comparisons.
This solution works correctly for all valid integer values because it only relies on standard integer comparisons and does not perform arithmetic operations that could overflow.