Prerequisites
Before attempting this problem, you should be comfortable with:
- Stacks - Understanding LIFO operations (push, pop, top) and their O(1) time complexity
- Balanced Binary Search Trees / Sorted Sets - Using self-balancing trees (TreeSet, SortedList) for O(log n) insertion, deletion, and ordered access
- Heaps / Priority Queues - Implementing max heaps for efficient maximum element retrieval
- Lazy Deletion - Deferring actual removal by marking elements as deleted and cleaning up on access
1. Two Balanced Trees
Intuition
A regular stack gives us O(1) access to the top element, but finding or removing the maximum requires scanning. To support efficient peekMax and popMax, we maintain two balanced trees (or sorted sets). One tree orders elements by their insertion index (simulating stack order), while the other orders by value (for quick max access). Each element is stored as a pair of (index, value). When we pop or popMax, we remove from both structures to keep them synchronized.
Algorithm
- Maintain two balanced trees:
stackkeyed by insertion count, andvalueskeyed by value. - push(x): Insert
(cnt, x)intostackand(x, cnt)intovalues. Incrementcnt. - pop(): Remove the last element from
stack(highest index), then remove the corresponding entry fromvalues. Return the value. - top(): Return the value of the last element in
stack. - peekMax(): Return the value of the last element in
values(highest value). - popMax(): Remove the last element from
values, then remove the corresponding entry fromstack. Return the value.
from sortedcontainers import SortedList
class MaxStack:
def __init__(self):
self.stack = SortedList()
self.values = SortedList()
self.cnt = 0
def push(self, x: int) -> None:
self.stack.add((self.cnt, x))
self.values.add((x, self.cnt))
self.cnt += 1
def pop(self) -> int:
idx, val = self.stack.pop()
self.values.remove((val, idx))
return val
def top(self) -> int:
return self.stack[-1][1]
def peekMax(self) -> int:
return self.values[-1][0]
def popMax(self) -> int:
val, idx = self.values.pop()
self.stack.remove((idx, val))
return valTime & Space Complexity
Time Complexity: for each operation except for initialization. All operations other than initialization involve finding/inserting/removing elements in a balanced tree once or twice. In general, the upper bound of time complexity for each of them is . However, note that
topandpeekMaxoperations, which require only the last element in a balanced tree, can be done in withset::rbegin()in C++ and special handling on the last element ofSortedListin Python. However,lastforTreeSetin Java hasn't implemented similar optimization yet, so we have to get the last element in .Space complexity: the maximum size of the two balanced trees.
Where is the number of elements to add to the stack.
2. Heap + Lazy Update
Intuition
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.
Algorithm
- 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.
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 -numTime & Space Complexity
Time Complexity:
push: . It costs to add an element to theheapand to add it to thestack.- The amortized time complexity of operations caused by a single
pop/popMaxcall is . For apopcall, we first remove the last element in thestackand add its ID toremovedin , resulting in the deletion of the top element in theheapin the future (whenpeekMaxorpopMaxis called), which has a time complexity of . Similarly,popMaxneeds immediately and 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 only . top: , excluding the time cost related topopMaxcalls we discussed above.peekMax: , excluding the time cost related topopcalls we discussed above.
Space Complexity: , the maximum size of the
heap,stack, andremoved.
Where is the number of elements to add to the stack.
Common Pitfalls
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.