Max Stack
Hard · Stack · Explanation and code example
Heap + Lazy Update
How it works
We can use a standard stack for regular push/pop/top operations and a max heap to quickly find the maximum. The challenge is that removing an element from one structure does not automatically remove it from the other. We solve this with lazy deletion: when we remove an element, we record its index in a removed set. Before accessing the top of the stack or heap, we skip over any elements that have been marked as removed. This defers the actual cleanup until it is needed.
Steps
- Maintain a
stackfor LIFO access, a maxheapfor maximum access, and aremovedset for tracking deleted indices. - push(x): Add
(x, cnt)to bothstackandheap. Incrementcnt. - pop(): Skip elements at the top of
stackthat are inremoved. Pop the top element, add its index toremoved, and return the value. - top(): Skip
removedelements at the top ofstack, then return the top value. - peekMax(): Skip
removedelements at the top ofheap, then return the top value. - popMax(): Skip
removedelements at the top ofheap. Pop the top, add its index toremoved, and return the value.
Complexity
-
Time Complexity:
push:O(log N). It costsO(log N)to add an element to theheapandO(1)to add it to thestack.- The amortized time complexity of operations caused by a single
pop/popMaxcall isO(log N). For apopcall, we first remove the last element in thestackand add its ID toremovedinO(1), resulting in the deletion of the top element in theheapin the future (whenpeekMaxorpopMaxis called), which has a time complexity ofO(log N). Similarly,popMaxneedsO(log N)immediately andO(1)for the operations later. Note that because we lazy-update the two data structures, future operations might never happen in some cases. However, even in the worst cases, the upper bound of the amortized time complexity is still onlyO(log N). top:O(1), excluding the time cost related topopMaxcalls we discussed above.peekMax:O(1), excluding the time cost related topopcalls we discussed above.
-
Space Complexity:
O(N), the maximum size of theheap,stack, andremoved.
Where
Nis the number of elements to add to the stack.
Things to watch for
Forgetting to Synchronize Both Data Structures
When using two balanced trees (or a stack + heap), removing an element from one structure without removing the corresponding entry from the other leads to stale data. Both pop and popMax must update both structures to maintain consistency.
Incorrect Handling of Duplicate Values
Multiple elements can have the same value but different insertion indices. Using only the value as a key causes collisions and incorrect behavior. Always pair values with unique identifiers (like insertion count) to distinguish between duplicates.
Not Implementing Lazy Deletion Properly
In the heap + lazy update approach, forgetting to skip removed elements before accessing the top of the stack or heap returns invalid data. The cleanup loop must run before every top, pop, peekMax, and popMax operation.
Using Wrong Comparator for Max Heap
When implementing a max heap, accidentally using a min heap comparator (or vice versa) returns the wrong maximum element. Ensure the comparator orders elements so the largest value and most recent index appear at the top.
Integer Overflow in Counter
If the counter used for unique indices is not managed properly and the stack experiences many push operations, the counter could overflow. While rare in practice, using a sufficiently large integer type prevents this edge case.
Python code example
import heapq
class MaxStack:
def __init__(self):
self.heap = []
self.cnt = 0
self.stack = []
self.removed = set()
def push(self, x: int) -> None:
heapq.heappush(self.heap, (-x, -self.cnt))
self.stack.append((x, self.cnt))
self.cnt += 1
def pop(self) -> int:
while self.stack and self.stack[-1][1] in self.removed:
self.stack.pop()
num, idx = self.stack.pop()
self.removed.add(idx)
return num
def top(self) -> int:
while self.stack and self.stack[-1][1] in self.removed:
self.stack.pop()
return self.stack[-1][0]
def peekMax(self) -> int:
while self.heap and -self.heap[0][1] in self.removed:
heapq.heappop(self.heap)
return -self.heap[0][0]
def popMax(self) -> int:
while self.heap and -self.heap[0][1] in self.removed:
heapq.heappop(self.heap)
num, idx = heapq.heappop(self.heap)
self.removed.add(-idx)
return -num
C++ code example
class MaxStack {
private:
stack<pair<int, int>> stk;
priority_queue<pair<int, int>> heap;
unordered_set<int> removed;
int cnt;
public:
MaxStack() { cnt = 0; }
void push(int x) {
stk.push({x, cnt});
heap.push({x, cnt});
cnt++;
}
int pop() {
while (removed.count(stk.top().second)) {
stk.pop();
}
const pair<int, int> p = stk.top();
stk.pop();
removed.insert(p.second);
return p.first;
}
int top() {
while (removed.count(stk.top().second)) {
stk.pop();
}
return stk.top().first;
}
int peekMax() {
while (removed.count(heap.top().second)) {
heap.pop();
}
return heap.top().first;
}
int popMax() {
while (removed.count(heap.top().second)) {
heap.pop();
}
const pair<int, int> p = heap.top();
heap.pop();
removed.insert(p.second);
return p.first;
}
};
Source and license
Adapted from NeetCode’s source · MIT license. Node types are supplied by LeetCode where needed.