LeetCode Problem: #155 — Min Stack
Difficulty: Medium
Topics: Stack, Design, Data Structures
Pattern: Auxiliary Stack / Constant-Time State Tracking
Series: LeetCode Interview Preparation — From Beginner to Expert
In the previous article, we learned how a Stack can solve problems involving nested structures.
Now we're going to take the same data structure and make it more powerful.
Imagine you're asked to design a stack that supports:
push()
pop()
top()
getMin()But there's one important requirement:
getMin()must return the minimum element in O(1) time.
At first, this sounds easy.
But there's a catch.
If we simply search the stack every time getMin() is called, we get:
O(n)per query.
The goal is:
push() → O(1)
pop() → O(1)
top() → O(1)
getMin() → O(1)This problem teaches an extremely valuable interview technique:
Maintain additional information while processing data so that future queries become cheap.
Design a stack that supports the following operations:
push(val)
pop()
top()
getMin()All operations must run in:
O(1)time.
push(val)Add val to the stack.
pop()Remove the element at the top.
top()Return the element currently at the top.
getMin()Return the smallest element currently present in the stack.
Let's perform:
push(-2)
push(0)
push(-3)The stack is:
-3 ← top
0
-2Minimum:
-3So:
getMin() → -3Now:
pop()removes -3.
Stack:
0 ← top
-2Now:
getMin() → -2Let's start with the simplest idea.
Use one normal stack:
stack = []For:
push(5)
push(2)
push(8)
push(1)we have:
[5, 2, 8, 1]To find the minimum, we could do:
min(stack)But:
min(stack)requires scanning the entire stack.
That's:
O(n)So although:
push → O(1)
pop → O(1)
top → O(1)we get:
getMin → O(n)That doesn't satisfy the problem.
Yes.
This is the key insight.
Suppose our stack is:
5
2
8
1The minimum is:
1But if we pop 1, the minimum becomes:
2So we need to know not only the current minimum, but the minimum at every level of the stack.
That's where an auxiliary stack comes in.
We'll maintain two stacks:
Stores the actual values.
stackStores the minimum value at each level.
minStackFor example:
push(5)Normal stack:
[5]Minimum stack:
[5]Then:
push(2)Normal stack:
[5, 2]Minimum stack:
[5, 2]Because the minimum is now 2.
Then:
push(8)Normal:
[5, 2, 8]Minimum:
[5, 2, 2]Notice something important.
We don't store 8 in the minimum stack.
Instead, we store:
current minimumat that point.
Then:
push(1)Normal:
[5, 2, 8, 1]Minimum:
[5, 2, 2, 1]Now getMin() is incredibly easy.
Simply return:
minStack[-1]That's:
O(1)The minimum stack remembers the answer to:
"What is the minimum of everything in the stack up to this point?"
For:
[5, 2, 8, 1]we maintain:
[5, 2, 2, 1]Each position corresponds to the same position in the main stack.
Therefore:
minStack[-1]always represents the minimum of the entire current stack.
class MinStack:
def __init__(self):
self.stack = []
self.minStack = []
def push(self, val):
self.stack.append(val)
if not self.minStack:
self.minStack.append(val)
else:
self.minStack.append(
min(val, self.minStack[-1])
)
def pop(self):
self.stack.pop()
self.minStack.pop()
def top(self):
return self.stack[-1]
def getMin(self):
return self.minStack[-1]This is the cleanest approach to understand first.
push()The important line is:
self.minStack.append(
min(val, self.minStack[-1])
)Suppose:
current minimum = 3
new value = 5Then:
min(5, 3) = 3So we store:
3But if:
current minimum = 3
new value = 1then:
min(1, 3) = 1So the new minimum becomes:
1pop()When we remove the top element:
self.stack.pop()we must also remove the corresponding minimum:
self.minStack.pop()This keeps both stacks synchronized.
top()Simple:
return self.stack[-1]The top of a Python list is:
stack[-1]getMin()This is the entire reason we created minStack.
return self.minStack[-1]No scanning.
No sorting.
No iteration.
Just one lookup.
Therefore:
O(1)Let's execute:
push(5)
push(2)
push(8)
push(1)
getMin()
pop()
getMin()
top()push(5)Stack:
[5]Min Stack:
[5]push(2)Stack:
[5, 2]Min Stack:
[5, 2]push(8)Stack:
[5, 2, 8]Min Stack:
[5, 2, 2]push(1)Stack:
[5, 2, 8, 1]Min Stack:
[5, 2, 2, 1]getMin()Return:
1pop()Both stacks become:
Stack:
[5, 2, 8]
Min Stack:
[5, 2, 2]getMin()Return:
2top()Return:
8Everything works in constant time.
You might propose this:
minStack = [5, 2, 1]instead of:
minStack = [5, 2, 2, 1]That can also work.
The idea would be to store a value only when it becomes a new minimum.
For example:
push(5) → minStack [5]
push(2) → minStack [5, 2]
push(8) → minStack [5, 2]
push(1) → minStack [5, 2, 1]But then pop() needs to know whether the removed value equals the current minimum.
That approach is valid, but the synchronized two-stack approach is often easier to reason about and implement correctly.
There's another elegant approach.
Instead of maintaining two separate stacks, we can store pairs:
(value, minimum_so_far)For example:
(5, 5)
(2, 2)
(8, 2)
(1, 1)Then the minimum is always the second value of the top pair.
class MinStack:
def __init__(self):
self.stack = []
def push(self, val):
if not self.stack:
current_min = val
else:
current_min = min(
val,
self.stack[-1][1]
)
self.stack.append((val, current_min))
def pop(self):
self.stack.pop()
def top(self):
return self.stack[-1][0]
def getMin(self):
return self.stack[-1][1]This is conceptually the same technique.
We're simply storing:
value
+
minimum so fartogether.
This problem teaches a pattern that appears far beyond stacks.
Suppose you're repeatedly asked:
"What is the minimum so far?"
Instead of recalculating it every time, maintain:
minimum_so_farThis is a general algorithmic technique:
Precompute or maintain useful state so future queries are cheap.
We've already seen a similar idea in our stock problem:
minimum price seen so farHere we're doing something similar:
minimum value at each stack levelThis idea also connects to prefix data structures.
For example:
nums = [5, 2, 8, 1]Prefix minimums are:
[5, 2, 2, 1]That's exactly what our minStack stores.
So Min Stack can be thought of as:
Prefix minimum tracking + stack operations.
Recognizing these connections makes new problems easier.
Consider:
push(2)
push(2)
push(5)Our stacks become:
Stack:
[2, 2, 5]
Min Stack:
[2, 2, 2]Now:
pop()Min Stack:
[2, 2]Minimum is still:
2Another pop:
[2]Minimum remains:
2This is why storing the minimum at every level is so convenient.
Nothing special is required.
For:
push(-2)
push(-10)
push(-3)Min Stack:
[-2, -10, -10]Therefore:
getMin() → -10min() During getMin()This:
def getMin(self):
return min(self.stack)is easy to write.
But it takes:
O(n)and violates the requirement.
The whole point is to maintain the minimum proactively.
minStack During pop()If you do:
self.stack.pop()but forget:
self.minStack.pop()the two structures become inconsistent.
Every stack operation must maintain the relationship between the two stacks.
Suppose:
[5, 2, 8, 1]and you only store:
minimum = 1After popping 1, you would need to rediscover that the previous minimum was 2.
That's why we preserve historical minimum information.
top() and getMin()They are different questions.
top()asks:
What was added most recently?
While:
getMin()asks:
What is the smallest value?
The two-stack design maintains both independently.
What if the stack needs:
getMin()
getMax()in O(1)?
You could maintain:
stack
minStack
maxStackNow all three operations remain O(1).
What if the interviewer asks for:
getAverage()You could maintain a running sum.
Then:
average = sum / countAgain, the same principle:
Maintain state while updates occur.
Now things become more interesting.
A single minimum isn't enough.
You need additional information about the second-smallest value and how it changes during pop().
This is where interviewers start testing whether you understand the underlying state-maintenance technique rather than just memorizing the standard Min Stack solution.
Suppose memory is extremely limited.
Can you still support:
getMin()in O(1)?
This becomes a much more advanced design question.
There are mathematical encoding techniques that can reduce auxiliary storage, but they make the implementation more complex and can introduce integer-overflow considerations in some languages.
For most interviews, the auxiliary-stack solution is the clean trade-off.
getMin() Is PossibleThis is worth understanding deeply.
A normal stack gives us:
top()in O(1).
But it doesn't naturally give:
minimum()in O(1).
So we augment the data structure.
Instead of storing only:
valuewe store:
value
minimum_so_farNow every stack entry carries enough information to answer the minimum query immediately.
This is called augmenting a data structure.
That concept is extremely useful in advanced software engineering interviews.
When an interviewer says:
"Support an operation in O(1), but the normal data structure would require O(n)."
Ask:
"Can I maintain extra information during updates?"
Examples:
Stack
+
MinimumArray
+
Prefix informationQueue
+
Maximum informationData structure
+
Cached stateThe general pattern is:
Expensive query
↓
Maintain state during updates
↓
Cheap queryImagine you're running a competition.
Instead of checking every participant whenever someone asks:
"What's the lowest score so far?"
you maintain:
lowest_scoreWhenever a new score arrives:
lowest_score = min(
lowest_score,
new_score
)Now answering the query takes:
O(1)Min Stack is the same idea, except we also need to correctly restore the previous state when elements are removed.
| Operation | Time | Extra Space |
|---|---|---|
push() | O(1) | O(1) per element |
pop() | O(1) | O(1) |
top() | O(1) | O(1) |
getMin() | O(1) | O(1) |
Overall auxiliary space:
O(n)because we store minimum information for the elements in the stack.
Problem:
Design a stack with getMin().
Required:
push() → O(1)
pop() → O(1)
top() → O(1)
getMin() → O(1)
Idea:
Use an auxiliary minimum stack.
Main Stack:
stores values.
Min Stack:
stores minimum so far.
push(x):
stack.push(x)
minStack.push(
min(x, minStack.top())
)
pop():
stack.pop()
minStack.pop()
top():
return stack.top()
getMin():
return minStack.top()
Time:
O(1) per operation
Space:
O(n)Try these before moving to the next problem.
Implement Min Stack using two stacks without using Python's built-in min().
Implement:
getMax()in O(1).
Implement a stack supporting both:
getMin()
getMax()in O(1).
Design a queue supporting:
enqueue()
dequeue()
getMin()in O(1).
Modify Min Stack so it can also return the frequency of the minimum value.
Try solving Min Stack using a single stack of tuples.
Our interview preparation journey is now building a stronger foundation.
| Problem | Core Pattern |
|---|---|
| #1 Two Sum | Hash Map |
| #121 Best Time to Buy/Sell Stock | Running Minimum |
| #217 Contains Duplicate | Hash Set |
| #53 Maximum Subarray | Kadane's Algorithm |
| #167 Two Sum II | Two Pointers |
| #15 3Sum | Sorting + Two Pointers |
| #20 Valid Parentheses | Stack |
| #155 Min Stack | Stack + Auxiliary State |
The important progression is:
Stack
↓
Valid Parentheses
↓
Understand LIFO
↓
Min Stack
↓
Augment the Stack
↓
Maintain Extra StateWe're moving from simply using data structures to designing data structures.
That's an important jump in interview preparation.
LeetCode #155 teaches much more than how to implement a minimum stack.
The real lesson is:
Don't repeatedly calculate information that you can maintain incrementally.
A normal stack knows:
What is on top?An augmented stack can also know:
What is the minimum?without scanning the entire structure.
The transformation is:
Normal Stack
↓
Add auxiliary state
↓
Track minimum after every update
↓
getMin() becomes O(1)This is a pattern you'll encounter repeatedly in algorithm and system design interviews.
When you see a problem that says:
"Support operation X in constant time."
don't immediately assume you need a magical algorithm.
First ask:
"What information can I maintain now so I don't have to calculate it later?"
That question can lead you to the solution.
Pixels to Perfection Design that Impresses