Facebook Pixel

155. Min Stack

MediumStackDesign
Leetcode Link

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 stack
  • push(val): Adds a new element val to the top of the stack
  • pop(): Removes the top element from the stack
  • top(): Returns the value of the top element without removing it
  • getMin(): 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:

  1. stk1: A regular stack that stores all the elements
  2. stk2: 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 of stk2)

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. The new element being pushed
  2. 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 elements
  • stk2: 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 stack
  • stk2 is initialized with inf (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 Evaluator

Example 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) where n is the number of elements in the stack.
  • The implementation uses two stacks (stk1 and stk2), each storing up to n 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 as stk1 (plus the initial inf value).
  • Total space used is 2n + 1, which simplifies to O(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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More