intermediate8 min read·Updated September 21, 2026

Min Stack with Duplicate Minima Explained: Tracing push(3, 5, 2, 2)

Master O(1) minimum queries with duplicate elements. Follow a complete step-by-step walkthrough of push(3, 5, 2, 2) and pop operations with mental models.

By Learnisim AI·Published September 21, 2026
LEARNING ARCSetup → Model in the example → Full trace → Twist → Edge cases → Apply
PREREQUISITES
  • Basic stack operations (push, pop, peek)
  • Big-O time complexity notation
Min Stack with Duplicate Minima: push(3) → push(5) → push(2) → push(2) → pop → getMin() Primary Stack (Stores All Elements) Auxiliary Min Stack (O(1) GetMin) Idx 0 Value: 3 push(3) Idx 1 Value: 5 push(5) Idx 2 Value: 2 push(2) Idx 3 Value: 2 (Duplicate) push(2) [TOP] Min 1 Min: 3 From 3 Min 2 Min: 2 From 2 Min 3 Min: 2 (Duplicate) [TOP Min] 2 <= 2 (True) pop() match Rule: Push to min stack if value <= current min. Pop min stack only when popped value equals top min!
push 3, 5, 2, 2, pop, getMin, pop, getMin overview diagram
Why

Why Standard Stacks Fail at $O(1)$ Minimum Queries

Phase 1: The Minimum Query Bottleneck

Imagine you are building a system that tracks financial transactions or UI states, and you need a standard stack supporting push, pop, top, and a special getMin() operation in time. Given our working example—a sequence of operations starting with push(3), push(5), push(2), push(2), followed by pop(), getMin(), pop(), and getMin()—you might initially think of keeping a single minVal variable alongside your main stack.
When you push(3), minVal becomes 3. When you push(5), minVal stays 3. But what happens when you push(2) twice, and then pop() once? If your single minVal variable was updated to 2 when the first 2 arrived, what should minVal become when you pop() the first 2? A naive tracker looks at the remaining stack and sees 3, 5, and 2, but it has lost the history of which 2 was removed and whether another 2 is still holding the minimum.
Scanning the entire stack on every getMin() call takes time, destroying the core performance guarantee of a stack data structure. To achieve minimum retrieval with duplicate values like our sequence, we need a smarter structure that remembers historical minimums without losing track of multiplicity.
Why Standard Stacks Fail at O(1) Minimum Queries (with Duplicate Minima) Example Sequence: push(3) → push(5) → push(2) → push(2) → pop() → getMin() Operations Stream: push(3) push(5) push(2) push(2) pop() getMin() Expect: 2 (duplicate safe!) Main Stack (Data) Stores actual elements 2 top 2 5 3 Naive Min Variable Single minVal tracker minVal variable 2 The Duplicate Trap 1. push(2) sets minVal = 2. 2. push(2) keeps minVal = 2. 3. pop() removes top 2. What should minVal become? Lost history! Another '2' is still in stack. Min Stack Solution Auxiliary stack tracks min history 2 (min) push(2) 2 (min) push(2) 3 (min) push(5) 3 (min) push(3) ✓ pop() pops min stack too! getMin() is O(1)
Why Standard Stacks Fail at $O(1)$ Minimum Queries diagram
Model

Building the Parallel Min-Stack Model

Phase 2: The Parallel Tracker Model

To achieve an time complexity for getMin(), we cannot afford to search through elements or recalculate running minimums during a pop operation. Instead, we can maintain a second data structure alongside our primary stack—often called the min stack or auxiliary stack. When you execute our locked example of operations starting with push(3), push(5), push(2), and push(2), the primary stack records every item in exact insertion order: . The auxiliary stack, however, acts as a selective memory for minimum values.
Whenever a new value arrives, we compare it against the current minimum at the top of the auxiliary stack. If the incoming value is smaller than or equal to that minimum, we push it onto the auxiliary stack as well. This design ensures that the top of the auxiliary stack always holds the absolute minimum of all elements currently alive in the primary stack. When a pop() occurs on the primary stack, we check if the removed value matches the top of the auxiliary stack; if it does, we pop the auxiliary stack too. This symmetric movement keeps both structures perfectly synchronized.
Parallel Min-Stack Model: push(3), push(5), push(2), push(2) Auxiliary stack tracks minimums in O(1) time, handling duplicate minima symmetrically Primary Stack (Data) 0 3 push(3) 1 5 push(5) 2 2 push(2) 3 2 (Top) push(2) TOP Min Stack (Auxiliary) 0 3 min: 3 1 2 min: 2 2 2 (Top) min: 2 5 (Skipped: 5 > 3) Core Mechanics Push Rule If val <= minStack.top(): Push to Min Stack! (Handles duplicate minima) Pop Rule If popped == minStack.top(): Pop Min Stack too! getMin() O(1) Always return top of Auxiliary Min Stack. Current Min = 2 Sync Push
Building the Parallel Min-Stack Model diagram
Worked example

Tracing Operations on the Parallel Min Stack

Phase 3: Working Through the Duplicate Sequence

To see our parallel min-stack model in action, let us trace the exact operation sequence from our locked example: push(3), push(5), push(2), push(2), pop(), getMin(), pop(), getMin().
Recall the golden rule of duplicate handling: the auxiliary min stack must record duplicate minimum values using a non-strict inequality () rather than a strict one (). Otherwise, popping the first instance of a minimum value would prematurely strip it from the min stack.

Given

- Primary stack:
- Auxiliary min stack:
- Operations: push(3), push(5), push(2), push(2), pop(), getMin(), pop(), getMin()

Steps

1. push(3): Primary stack becomes . Min stack is empty, so we push 3. Min stack: . Current minimum is 3.
2. push(5): Primary stack becomes . Compare (False), so min stack remains .
3. push(2): Primary stack becomes . Compare (True), so push 2 to min stack. Min stack: . Current minimum is 2.
4. push(2): Primary stack becomes . Compare (True — this is the duplicate trap!), so push 2 to min stack. Min stack: . Current minimum is still 2.
5. pop(): Remove top from primary stack (removes the second 2). Primary stack is now . Since the popped value equals the top of the min stack (), pop the min stack as well. Min stack becomes .
6. getMin(): Inspect top of min stack, which is 2.
7. pop(): Remove next from primary stack (removes the first 2). Primary stack is now . Since the popped value equals the top of the min stack (), pop the min stack again. Min stack becomes .
8. getMin(): Inspect top of min stack, which is now 3.

Result

The trace successfully returns 2 after the first pop and 3 after the second pop, proving that duplicate minima are preserved correctly without overhead.
Try this: Trace the stack state manually if the sequence started with push(2), push(2), push(1), pop(). What would the auxiliary min stack look like after the pop?
Tracing Phase 3: The Duplicate Trap with (2 ≤ 2) After push(3), push(5), push(2), push(2) — showing duplicate handling in Aux Min Stack Primary Stack (Main) 3 [0] 5 [1] 2 [2] 2 (duplicate) [3] Auxiliary Min Stack (≤) 3 2 2 (pushed on 2 ≤ 2) Rule: 2 ≤ 2 True! Push duplicate Current getMin() = 2 (top of aux stack). Duplicate 2 ensures correct pop recovery!
Tracing Operations on the Parallel Min Stack diagram
Practice

Predicting State After Removing Duplicate Minima

Phase 4: Practice

Now that you have traced the sequence push(3), push(5), push(2), push(2), pop(), getMin(), pop(), getMin(), let's test whether your mental model holds up against a slight variation in the duplicate handling rule.
Recall that the auxiliary stack must store every instance of a duplicate minimum (using instead of during pushes) so that popping one duplicate does not prematurely strip away the running minimum for the remaining elements.

The Task

Imagine you execute the exact same initial setup: push(3), push(5), push(2), push(2). But suppose your auxiliary stack implementation mistakenly used strict inequality ( instead of ) when pushing new minimums.
Trace what the primary stack, auxiliary stack, and the result of getMin() would look like immediately after the first pop() operation under that flawed strict-inequality rule.
Apply

Transferring the Min Stack Pattern to Sliding Windows and Related Structures

Phase 5: Applying the Min-Stack Pattern

Now that you have traced every state of our working example—handling duplicate values like the second 2 without losing track of the first—you can lift this exact invariant design and drop it into new architectural challenges. The core pattern we built was maintaining a shadow auxiliary state that only updates when its inclusion criteria (such as ) are met. This decouples the query speed of expensive aggregates from the linear growth of the underlying dataset.
Consider how you would adapt this pattern to a Max Stack, which must support getMax() in time alongside standard stack operations. The auxiliary stack logic flips immediately: you push to the max-stack whenever a new element is greater than or equal to the current maximum, and you pop from it only when the primary stack's popped element equals the current maximum.
Another direct application is avoiding redundant computation in recursive backtracking or tree traversals where you need to pass down state summaries. By embedding the shadow tracking logic directly into your wrapper class, callers interact with a simple interface while the underlying class manages historical invariant records safely.

Transferring to Related Problems

When faced with a sliding window minimum problem (finding the minimum in every window of size ), you can no longer rely on a simple stack because elements leave from the bottom of the window rather than the top. However, the intuition of pairing elements with historical context—storing indices alongside values—evolves directly from the duplicate-min stack strategy you just mastered.
python
class MaxStack:
    def __init__(self):
        self.stack = []
        self.max_stack = []
    
    def push(self, val: int) -> None:
        # TODO: Implement push keeping max_stack synchronized
        pass
    
    def pop(self) -> int:
        # TODO: Implement pop maintaining duplicate maxima
        pass
    
    def getMax(self) -> int:
        # TODO: Return current maximum in O(1)
        pass

FAQ

What happens during getMin() in our working example after pushing two 2s and calling pop() once?
After pushing 3, 5, 2, and 2, the min stack contains two 2s at the top. When you call pop(), the first 2 is removed, but the remaining element is still 2. Therefore, getMin() correctly returns 2.
Why does a standard stack fail to provide O(1) minimum queries?
A standard stack only gives access to the top element. Finding the minimum among arbitrary elements requires scanning the entire stack, which takes O(N) time instead of O(1).
How does storing frequency counts compare to pushing duplicate minimums?
Instead of pushing identical minimum values onto a secondary auxiliary stack, some implementations store pairs of (value, frequency). Both achieve O(1) time complexity, but tracking duplicate entries explicitly simplifies the code logic.
What is the space complexity of using a parallel min-stack model?
The worst-case space complexity is O(N) because in a strictly decreasing sequence (e.g., pushing 5, 4, 3, 2, 1), the auxiliary min stack will grow to the same size as the main stack.

Keep learning