Maximum Frequency Stack
HardPrompt
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 <= 109- At most
2 * 104calls will be made topushandpop. - It is guaranteed that there will be at least one element in the stack before calling
pop.
Approaches
2 approaches with complexity analysis and trade-offs.
This is a straightforward, brute-force approach that directly simulates the requirements using basic data structures. We use a list to act as our stack. The push operation is simple, just adding an element to the list. The main work is done in the pop operation, which requires recalculating all element frequencies from scratch, finding the maximum frequency, and then scanning the list again to find the correct element to remove. This leads to a linear time complexity for pop.
Algorithm
- Data Structure: Use a single
List(likeArrayListin Java) to store the elements in the order they are pushed. push(val)operation:- Simply append the new element
valto the end of the list.
- Simply append the new element
pop()operation:- Create a temporary
HashMapto calculate the frequency of every element currently in the list. - Iterate through the list to populate the frequency map and find the maximum frequency (
maxFreq). - Iterate through the list backwards, from the last element to the first.
- For each element, check its frequency in the map. The first element encountered that has a frequency equal to
maxFreqis the one to be popped. - Remove this element from the list by its index.
- Return the removed element.
- Create a temporary
Walkthrough
In this approach, we maintain the stack's elements in an ArrayList. When push(val) is called, we add val to the end of this list, which is an O(1) operation on average.
The pop() method is more complex. Since we need to pop the most frequent element, and in case of a tie, the one closest to the top, we must first determine the frequencies of all elements. We can do this by iterating through the entire list and using a HashMap to store counts. While doing so, we can also keep track of the maximum frequency seen so far. After this initial O(N) scan, we know the maxFreq. Now, to respect the tie-breaking rule (closest to the top), we must scan the list again, but this time from the end towards the beginning. The first element we find whose frequency matches maxFreq is our target. We then remove it from the list and return it. The removal from an ArrayList can also take O(N) time, making the overall pop operation quite slow.
class FreqStack { List<Integer> stack; public FreqStack() { stack = new ArrayList<>(); } public void push(int val) { stack.add(val); } public int pop() { if (stack.isEmpty()) { return -1; // Or throw an exception } Map<Integer, Integer> freqMap = new HashMap<>(); int maxFreq = 0; for (int val : stack) { int newFreq = freqMap.getOrDefault(val, 0) + 1; freqMap.put(val, newFreq); maxFreq = Math.max(maxFreq, newFreq); } int valToPop = -1; int indexToPop = -1; for (int i = stack.size() - 1; i >= 0; i--) { int currentVal = stack.get(i); if (freqMap.get(currentVal) == maxFreq) { valToPop = currentVal; indexToPop = i; break; } } stack.remove(indexToPop); return valToPop; }}Complexity
Time
- `push(val)`: O(1) amortized time. - `pop()`: O(N) time, where N is the number of elements in the stack. This is because we perform two passes over the stack in the worst case.
Space
O(N), where N is the number of elements in the stack. This space is used to store the elements themselves. The `pop` operation requires additional O(U) space for the frequency map, where U is the number of unique elements (U <= N).
Trade-offs
Pros
Simple to understand and implement.
Uses only basic and common data structures.
Cons
The
popoperation is very inefficient with a time complexity ofO(N), whereNis the number of elements in the stack.Removing an element from the middle of an
ArrayListis also anO(N)operation, further contributing to the poor performance.It is not suitable for scenarios with a large number of elements or frequent
popcalls.
This highly efficient approach achieves constant time complexity for both push and pop operations by using a clever combination of data structures. It avoids the costly re-computation of frequencies by maintaining them incrementally. The core idea is to group elements by their frequency. For each frequency level, we maintain a stack of elements that have that frequency. This allows us to instantly access the most frequent elements and, thanks to the stack property, find the one that was added most recently.
Algorithm
- Data Structures:
freqMap: AHashMap<Integer, Integer>to store the frequency of each element (element -> frequency).group: AHashMap<Integer, Stack<Integer>>to group elements by their frequency (frequency -> Stack of elements).maxFreq: An integer to keep track of the current maximum frequency.
push(val)operation:- Retrieve the current frequency of
valfromfreqMap, increment it, and update the map. - Update
maxFreqto be the maximum of its current value and the new frequency ofval. - Access the
groupmap with the new frequency as the key. Pushvalonto the stack found at that key. If no stack exists, create a new one first.
- Retrieve the current frequency of
pop()operation:- Get the stack of elements associated with
maxFreqfrom thegroupmap. - Pop the top element from this stack. This element is the most frequent and was the most recently pushed among all elements with that frequency.
- Decrement the frequency of the popped element in
freqMap. - If the stack for
maxFreqbecomes empty after the pop, it means there are no more elements with that frequency, so decrementmaxFreq. - Return the popped element.
- Get the stack of elements associated with
Walkthrough
To optimize the pop operation, we need to avoid the linear scans. We can achieve this by maintaining more state. We use two HashMaps and an integer variable:
freqMap: AHashMap<Integer, Integer>that maps each element to its current frequency. This allowsO(1)lookup of any element's frequency.group: AHashMap<Integer, Stack<Integer>>. This is the key data structure. It maps a frequency to aStackof elements that have that frequency. For example,group.get(3)would give us a stack of all numbers that currently appear 3 times. The stack ensures that elements that reached this frequency later are on top.maxFreq: An integer that tracks the current maximum frequency among all elements. This avoids the need to search for it.
How it works:
push(val): We find the new frequency ofvaland updatefreqMap. We updatemaxFreqif necessary. Then, we pushvalonto the stack corresponding to its new frequency in thegroupmap. All these areO(1)operations.pop(): We know the element to pop must have a frequency ofmaxFreq. We usegroup.get(maxFreq)to get the stack of such elements. The top of this stack is our target because it was the last one to achieve this frequency. We pop it, update its frequency infreqMap, and if the stack formaxFreqbecomes empty, we decrementmaxFreq. This is also anO(1)operation.
class FreqStack { // Map to store frequency of each element Map<Integer, Integer> freqMap; // Map to group elements by their frequency. Each frequency maps to a stack. Map<Integer, Stack<Integer>> group; // Variable to track the maximum frequency encountered so far int maxFreq; public FreqStack() { freqMap = new HashMap<>(); group = new HashMap<>(); maxFreq = 0; } public void push(int val) { // Get current frequency and increment it int freq = freqMap.getOrDefault(val, 0) + 1; // Update the frequency map freqMap.put(val, freq); // Update the max frequency maxFreq = Math.max(maxFreq, freq); // Add the element to the stack corresponding to its new frequency group.computeIfAbsent(freq, k -> new Stack<>()).push(val); } public int pop() { // Get the stack of elements with the maximum frequency Stack<Integer> topStack = group.get(maxFreq); // Pop the most recently added element from this stack int val = topStack.pop(); // Decrement the frequency of the popped element freqMap.put(val, freqMap.get(val) - 1); // If the stack for the max frequency becomes empty and there are still elements, // it means the new max frequency is one less. if (topStack.isEmpty()) { maxFreq--; } return val; }}Complexity
Time
- `push(val)`: O(1) average time. - `pop()`: O(1) average time. All operations involve hash map and stack manipulations, which take constant time on average.
Space
O(N), where N is the number of elements pushed to the stack. The `freqMap` can store up to U unique elements (where U <= N), and the `group` map stores N total elements across all its stacks.
Trade-offs
Pros
Extremely efficient, with average time complexity of O(1) for both
pushandpopoperations.Scales well with a large number of operations.
Cons
Requires more complex data structures, which can be harder to reason about initially.
Uses more memory due to the overhead of multiple HashMaps and Stack objects compared to a simple list.
Solutions
Solution
class FreqStack { private Map < Integer , Integer > cnt = new HashMap <>(); private Map < Integer , Deque < Integer >> d = new HashMap <>(); private int mx ; public FreqStack () { } public void push ( int val ) { cnt . put ( val , cnt . getOrDefault ( val , 0 ) + 1 ); int t = cnt . get ( val ); d . computeIfAbsent ( t , k -> new ArrayDeque <>()). push ( val ); mx = Math . max ( mx , t ); } public int pop () { int val = d . get ( mx ). pop (); cnt . put ( val , cnt . get ( val ) - 1 ); if ( d . get ( mx ). isEmpty ()) { -- mx ; } return val ; } } /** * Your FreqStack object will be instantiated and called as such: * FreqStack obj = new FreqStack(); * obj.push(val); * int param_2 = obj.pop(); */class FreqStack { public: FreqStack () { } void push ( int val ) { ++ cnt [ val ]; d [ cnt [ val ]]. push ( val ); mx = max ( mx , cnt [ val ]); } int pop () { int val = d [ mx ]. top (); -- cnt [ val ]; d [ mx ]. pop (); if ( d [ mx ]. empty ()) -- mx ; return val ; } private: unordered_map < int , int > cnt ; unordered_map < int , stack < int >> d ; int mx = 0 ; }; /** * Your FreqStack object will be instantiated and called as such: * FreqStack* obj = new FreqStack(); * obj->push(val); * int param_2 = obj->pop(); */class FreqStack : def __init__ ( self ): self . cnt = defaultdict ( int ) self . d = defaultdict ( list ) self . mx = 0 def push ( self , val : int ) -> None : self . cnt [ val ] += 1 self . d [ self . cnt [ val ]]. append ( val ) self . mx = max ( self . mx , self . cnt [ val ]) def pop ( self ) -> int : val = self . d [ self . mx ]. pop () self . cnt [ val ] -= 1 if not self . d [ self . mx ]: self . mx -= 1 return val # Your FreqStack object will be instantiated and called as such: # obj = FreqStack() # obj.push(val) # param_2 = obj.pop()Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.