Maximum Frequency Stack
Description
Design a stack-like data structure that supports two operations: pushing an element onto the stack and popping 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 (the one pushed most recently) is removed and returned.
In other words, pop() should always remove the element that has appeared the greatest number of times among all elements currently in the stack. When two or more elements share the same highest frequency, the one that was added latest wins — mimicking stack-like recency behavior within each frequency tier.
Examples
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()— Create an empty frequency stack.push(5)— Stack: [5]push(7)— Stack: [5, 7]push(5)— Stack: [5, 7, 5]push(7)— Stack: [5, 7, 5, 7]push(4)— Stack: [5, 7, 5, 7, 4]push(5)— Stack: [5, 7, 5, 7, 4, 5]pop()→ returns 5. Element 5 has the highest frequency (3). After pop, stack conceptually becomes [5, 7, 5, 7, 4].pop()→ returns 7. Both 5 and 7 now have frequency 2, but 7 was pushed more recently (position 4 vs position 3 for the latest 5). After pop: [5, 7, 5, 4].pop()→ returns 5. Element 5 has frequency 2 while others have 1. After pop: [5, 7, 4].pop()→ returns 4. All elements have frequency 1, but 4 was pushed most recently. After pop: [5, 7].
Constraints
- 0 ≤ val ≤ 10^9
- At most 2 × 10^4 calls will be made to
pushandpop. - It is guaranteed that there will be at least one element in the stack before calling
pop.
Editorial
Brute Force — Sorting by Frequency and Recency
Intuition
The simplest way to solve this is to think about what pop() literally asks: find the element with the highest frequency, and among ties, pick the one that was pushed most recently.
We can maintain a list of all elements in the order they were pushed, along with a frequency counter. When pop() is called, we scan through all elements, determine which one has the maximum frequency, and among those with maximum frequency, pick the one that appears latest in the push order. Then we remove that element.
Think of it like a classroom poll. Every student writes their favorite color on a card and places it in a pile. To find the "winner," the teacher counts every card to find the most popular color. If two colors tie, the teacher picks whichever one was submitted last. This counting-every-time approach works but is slow when the pile grows large.
Step-by-Step Explanation
Let's trace through a smaller example:
Operations: push(5), push(7), push(5), push(7), push(4), push(5), pop(), pop()
Step 1: push(5) → elements = [(5, t=1)], freq = {5:1}
Step 2: push(7) → elements = [(5, t=1), (7, t=2)], freq = {5:1, 7:1}
Step 3: push(5) → elements = [(5, t=1), (7, t=2), (5, t=3)], freq = {5:2, 7:1}
Step 4: push(7) → elements = [(5, t=1), (7, t=2), (5, t=3), (7, t=4)], freq = {5:2, 7:2}
Step 5: push(4) → elements = [(5, t=1), (7, t=2), (5, t=3), (7, t=4), (4, t=5)], freq = {5:2, 7:2, 4:1}
Step 6: push(5) → elements = [(5, t=1), (7, t=2), (5, t=3), (7, t=4), (4, t=5), (5, t=6)], freq = {5:3, 7:2, 4:1}
Step 7: pop():
- Scan all elements: max frequency = 3, belonging to element 5
- Among elements with value 5, the latest was pushed at t=6 (index 5)
- Remove it → elements = [(5, t=1), (7, t=2), (5, t=3), (7, t=4), (4, t=5)]
- freq = {5:2, 7:2, 4:1}
- Return 5
Step 8: pop():
- Scan all elements: max frequency = 2, belonging to both 5 and 7
- Latest occurrence of an element with freq 2: 7 at t=4 (index 3)
- Why 7 and not 5? Because 7's latest push (t=4) is more recent than 5's latest push (t=3)
- Remove it → elements = [(5, t=1), (7, t=2), (5, t=3), (4, t=5)]
- freq = {5:2, 7:1, 4:1}
- Return 7
Brute Force Pop — Scanning for Max Frequency Element — Watch how we scan the entire element list to find the most frequent element with the latest push timestamp, then remove it.
Algorithm
Data Structures:
elements: a list of (value, timestamp) pairsfreq: a hash map tracking the frequency of each valuets: a timestamp counter incremented on each push
push(val):
- Increment
ts - Increment
freq[val] - Append
(val, ts)toelements
pop():
- Find the maximum frequency across all values in
freq - Scan
elementsfrom right to left for the last element whose value has that max frequency - Remove that element from
elements - Decrement
freq[val] - Return
val
Code
#include <vector>
#include <unordered_map>
using namespace std;
class FreqStack {
vector<pair<int,int>> elements; // (value, timestamp)
unordered_map<int,int> freq;
int ts = 0;
public:
FreqStack() {}
void push(int val) {
ts++;
freq[val]++;
elements.push_back({val, ts});
}
int pop() {
// Find max frequency
int maxFreq = 0;
for (auto& [key, cnt] : freq) {
if (cnt > 0) maxFreq = max(maxFreq, cnt);
}
// Find rightmost element with max frequency
int bestIdx = -1;
for (int i = elements.size() - 1; i >= 0; i--) {
if (freq[elements[i].first] == maxFreq) {
bestIdx = i;
break;
}
}
int val = elements[bestIdx].first;
elements.erase(elements.begin() + bestIdx);
freq[val]--;
return val;
}
};class FreqStack:
def __init__(self):
self.elements = [] # list of (value, timestamp)
self.freq = {} # value -> frequency
self.ts = 0
def push(self, val: int) -> None:
self.ts += 1
self.freq[val] = self.freq.get(val, 0) + 1
self.elements.append((val, self.ts))
def pop(self) -> int:
# Find maximum frequency
max_freq = max(cnt for cnt in self.freq.values() if cnt > 0)
# Find rightmost element with max frequency
for i in range(len(self.elements) - 1, -1, -1):
if self.freq[self.elements[i][0]] == max_freq:
val = self.elements[i][0]
self.elements.pop(i)
self.freq[val] -= 1
return valimport java.util.*;
class FreqStack {
private List<int[]> elements; // each entry: {value, timestamp}
private Map<Integer, Integer> freq;
private int ts;
public FreqStack() {
elements = new ArrayList<>();
freq = new HashMap<>();
ts = 0;
}
public void push(int val) {
ts++;
freq.put(val, freq.getOrDefault(val, 0) + 1);
elements.add(new int[]{val, ts});
}
public int pop() {
// Find max frequency
int maxFreq = 0;
for (int cnt : freq.values()) {
maxFreq = Math.max(maxFreq, cnt);
}
// Find rightmost element with max frequency
for (int i = elements.size() - 1; i >= 0; i--) {
int val = elements.get(i)[0];
if (freq.get(val) == maxFreq) {
elements.remove(i);
freq.put(val, freq.get(val) - 1);
return val;
}
}
return -1; // unreachable
}
}Complexity Analysis
Time Complexity:
push: O(1) — incrementing a counter and appending to a list are constant-time operations.pop: O(n) — we scan the frequency map to find the max frequency (O(number of unique elements)), then scan the elements list backwards to find the rightmost element with that frequency. In the worst case, this scans all n elements. Additionally, removing from the middle of a list is O(n) due to shifting.
Space Complexity: O(n)
We store every pushed element in the elements list and maintain a frequency map. Both grow linearly with the number of push operations.
Why This Approach Is Not Efficient
Every pop() call requires scanning the entire elements list to find the maximum-frequency element, making it O(n) per pop. With up to 2×10^4 operations, a sequence of n pushes followed by n pops leads to O(n²) total work — around 4×10^8 operations in the worst case, which is too slow.
The core inefficiency is that we recompute the maximum frequency and re-scan for the best element every single time. We have no quick way to jump directly to the most frequent, most recent element.
What if we could organize elements so that the most frequent one is always immediately accessible? A heap (priority queue) gives us exactly this — it keeps the "best" element at the top, and extracting it costs only O(log n) instead of O(n).
Better Approach — Max Heap with Frequency and Timestamp
Intuition
Instead of scanning all elements during pop, we can use a max heap (priority queue) to always keep the "best" element at the top.
What makes an element the "best" for popping? Two criteria, in order:
- Highest frequency — the element that appears most often
- Most recent push — among elements tied in frequency, the one pushed latest
We can combine both criteria into a single heap entry: (-frequency, -timestamp, value). Using negative values with a min heap simulates max heap behavior. The heap automatically keeps the entry with the highest frequency (and latest timestamp among ties) at the top.
On push(val), we increment the frequency counter, increment the timestamp, and push a new tuple onto the heap. On pop(), we simply extract the top of the heap and decrement that element's frequency.
The clever part: we never remove stale entries from the heap. Old entries with outdated frequencies remain, but since we always push new entries with the current (higher) frequency, those newer entries will naturally surface to the top first. This "lazy" approach avoids the expensive operation of searching through the heap to remove outdated entries.
Step-by-Step Explanation
Let's trace through: push(5), push(7), push(5), push(7), push(4), push(5), pop(), pop()
Step 1: push(5) — ts=1, freq={5:1}, heap push (-1, -1, 5).
Heap: [(-1,-1,5)]
Step 2: push(7) — ts=2, freq={5:1, 7:1}, heap push (-1, -2, 7).
Heap: [(-1,-2,7), (-1,-1,5)]
Step 3: push(5) — ts=3, freq={5:2, 7:1}, heap push (-2, -3, 5).
Heap: [(-2,-3,5), (-1,-1,5), (-1,-2,7)]
Note: 5's new entry with freq=2 rises to the top.
Step 4: push(7) — ts=4, freq={5:2, 7:2}, heap push (-2, -4, 7).
Heap top: (-2,-4,7) because -4 < -3 so 7 is more recent among freq-2.
Step 5: push(4) — ts=5, freq={5:2, 7:2, 4:1}, heap push (-1, -5, 4).
Heap top remains (-2,-4,7) since freq 2 > freq 1.
Step 6: push(5) — ts=6, freq={5:3, 7:2, 4:1}, heap push (-3, -6, 5).
Heap top: (-3,-6,5) — freq 3 is highest.
Step 7: pop() — Extract heap top (-3,-6,5) → val=5.
Decrement freq[5]=2. Return 5.
Old entries for 5 with freq 1 and 2 remain in heap but won't cause issues.
Step 8: pop() — Extract heap top.
Top is (-2,-4,7) → val=7. freq[7] was 2, now becomes 1. Return 7.
Why 7 before 5? Both had freq 2, but 7's timestamp (-4) beats 5's timestamp (-3) since |-4|>|-3|.
Max Heap Pop — Extracting Most Frequent, Most Recent Element — Watch how the max heap keeps the element with the highest frequency (and latest timestamp among ties) at the top for O(log n) pop operations.
Algorithm
Data Structures:
freq: hash map tracking current frequency of each valueheap: a max heap (simulated via min heap with negated values) storing tuples (-frequency, -timestamp, value)ts: a monotonically increasing timestamp counter
push(val):
- Increment
ts - Increment
freq[val] - Push
(-freq[val], -ts, val)onto the heap
pop():
- Extract the top element from the heap →
(-f, -t, val) - Decrement
freq[val] - Return
val
Code
#include <unordered_map>
#include <queue>
using namespace std;
class FreqStack {
unordered_map<int, int> freq;
// Max heap: {frequency, timestamp, value}
priority_queue<tuple<int,int,int>> heap;
int ts = 0;
public:
FreqStack() {}
void push(int val) {
ts++;
freq[val]++;
heap.push({freq[val], ts, val});
}
int pop() {
auto [f, t, val] = heap.top();
heap.pop();
freq[val]--;
return val;
}
};from heapq import heappush, heappop
from collections import defaultdict
class FreqStack:
def __init__(self):
self.freq = defaultdict(int)
self.heap = [] # min heap with negated values
self.ts = 0
def push(self, val: int) -> None:
self.ts += 1
self.freq[val] += 1
heappush(self.heap, (-self.freq[val], -self.ts, val))
def pop(self) -> int:
_, _, val = heappop(self.heap)
self.freq[val] -= 1
return valimport java.util.*;
class FreqStack {
private Map<Integer, Integer> freq;
// Max heap: compare by frequency desc, then timestamp desc
private PriorityQueue<int[]> heap;
private int ts;
public FreqStack() {
freq = new HashMap<>();
heap = new PriorityQueue<>((a, b) -> {
if (a[0] != b[0]) return b[0] - a[0]; // higher freq first
return b[1] - a[1]; // higher timestamp first
});
ts = 0;
}
public void push(int val) {
ts++;
freq.put(val, freq.getOrDefault(val, 0) + 1);
heap.offer(new int[]{freq.get(val), ts, val});
}
public int pop() {
int[] top = heap.poll();
int val = top[2];
freq.put(val, freq.get(val) - 1);
return val;
}
}Complexity Analysis
Time Complexity:
push: O(log n) — inserting into a heap with n entries takes O(log n) time. The frequency counter update is O(1).pop: O(log n) — extracting the top of a heap with n entries takes O(log n) time. The frequency decrement is O(1).
Space Complexity: O(n)
The heap stores one entry per push operation and never removes stale entries (they remain until naturally popped). With n total pushes, the heap can hold up to n entries. The frequency map holds at most m entries where m is the number of unique values (m ≤ n).
Why This Approach Is Not Efficient
While the heap approach improves pop from O(n) to O(log n), it has two drawbacks:
-
O(log n) per operation — both push and pop involve heap operations. For 2×10^4 operations, this is fast enough, but can we do better?
-
Heap pollution — the heap accumulates stale entries. After pushing the same value many times and popping it, old entries with outdated frequencies linger in the heap. This wastes memory and, in extreme cases with millions of operations, can degrade performance.
Is there a design that achieves O(1) for both push and pop? Yes — by rethinking how we organize elements. Instead of a global heap sorted by frequency, we can group elements by their frequency into separate stacks. The key insight is that when an element reaches frequency k, we push it onto the stack for frequency k. Popping always takes from the highest-frequency stack. Since we track max_freq and each stack is a simple list, both operations become O(1).
Optimal Approach — Frequency Group Stacks
Intuition
The breakthrough idea is to maintain a separate stack for each frequency level.
Imagine a set of shelves labeled 1, 2, 3, and so on. Each shelf holds elements that have reached that particular frequency. When you push value 5 for the first time, it goes onto shelf 1. When you push 5 again (second time), it also goes onto shelf 2. When you push 5 a third time, it goes onto shelf 3 as well.
To pop, you always look at the highest numbered shelf (max_freq) and take the top element from that shelf's stack. This automatically handles both requirements:
- Highest frequency — you always look at the highest shelf
- Most recent — within each shelf, the most recently added element is on top (stack behavior)
When you pop from the highest shelf and it becomes empty, you simply lower max_freq by 1. This works because if shelf k is empty, the next most frequent elements must be on shelf k-1.
Why does pushing at every frequency level work? Consider pushing 5 three times. After those pushes:
- Shelf 1 stack has: [..., 5] (5 appeared at least once)
- Shelf 2 stack has: [..., 5] (5 appeared at least twice)
- Shelf 3 stack has: [5] (5 appeared three times)
When we pop from shelf 3, we get 5 and decrease its frequency to 2. Shelf 2 still correctly contains 5 — we don't need to add it. This elegant property eliminates the need for any cleanup or re-insertion.

Step-by-Step Explanation
Let's trace: push(5), push(7), push(5), push(7), push(4), push(5), pop(), pop(), pop(), pop()
Step 1: push(5) — freq[5]=1, push 5 onto group[1]. max_freq=1.
group = {1: [5]}
Step 2: push(7) — freq[7]=1, push 7 onto group[1]. max_freq=1.
group = {1: [5, 7]}
Step 3: push(5) — freq[5]=2, push 5 onto group[2]. max_freq=2.
group = {1: [5, 7], 2: [5]}
Step 4: push(7) — freq[7]=2, push 7 onto group[2]. max_freq=2.
group = {1: [5, 7], 2: [5, 7]}
Step 5: push(4) — freq[4]=1, push 4 onto group[1]. max_freq stays 2.
group = {1: [5, 7, 4], 2: [5, 7]}
Step 6: push(5) — freq[5]=3, push 5 onto group[3]. max_freq=3.
group = {1: [5, 7, 4], 2: [5, 7], 3: [5]}
Step 7: pop() — Look at group[max_freq] = group[3] = [5]. Pop top → 5.
freq[5]=2. group[3] is now empty, so max_freq decreases to 2.
group = {1: [5, 7, 4], 2: [5, 7]}
Return 5.
Step 8: pop() — Look at group[2] = [5, 7]. Pop top → 7.
freq[7]=1. group[2] = [5], not empty, max_freq stays 2.
group = {1: [5, 7, 4], 2: [5]}
Return 7.
Step 9: pop() — Look at group[2] = [5]. Pop top → 5.
freq[5]=1. group[2] is now empty, max_freq decreases to 1.
group = {1: [5, 7, 4]}
Return 5.
Step 10: pop() — Look at group[1] = [5, 7, 4]. Pop top → 4.
freq[4]=0. group[1] = [5, 7], not empty, max_freq stays 1.
group = {1: [5, 7]}
Return 4.
Frequency Group Stacks — Push and Pop Operations — Watch how elements are pushed onto frequency-level stacks and popped from the highest-frequency stack, achieving O(1) operations.
Algorithm
Data Structures:
freq: hash map mapping each value to its current frequencygroup: hash map mapping each frequency level to a stack (list) of values at that frequencymax_freq: integer tracking the current maximum frequency
push(val):
- Increment
freq[val] - Let
f = freq[val] - Append
valtogroup[f] - Update
max_freq = max(max_freq, f)
pop():
- Pop the top element from
group[max_freq]→val - Decrement
freq[val] - If
group[max_freq]is now empty, decrementmax_freq - Return
val
Code
#include <unordered_map>
#include <vector>
using namespace std;
class FreqStack {
unordered_map<int, int> freq; // value -> frequency
unordered_map<int, vector<int>> group; // frequency -> stack of values
int maxFreq = 0;
public:
FreqStack() {}
void push(int val) {
freq[val]++;
int f = freq[val];
group[f].push_back(val);
maxFreq = max(maxFreq, f);
}
int pop() {
int val = group[maxFreq].back();
group[maxFreq].pop_back();
freq[val]--;
if (group[maxFreq].empty()) {
maxFreq--;
}
return val;
}
};from collections import defaultdict
class FreqStack:
def __init__(self):
self.freq = defaultdict(int) # value -> frequency
self.group = defaultdict(list) # frequency -> stack of values
self.max_freq = 0
def push(self, val: int) -> None:
self.freq[val] += 1
f = self.freq[val]
self.group[f].append(val)
self.max_freq = max(self.max_freq, f)
def pop(self) -> int:
val = self.group[self.max_freq].pop()
self.freq[val] -= 1
if not self.group[self.max_freq]:
self.max_freq -= 1
return valimport java.util.*;
class FreqStack {
private Map<Integer, Integer> freq; // value -> frequency
private Map<Integer, Deque<Integer>> group; // frequency -> stack
private int maxFreq;
public FreqStack() {
freq = new HashMap<>();
group = new HashMap<>();
maxFreq = 0;
}
public void push(int val) {
int f = freq.getOrDefault(val, 0) + 1;
freq.put(val, f);
group.computeIfAbsent(f, k -> new ArrayDeque<>()).push(val);
maxFreq = Math.max(maxFreq, f);
}
public int pop() {
int val = group.get(maxFreq).pop();
freq.put(val, freq.get(val) - 1);
if (group.get(maxFreq).isEmpty()) {
maxFreq--;
}
return val;
}
}Complexity Analysis
Time Complexity:
push: O(1) — incrementing a counter in a hash map, appending to a list, and taking the max of two integers are all constant-time operations.pop: O(1) — popping from the end of a list, decrementing a counter, checking if a list is empty, and decrementing an integer are all constant-time operations.
This is the best possible time complexity for this problem — every operation runs in constant time.
Space Complexity: O(n)
Each push adds exactly one entry to exactly one frequency group stack. With n total pushes, there are n entries across all group stacks. The frequency map stores at most n entries (one per unique value). Total space: O(n).
Notably, unlike the heap approach, there are no stale entries. Every entry in every group stack represents a valid, current element — no wasted memory.