155. Min Stack
Problem Description
You need to design a special stack data structure that supports all standard stack operations plus an additional operation to retrieve the minimum element - all in constant time.
The stack should support these operations:
MinStack()
: Creates a new empty stackpush(val)
: Adds a new elementval
to the top of the stackpop()
: Removes the top element from the stacktop()
: Returns the value of the top element without removing itgetMin()
: Returns the minimum element currently in the stack
The key challenge is that all operations, including getMin()
, must run in O(1)
time complexity. This means you cannot simply scan through all elements to find the minimum each time getMin()
is called.
The solution uses two stacks:
stk1
: A regular stack that stores all the elementsstk2
: An auxiliary stack that keeps track of the minimum element at each state
When pushing a new element:
- Add the element to
stk1
normally - Add to
stk2
the minimum between the new element and the current minimum (top ofstk2
)
When popping an element:
- Remove from both stacks to keep them synchronized
This way, the top of stk2
always contains the minimum element of all elements currently in the stack, allowing getMin()
to return it in O(1)
time.
The stk2
is initialized with inf
(infinity) to handle the edge case when comparing with the first element pushed to the stack.
Intuition
The natural approach to finding the minimum element would be to scan through all elements in the stack, but this takes O(n)
time. We need a way to "remember" the minimum element at all times without searching.
The key insight is that when we push elements onto a stack, the minimum element at any point depends on:
- The new element being pushed
- The minimum element among all elements below it in the stack
This observation leads us to think: what if we track the minimum element at each level of the stack?
Consider this example:
- Push 5: stack = [5], minimum = 5
- Push 3: stack = [5, 3], minimum = 3
- Push 7: stack = [5, 3, 7], minimum = 3
- Push 2: stack = [5, 3, 7, 2], minimum = 2
Notice that at each level, we need to know what the minimum was up to that point. If we pop 2, the minimum goes back to 3. If we pop 7, the minimum is still 3. If we pop 3, the minimum becomes 5.
This pattern suggests we need to store the "minimum so far" for each element in the stack. We could store pairs (value, min_at_this_point)
in a single stack, but using two parallel stacks is cleaner:
- One stack stores the actual values
- Another stack stores the corresponding minimum at each level
When we push a value, we calculate min(new_value, current_minimum)
and push this to the minimum stack. This ensures that the top of the minimum stack always holds the current minimum of all elements.
The initialization with inf
in stk2
is a clever trick to handle the empty stack case - when the first element is pushed, min(val, inf)
will always be val
, making it the minimum.
Solution Approach
The implementation uses two stacks to maintain both the elements and their corresponding minimum values:
Data Structures:
stk1
: Main stack to store all the actual elementsstk2
: Auxiliary stack to store the minimum element at each level
Initialization:
def __init__(self):
self.stk1 = []
self.stk2 = [inf]
stk1
starts empty as a regular stackstk2
is initialized withinf
(infinity) to handle the edge case when comparing with the first pushed element
Push Operation:
def push(self, val: int) -> None:
self.stk1.append(val)
self.stk2.append(min(val, self.stk2[-1]))
- Append the value to
stk1
normally - Calculate the new minimum by comparing
val
with the current minimum (stk2[-1]
) - Append this new minimum to
stk2
- This ensures
stk2[-1]
always contains the minimum of all elements in the stack
Pop Operation:
def pop(self) -> None:
self.stk1.pop()
self.stk2.pop()
- Remove the top element from both stacks
- This maintains the synchronization between the two stacks
- After popping,
stk2[-1]
automatically becomes the minimum of the remaining elements
Top Operation:
def top(self) -> int:
return self.stk1[-1]
- Simply return the last element of
stk1
without removing it
GetMin Operation:
def getMin(self) -> int:
return self.stk2[-1]
- Return the last element of
stk2
, which is always the current minimum
Time Complexity: All operations run in O(1)
time since they only involve appending, removing, or accessing the last element of a list.
Space Complexity: O(n)
where n is the number of elements in the stack, as we maintain two stacks of equal size.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a sequence of operations to see how the two-stack approach maintains the minimum element:
Initial State:
stk1 = []
stk2 = [inf]
Operation 1: push(4)
- Add 4 to
stk1
:stk1 = [4]
- Calculate min(4, inf) = 4, add to
stk2
:stk2 = [inf, 4]
- Current minimum: 4
Operation 2: push(7)
- Add 7 to
stk1
:stk1 = [4, 7]
- Calculate min(7, 4) = 4, add to
stk2
:stk2 = [inf, 4, 4]
- Current minimum: 4 (unchanged because 7 > 4)
Operation 3: push(2)
- Add 2 to
stk1
:stk1 = [4, 7, 2]
- Calculate min(2, 4) = 2, add to
stk2
:stk2 = [inf, 4, 4, 2]
- Current minimum: 2 (updated because 2 < 4)
Operation 4: getMin()
- Return
stk2[-1]
= 2 - No changes to stacks
Operation 5: pop()
- Remove from
stk1
:stk1 = [4, 7]
- Remove from
stk2
:stk2 = [inf, 4, 4]
- Current minimum automatically reverts to 4
Operation 6: push(1)
- Add 1 to
stk1
:stk1 = [4, 7, 1]
- Calculate min(1, 4) = 1, add to
stk2
:stk2 = [inf, 4, 4, 1]
- Current minimum: 1
Operation 7: top()
- Return
stk1[-1]
= 1 - No changes to stacks
Operation 8: getMin()
- Return
stk2[-1]
= 1
Notice how stk2
always maintains the minimum at each level, allowing us to retrieve the current minimum in O(1) time by simply accessing the top element.
Solution Implementation
1class MinStack:
2 def __init__(self) -> None:
3 """
4 Initialize the MinStack with two stacks:
5 - stack: stores all elements
6 - min_stack: stores the minimum element at each state
7 """
8 self.stack = []
9 self.min_stack = [float('inf')] # Initialize with infinity as sentinel value
10
11 def push(self, val: int) -> None:
12 """
13 Push element val onto the stack.
14 Also update the minimum stack with the new minimum.
15
16 Args:
17 val: Integer value to be pushed onto the stack
18 """
19 self.stack.append(val)
20 # Keep track of minimum by comparing with current minimum
21 self.min_stack.append(min(val, self.min_stack[-1]))
22
23 def pop(self) -> None:
24 """
25 Remove the element on top of the stack.
26 Also remove the corresponding minimum from min_stack.
27 """
28 self.stack.pop()
29 self.min_stack.pop()
30
31 def top(self) -> int:
32 """
33 Get the top element of the stack without removing it.
34
35 Returns:
36 The top element of the stack
37 """
38 return self.stack[-1]
39
40 def getMin(self) -> int:
41 """
42 Retrieve the minimum element in the stack in O(1) time.
43
44 Returns:
45 The minimum element currently in the stack
46 """
47 return self.min_stack[-1]
48
49
50# Your MinStack object will be instantiated and called as such:
51# obj = MinStack()
52# obj.push(val)
53# obj.pop()
54# param_3 = obj.top()
55# param_4 = obj.getMin()
56
1/**
2 * MinStack implementation that supports push, pop, top, and retrieving minimum element in O(1) time.
3 * Uses two stacks: one for storing all elements and another for tracking minimum values.
4 */
5class MinStack {
6 // Main stack to store all elements
7 private Deque<Integer> mainStack;
8 // Auxiliary stack to keep track of minimum elements
9 private Deque<Integer> minStack;
10
11 /**
12 * Constructor initializes both stacks.
13 * Pushes MAX_VALUE to minStack as initial value to handle edge cases.
14 */
15 public MinStack() {
16 mainStack = new ArrayDeque<>();
17 minStack = new ArrayDeque<>();
18 // Initialize minStack with MAX_VALUE to avoid null checks
19 minStack.push(Integer.MAX_VALUE);
20 }
21
22 /**
23 * Pushes an element onto the stack.
24 * Also updates the minimum value tracker.
25 * @param val the value to be pushed
26 */
27 public void push(int val) {
28 // Push value to main stack
29 mainStack.push(val);
30 // Push the minimum between current value and previous minimum to minStack
31 minStack.push(Math.min(val, minStack.peek()));
32 }
33
34 /**
35 * Removes the top element from the stack.
36 * Also removes the corresponding minimum tracker.
37 */
38 public void pop() {
39 mainStack.pop();
40 minStack.pop();
41 }
42
43 /**
44 * Gets the top element of the stack without removing it.
45 * @return the top element
46 */
47 public int top() {
48 return mainStack.peek();
49 }
50
51 /**
52 * Retrieves the minimum element in the stack in O(1) time.
53 * @return the minimum element in the current stack
54 */
55 public int getMin() {
56 return minStack.peek();
57 }
58}
59
60/**
61 * Your MinStack object will be instantiated and called as such:
62 * MinStack obj = new MinStack();
63 * obj.push(val);
64 * obj.pop();
65 * int param_3 = obj.top();
66 * int param_4 = obj.getMin();
67 */
68
1class MinStack {
2public:
3 /**
4 * Initialize the MinStack with two internal stacks:
5 * - mainStack: stores all elements
6 * - minStack: stores the minimum element at each level
7 */
8 MinStack() {
9 // Initialize minStack with INT_MAX as a sentinel value
10 // This handles the edge case when the stack is empty
11 minStack.push(INT_MAX);
12 }
13
14 /**
15 * Push element val onto the stack
16 * Time Complexity: O(1)
17 * @param val: the value to be pushed
18 */
19 void push(int val) {
20 // Push the value onto the main stack
21 mainStack.push(val);
22
23 // Push the minimum between current value and previous minimum
24 // This ensures minStack.top() always contains the current minimum
25 minStack.push(min(val, minStack.top()));
26 }
27
28 /**
29 * Remove the element on top of the stack
30 * Time Complexity: O(1)
31 */
32 void pop() {
33 // Remove top element from both stacks to maintain synchronization
34 mainStack.pop();
35 minStack.pop();
36 }
37
38 /**
39 * Get the top element of the stack
40 * Time Complexity: O(1)
41 * @return: the top element
42 */
43 int top() {
44 return mainStack.top();
45 }
46
47 /**
48 * Retrieve the minimum element in the stack
49 * Time Complexity: O(1)
50 * @return: the minimum element
51 */
52 int getMin() {
53 return minStack.top();
54 }
55
56private:
57 stack<int> mainStack; // Stack to store all elements
58 stack<int> minStack; // Stack to track minimum elements at each level
59};
60
61/**
62 * Your MinStack object will be instantiated and called as such:
63 * MinStack* obj = new MinStack();
64 * obj->push(val);
65 * obj->pop();
66 * int param_3 = obj->top();
67 * int param_4 = obj->getMin();
68 */
69
1// Main stack to store all elements
2let mainStack: number[] = [];
3// Auxiliary stack to keep track of minimum values
4let minStack: number[] = [];
5
6/**
7 * Initialize the data structures
8 */
9function initializeMinStack(): void {
10 mainStack = [];
11 minStack = [Infinity];
12}
13
14/**
15 * Push element onto stack
16 * @param val - The value to be pushed onto the stack
17 */
18function push(val: number): void {
19 // Push value onto main stack
20 mainStack.push(val);
21 // Push the minimum between current value and current minimum onto min stack
22 const currentMin = minStack[minStack.length - 1];
23 minStack.push(Math.min(val, currentMin));
24}
25
26/**
27 * Remove the element on top of the stack
28 */
29function pop(): void {
30 // Remove top element from both stacks
31 mainStack.pop();
32 minStack.pop();
33}
34
35/**
36 * Get the top element of the stack
37 * @returns The top element
38 */
39function top(): number {
40 // Return the last element of main stack
41 return mainStack[mainStack.length - 1];
42}
43
44/**
45 * Retrieve the minimum element in the stack
46 * @returns The minimum element in constant time
47 */
48function getMin(): number {
49 // Return the last element of min stack which stores current minimum
50 return minStack[minStack.length - 1];
51}
52
53// Initialize stacks on load
54initializeMinStack();
55
Time and Space Complexity
Time Complexity:
__init__()
:O(1)
- Initializing two empty lists (with one having a single element) takes constant time.push(val)
:O(1)
- Appending to both stacks and computing the minimum of two values are constant time operations.pop()
:O(1)
- Removing the last element from both stacks is a constant time operation.top()
:O(1)
- Accessing the last element of a list is a constant time operation.getMin()
:O(1)
- Accessing the last element of the second stack is a constant time operation.
Space Complexity:
O(n)
wheren
is the number of elements in the stack.- The implementation uses two stacks (
stk1
andstk2
), each storing up ton
elements. stk1
stores all the actual values pushed to the stack.stk2
stores the minimum value at each state of the stack, maintaining the same size asstk1
(plus the initialinf
value).- Total space used is
2n + 1
, which simplifies toO(n)
.
Common Pitfalls
1. Not Handling Empty Stack Edge Cases
A common mistake is forgetting to handle operations on an empty stack, which can cause runtime errors.
Pitfall Example:
def pop(self) -> None:
self.stack.pop() # Raises IndexError if stack is empty
self.min_stack.pop()
Solution: Add validation checks before performing operations:
def pop(self) -> None:
if self.stack:
self.stack.pop()
self.min_stack.pop()
def top(self) -> int:
if self.stack:
return self.stack[-1]
return None # or raise an exception
def getMin(self) -> int:
if len(self.min_stack) > 1: # Remember min_stack has sentinel value
return self.min_stack[-1]
return None # or raise an exception
2. Forgetting to Initialize min_stack with Sentinel Value
Pitfall Example:
def __init__(self):
self.stack = []
self.min_stack = [] # Empty initialization causes error on first push
This causes an IndexError when trying to access self.min_stack[-1]
during the first push operation.
Solution: Always initialize min_stack with infinity as a sentinel:
def __init__(self):
self.stack = []
self.min_stack = [float('inf')]
3. Using Only One Stack with Tuple Storage
While technically correct, storing tuples like (value, current_min)
in a single stack can be less intuitive and harder to debug:
Pitfall Example:
def push(self, val: int) -> None:
current_min = val if not self.stack else min(val, self.stack[-1][1])
self.stack.append((val, current_min))
Why it's problematic:
- Less readable and maintainable
- Accessing elements requires remembering tuple indices
- More prone to index errors
4. Recalculating Minimum on Every getMin() Call
Pitfall Example:
def getMin(self) -> int:
return min(self.stack) # O(n) time complexity!
This defeats the purpose of the problem as it requires O(n) time instead of O(1).
5. Not Synchronizing Both Stacks During Pop
Pitfall Example:
def pop(self) -> None:
popped_val = self.stack.pop()
# Forgetting to pop from min_stack or conditionally popping
if popped_val == self.min_stack[-1]:
self.min_stack.pop() # Wrong! Breaks synchronization
This breaks the correspondence between stack positions and their minimums. Always pop from both stacks to maintain synchronization.
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!