895. Maximum Frequency Stack - Explanation
Description
Design a stack-like data structure to push elements to the stack and pop the most frequent element from the stack.
Implement the FreqStack class:
FreqStack()constructs an empty frequency stack.void push(int val)pushes an integervalonto the top of the stack.int pop()removes and returns the most frequent element in the stack.- If there is a tie for the most frequent element, the element closest to the stack's top is removed and returned.
Example 1:
Input: ["FreqStack", "push", "push", "push", "push", "push", "push", "pop", "pop", "pop", "pop"]
[[], [5], [7], [5], [7], [4], [5], [], [], [], []]
Output: [null, null, null, null, null, null, null, 5, 7, 5, 4]Explanation:
FreqStack freqStack = new FreqStack();
freqStack.push(5); // The stack is [5]
freqStack.push(7); // The stack is [5,7]
freqStack.push(5); // The stack is [5,7,5]
freqStack.push(7); // The stack is [5,7,5,7]
freqStack.push(4); // The stack is [5,7,5,7,4]
freqStack.push(5); // The stack is [5,7,5,7,4,5]
freqStack.pop(); // return 5, as 5 is the most frequent. The stack becomes [5,7,5,7,4].
freqStack.pop(); // return 7, as 5 and 7 is the most frequent, but 7 is closest to the top. The stack becomes [5,7,5,4].
freqStack.pop(); // return 5, as 5 is the most frequent. The stack becomes [5,7,4].
freqStack.pop(); // return 4, as 4, 5 and 7 is the most frequent, but 4 is closest to the top. The stack becomes [5,7].
Constraints:
0 <= val <= 1,000,000,000- At most
20,000calls will be made topushandpop. - It is guaranteed that there will be at least one element in the stack before calling
pop.
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Stacks - The core data structure used to maintain elements by frequency level
- Hash Maps - Used for tracking element frequencies and grouping elements by frequency
- Heaps/Priority Queues - One solution approach uses heaps to efficiently retrieve the most frequent element
- Data Structure Design - Understanding how to combine multiple data structures to achieve efficient operations
1. Brute Force
Intuition
The most straightforward approach is to maintain a regular stack along with a frequency count for each element. When we need to pop, we find the maximum frequency among all elements, then scan backwards through the stack to find the most recent element with that frequency. While simple to understand, this requires scanning the entire stack on every pop operation.
Algorithm
- Initialize a hash map
cntto track the frequency of each element and a liststackto store pushed elements. - For
push(val): append the value to the stack and increment its count in the hash map. - For
pop(): find the maximum frequency across all elements, then iterate from the end of the stack to find the first element with that frequency. Remove it from the stack, decrement its count, and return it.
class FreqStack:
def __init__(self):
self.cnt = defaultdict(int)
self.stack = []
def push(self, val: int) -> None:
self.stack.append(val)
self.cnt[val] += 1
def pop(self) -> int:
maxCnt = max(self.cnt.values())
i = len(self.stack) - 1
while self.cnt[self.stack[i]] != maxCnt:
i -= 1
self.cnt[self.stack[i]] -= 1
return self.stack.pop(i)Time & Space Complexity
- Time complexity:
- time for each function call.
- time for each function call.
- Space complexity:
Where is the number of elements in the stack.
2. Heap
Intuition
We can use a max-heap to efficiently retrieve the element that should be popped next. Each heap entry stores three pieces of information: the element's frequency, its insertion order (index), and the value itself. By prioritizing higher frequencies and then more recent insertions, the heap always gives us the correct element to pop in logarithmic time.
Algorithm
- Initialize a max-heap, a frequency hash map
cnt, and anindexcounter starting at0. - For
push(val): increment the value's frequency, then push a tuple of(frequency, index, val)onto the heap. Increment the index counter. - For
pop(): pop the top element from the heap (which has the highest frequency and most recent index among ties), decrement that value's frequency, and return the value.
class FreqStack:
def __init__(self):
self.heap = []
self.cnt = defaultdict(int)
self.index = 0
def push(self, val: int) -> None:
self.cnt[val] += 1
heapq.heappush(self.heap, (-self.cnt[val], -self.index, val))
self.index += 1
def pop(self) -> int:
_, _, val = heapq.heappop(self.heap)
self.cnt[val] -= 1
return valTime & Space Complexity
- Time complexity:
- time for each function call.
- time for each function call.
- Space complexity:
Where is the number of elements in the stack.
3. Stack Of Stacks (Hash Map)
Intuition
The key insight is that we can group elements by their frequency level. When an element is pushed for the first time, it goes into stack 1. When pushed again, it also goes into stack 2 (while remaining in stack 1). This way, the stack at the highest frequency level always contains the elements we should consider popping first, and the top of that stack is the most recently pushed among them.
Algorithm
- Initialize a frequency hash map
cnt, a hash mapstackswhere each key is a frequency and each value is a stack, and a variablemaxCntto track the current maximum frequency. - For
push(val): increment the value's count. If this count exceedsmaxCnt, updatemaxCntand create a new stack for that frequency. Push the value onto the stack at its new frequency level. - For
pop(): pop from the stack atmaxCnt, decrement that value's frequency. If the stack atmaxCntbecomes empty, decrementmaxCnt. Return the popped value.
class FreqStack:
def __init__(self):
self.cnt = {}
self.maxCnt = 0
self.stacks = {}
def push(self, val: int) -> None:
valCnt = 1 + self.cnt.get(val, 0)
self.cnt[val] = valCnt
if valCnt > self.maxCnt:
self.maxCnt = valCnt
self.stacks[valCnt] = []
self.stacks[valCnt].append(val)
def pop(self) -> int:
res = self.stacks[self.maxCnt].pop()
self.cnt[res] -= 1
if not self.stacks[self.maxCnt]:
self.maxCnt -= 1
return resTime & Space Complexity
- Time complexity:
- time for each function call.
- time for each function call.
- Space complexity:
Where is the number of elements in the stack.
4. Stack Of Stacks (Dynamic Array)
Intuition
Instead of using a hash map for frequency-indexed stacks, we can use a dynamic array (list). The index in the array represents the frequency level. When an element reaches a new frequency, we extend the array if needed. The last non-empty stack in the array always corresponds to the maximum frequency, eliminating the need to track maxCnt separately.
Algorithm
- Initialize a frequency hash map
cntand a liststackswith an empty placeholder at index0(since frequencies start at1). - For
push(val): increment the value's count. If this count equals the current length ofstacks, append a new empty stack. Push the value onto the stack at index equal to its frequency. - For
pop(): pop from the last stack in the list, decrement that value's frequency. If the last stack becomes empty, remove it from the list. Return the popped value.
class FreqStack:
def __init__(self):
self.cnt = {}
self.stacks = [[]]
def push(self, val: int) -> None:
valCnt = 1 + self.cnt.get(val, 0)
self.cnt[val] = valCnt
if valCnt == len(self.stacks):
self.stacks.append([])
self.stacks[valCnt].append(val)
def pop(self) -> int:
res = self.stacks[-1].pop()
self.cnt[res] -= 1
if not self.stacks[-1]:
self.stacks.pop()
return resTime & Space Complexity
- Time complexity:
- time for each function call.
- time for each function call.
- Space complexity:
Where is the number of elements in the stack.
Common Pitfalls
Breaking Ties Incorrectly
When multiple elements share the highest frequency, the most recently pushed one must be popped first. A common mistake is to pop an arbitrary element with the maximum frequency, or to pop the first occurrence instead of the last. The heap solution handles this by storing insertion order as a secondary sort key; the stack-of-stacks solution naturally maintains recency by using stack ordering within each frequency level.
Not Decrementing Frequency After Pop
After popping an element, its frequency count must be decremented. Forgetting this step causes the data structure to believe the element still occurs at its previous frequency, leading to incorrect behavior on subsequent operations. Both the frequency map and any frequency-indexed structures must be updated consistently.
Mismanaging the Maximum Frequency Tracker
The stack-of-stacks approach tracks maxCnt to know which stack to pop from. A subtle bug occurs when the stack at maxCnt becomes empty after a pop but maxCnt is not decremented. The next pop would then try to access an empty stack. Always check if the current frequency stack is empty after popping and reduce maxCnt accordingly.
Using a Single Stack and Scanning
While conceptually simple, maintaining a single stack and scanning backwards to find the most frequent recent element leads to O(n) pop operations. This approach times out on large inputs. The key insight is that elements can exist at multiple frequency levels simultaneously (when pushed multiple times), which the stack-of-stacks approach leverages for O(1) operations.
Heap Stale Entries
In the heap-based solution, old entries remain in the heap even after frequencies change through pops. While this doesn't affect correctness (since we decrement the frequency counter separately), it can cause memory bloat. Some implementations add lazy deletion, but the simpler approach is to accept that the heap may contain stale entries whose values no longer reflect current frequencies, relying on the frequency map as the source of truth.
Sign in to join the discussion