895. Maximum Frequency Stack
Problem Description
You need to design a special stack data structure called FreqStack
that supports two operations:
- Push: Add an element to the stack
- Pop: Remove and return the element that appears most frequently in the stack
The key challenge is that when you pop, you don't just remove any occurrence of the most frequent element - you need to follow specific rules:
- Find the element(s) with the highest frequency count
- If multiple elements share the same highest frequency, remove the one that was pushed most recently (closest to the top)
For example, if you push the sequence [5, 7, 5, 7, 4, 5]
:
- Element
5
appears 3 times (highest frequency) - Elements
7
appears 2 times - Element
4
appears 1 time
When you call pop()
, it should return 5
because it has the highest frequency. The specific 5
removed would be the last one pushed.
If after that pop you have [5, 7, 5, 7, 4]
and call pop()
again:
- Both
5
and7
now appear 2 times (tied for highest frequency) - Since
7
was pushed more recently than the remaining5
, it returns7
The implementation uses:
- A hash table
cnt
to track the frequency of each element - A max heap
q
that stores tuples of(-frequency, -timestamp, value)
to efficiently find the most frequent and most recent element - A timestamp counter
ts
to track the order of insertions
The negative values in the heap ensure that higher frequencies and more recent timestamps have priority when popping elements.
Intuition
When thinking about this problem, we need to handle two key requirements: tracking frequency and maintaining recency when frequencies are tied.
The first thought might be to use a simple frequency counter, but that alone won't help us efficiently find the element with maximum frequency. We'd have to scan through all elements every time we pop, which would be inefficient.
This naturally leads us to consider a heap (priority queue) since it can give us the maximum element in O(log n)
time. But what should we store in the heap?
We need to prioritize by frequency first, so that should be our primary sorting criterion. However, when frequencies are equal, we need the most recently pushed element. This means we need to track when each element was pushed - hence the need for timestamps.
The clever insight is to combine both frequency and timestamp into a single tuple for the heap. By using (-frequency, -timestamp, value)
, we create a max heap where:
- Higher frequencies come first (more negative means higher priority in a min heap)
- Among equal frequencies, larger timestamps come first (more recent pushes)
Why don't we remove elements from the heap when their frequency decreases? This would require searching through the heap, which is expensive. Instead, we use a "lazy deletion" approach - we keep all entries in the heap and only check if they're valid when we pop them. The frequency counter cnt
tells us the current actual frequency of each element.
When we pop, we get the top element from the heap. Since we decrement its frequency in cnt
, any outdated entries for this element in the heap (with higher frequencies) will naturally be ignored in future operations because we always use the most recent frequency when pushing new entries.
This approach elegantly handles both requirements with O(log n)
complexity for both push and pop operations.
Learn more about Stack patterns.
Solution Approach
The solution uses a hash table combined with a priority queue (max heap) to efficiently track and retrieve the most frequent elements.
Data Structures Used:
-
Hash Table
cnt
: Adefaultdict(int)
that maintains the current frequency count for each element in the stack. -
Priority Queue
q
: A max heap implemented using Python'sheapq
(which is a min heap by default, so we use negative values). Each entry is a tuple(-frequency, -timestamp, value)
. -
Timestamp Counter
ts
: An integer that increments with each push operation to track the order of insertions.
Implementation Details:
Initialization (__init__
):
- Initialize
cnt
as an empty frequency counter - Initialize
q
as an empty list (will be used as a heap) - Set
ts = 0
as the starting timestamp
Push Operation (push
):
- Increment the timestamp:
self.ts += 1
- Update the frequency count:
self.cnt[val] += 1
- Add a new entry to the heap:
heappush(self.q, (-self.cnt[val], -self.ts, val))
- The negative frequency ensures higher frequencies have priority
- The negative timestamp ensures more recent elements have priority when frequencies are equal
- The actual value is stored as the third element
Pop Operation (pop
):
- Extract the top element from the heap:
val = heappop(self.q)[2]
- This gives us the element with the highest frequency
- If frequencies are tied, it gives us the most recently pushed one
- Decrement the frequency count:
self.cnt[val] -= 1
- Return the popped value:
return val
Key Insights:
-
No Heap Cleanup: The implementation doesn't remove outdated entries from the heap. When an element is popped, its frequency decreases, but old entries with higher frequencies remain in the heap. This is acceptable because:
- We always push new entries with current frequencies
- The heap will naturally prioritize the most recent, accurate entries
- This avoids the costly operation of searching and removing from the heap
-
Time Complexity:
- Push:
O(log n)
where n is the number of entries in the heap - Pop:
O(log n)
for the heap operation
- Push:
-
Space Complexity:
O(n)
where n is the total number of push operations, as we store all entries in the heap without cleanup.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with the sequence of operations:
push(4)
,push(5)
,push(4)
,push(6)
,push(5)
,push(4)
,pop()
,pop()
Initial State:
cnt = {}
(empty frequency counter)q = []
(empty heap)ts = 0
(timestamp counter)
Step 1: push(4)
- Increment timestamp:
ts = 1
- Update frequency:
cnt[4] = 1
- Add to heap:
heappush(q, (-1, -1, 4))
- State:
cnt = {4: 1}
,q = [(-1, -1, 4)]
Step 2: push(5)
- Increment timestamp:
ts = 2
- Update frequency:
cnt[5] = 1
- Add to heap:
heappush(q, (-1, -2, 5))
- State:
cnt = {4: 1, 5: 1}
,q = [(-1, -2, 5), (-1, -1, 4)]
- Note: 5 has higher priority due to more recent timestamp
Step 3: push(4)
- Increment timestamp:
ts = 3
- Update frequency:
cnt[4] = 2
- Add to heap:
heappush(q, (-2, -3, 4))
- State:
cnt = {4: 2, 5: 1}
,q = [(-2, -3, 4), (-1, -2, 5), (-1, -1, 4)]
- The heap now has 4 at the top (frequency 2 beats frequency 1)
Step 4: push(6)
- Increment timestamp:
ts = 4
- Update frequency:
cnt[6] = 1
- Add to heap:
heappush(q, (-1, -4, 6))
- State:
cnt = {4: 2, 5: 1, 6: 1}
, heap has 4 with frequency 2 at top
Step 5: push(5)
- Increment timestamp:
ts = 5
- Update frequency:
cnt[5] = 2
- Add to heap:
heappush(q, (-2, -5, 5))
- State:
cnt = {4: 2, 5: 2, 6: 1}
, heap has 5 with (-2, -5) at top- Both 4 and 5 have frequency 2, but 5 was pushed more recently
Step 6: push(4)
- Increment timestamp:
ts = 6
- Update frequency:
cnt[4] = 3
- Add to heap:
heappush(q, (-3, -6, 4))
- State:
cnt = {4: 3, 5: 2, 6: 1}
, heap has 4 with frequency 3 at top
Step 7: pop()
- Extract from heap:
heappop(q)
returns(-3, -6, 4)
, soval = 4
- Decrement frequency:
cnt[4] = 2
- Return:
4
- State:
cnt = {4: 2, 5: 2, 6: 1}
- Note: Old entries for 4 with frequencies 1 and 2 still exist in heap but are now outdated
Step 8: pop()
- Extract from heap: Top is
(-2, -5, 5)
(5 with frequency 2, timestamp 5)- This beats
(-2, -3, 4)
(4 with frequency 2, timestamp 3) because 5 is more recent
- This beats
- Pop returns
val = 5
- Decrement frequency:
cnt[5] = 1
- Return:
5
The key observation is that the heap maintains all historical entries, but the combination of frequency and timestamp ensures we always get the correct element. The outdated entries (like the old frequency-1 entry for 4) never interfere because newer entries with higher frequencies naturally take priority.
Solution Implementation
1from collections import defaultdict
2from heapq import heappush, heappop
3from typing import Optional
4
5
6class FreqStack:
7 """
8 A stack-like data structure that pops the most frequently occurring element.
9 When multiple elements have the same frequency, the most recently pushed element is popped.
10 """
11
12 def __init__(self):
13 """Initialize the frequency stack with necessary data structures."""
14 # Dictionary to track the frequency count of each element
15 self.frequency_count = defaultdict(int)
16
17 # Min heap (priority queue) to maintain elements by frequency and recency
18 # Stored as tuples: (-frequency, -timestamp, value)
19 # Negative values used to simulate max heap behavior
20 self.priority_queue = []
21
22 # Timestamp counter to track insertion order
23 self.timestamp = 0
24
25 def push(self, val: int) -> None:
26 """
27 Push an element onto the frequency stack.
28
29 Args:
30 val: The integer value to push onto the stack
31 """
32 # Increment timestamp for this operation
33 self.timestamp += 1
34
35 # Update frequency count for this value
36 self.frequency_count[val] += 1
37
38 # Add to priority queue with negative frequency and timestamp for max heap behavior
39 # Priority order: highest frequency first, then most recent timestamp
40 heappush(self.priority_queue,
41 (-self.frequency_count[val], -self.timestamp, val))
42
43 def pop(self) -> int:
44 """
45 Pop and return the most frequent element from the stack.
46 If there's a tie in frequency, return the most recently pushed element.
47
48 Returns:
49 The popped integer value
50 """
51 # Extract the value with highest priority (most frequent, most recent)
52 _, _, value = heappop(self.priority_queue)
53
54 # Decrease the frequency count for this value
55 self.frequency_count[value] -= 1
56
57 return value
58
59
60# Your FreqStack object will be instantiated and called as such:
61# obj = FreqStack()
62# obj.push(val)
63# param_2 = obj.pop()
64
1class FreqStack {
2 // Map to track the frequency count of each element
3 private Map<Integer, Integer> frequencyMap = new HashMap<>();
4
5 // Priority queue to maintain elements based on frequency and insertion order
6 // Each array contains: [frequency, timestamp, value]
7 // Ordering: higher frequency first, then more recent timestamp first
8 private PriorityQueue<int[]> priorityQueue = new PriorityQueue<>((a, b) -> {
9 if (a[0] == b[0]) {
10 // Same frequency: compare by timestamp (larger timestamp = more recent)
11 return b[1] - a[1];
12 }
13 // Different frequency: higher frequency comes first
14 return b[0] - a[0];
15 });
16
17 // Timestamp counter to track insertion order
18 private int timestamp;
19
20 /**
21 * Constructor to initialize the FreqStack
22 */
23 public FreqStack() {
24 }
25
26 /**
27 * Pushes an element onto the stack
28 * @param val the value to be pushed
29 */
30 public void push(int val) {
31 // Update the frequency count for this value
32 frequencyMap.put(val, frequencyMap.getOrDefault(val, 0) + 1);
33
34 // Add to priority queue with current frequency, timestamp, and value
35 priorityQueue.offer(new int[] {frequencyMap.get(val), ++timestamp, val});
36 }
37
38 /**
39 * Removes and returns the most frequent element from the stack
40 * If there is a tie for most frequent element, the most recently pushed element is removed
41 * @return the popped value
42 */
43 public int pop() {
44 // Remove the element with highest priority (highest frequency, most recent)
45 int value = priorityQueue.poll()[2];
46
47 // Decrease the frequency count for this value
48 frequencyMap.put(value, frequencyMap.get(value) - 1);
49
50 return value;
51 }
52}
53
54/**
55 * Your FreqStack object will be instantiated and called as such:
56 * FreqStack obj = new FreqStack();
57 * obj.push(val);
58 * int param_2 = obj.pop();
59 */
60
1class FreqStack {
2public:
3 FreqStack() {
4 // Constructor - no initialization needed as member variables are already initialized
5 }
6
7 void push(int val) {
8 // Increment the frequency count for this value
9 ++frequency[val];
10
11 // Push a tuple to the priority queue containing:
12 // 1. frequency[val]: current frequency of the value (higher frequency = higher priority)
13 // 2. ++timestamp: incrementing timestamp (later pushed = higher priority for same frequency)
14 // 3. val: the actual value
15 maxHeap.emplace(frequency[val], ++timestamp, val);
16 }
17
18 int pop() {
19 // Get the top element from the priority queue (most frequent, most recent)
20 auto [freq, time, value] = maxHeap.top();
21 maxHeap.pop();
22
23 // Decrement the frequency count for this value
24 --frequency[value];
25
26 return value;
27 }
28
29private:
30 // Maps each value to its current frequency in the stack
31 unordered_map<int, int> frequency;
32
33 // Max heap that stores tuples of (frequency, timestamp, value)
34 // Elements are ordered by frequency first, then by timestamp
35 priority_queue<tuple<int, int, int>> maxHeap;
36
37 // Global timestamp counter to track insertion order
38 int timestamp = 0;
39};
40
41/**
42 * Your FreqStack object will be instantiated and called as such:
43 * FreqStack* obj = new FreqStack();
44 * obj->push(val);
45 * int param_2 = obj->pop();
46 */
47
1// Maps each value to its current frequency in the stack
2const frequency: Map<number, number> = new Map();
3
4// Array of tuples storing [frequency, timestamp, value]
5// We'll manually maintain this as a max heap
6const maxHeap: Array<[number, number, number]> = [];
7
8// Global timestamp counter to track insertion order
9let timestamp: number = 0;
10
11// Helper function to compare heap elements
12// Returns true if a should have higher priority than b
13function compareTuples(a: [number, number, number], b: [number, number, number]): boolean {
14 // First compare by frequency (higher frequency = higher priority)
15 if (a[0] !== b[0]) {
16 return a[0] > b[0];
17 }
18 // If frequencies are equal, compare by timestamp (later timestamp = higher priority)
19 return a[1] > b[1];
20}
21
22// Helper function to maintain heap property after insertion
23function heapifyUp(index: number): void {
24 while (index > 0) {
25 const parentIndex = Math.floor((index - 1) / 2);
26 if (compareTuples(maxHeap[index], maxHeap[parentIndex])) {
27 // Swap with parent
28 [maxHeap[index], maxHeap[parentIndex]] = [maxHeap[parentIndex], maxHeap[index]];
29 index = parentIndex;
30 } else {
31 break;
32 }
33 }
34}
35
36// Helper function to maintain heap property after removal
37function heapifyDown(index: number): void {
38 const length = maxHeap.length;
39 while (true) {
40 let largest = index;
41 const leftChild = 2 * index + 1;
42 const rightChild = 2 * index + 2;
43
44 if (leftChild < length && compareTuples(maxHeap[leftChild], maxHeap[largest])) {
45 largest = leftChild;
46 }
47 if (rightChild < length && compareTuples(maxHeap[rightChild], maxHeap[largest])) {
48 largest = rightChild;
49 }
50
51 if (largest !== index) {
52 // Swap with largest child
53 [maxHeap[index], maxHeap[largest]] = [maxHeap[largest], maxHeap[index]];
54 index = largest;
55 } else {
56 break;
57 }
58 }
59}
60
61function push(val: number): void {
62 // Increment the frequency count for this value
63 const currentFreq = (frequency.get(val) || 0) + 1;
64 frequency.set(val, currentFreq);
65
66 // Increment timestamp for this insertion
67 timestamp++;
68
69 // Add tuple to the heap containing:
70 // 1. currentFreq: current frequency of the value (higher frequency = higher priority)
71 // 2. timestamp: current timestamp (later pushed = higher priority for same frequency)
72 // 3. val: the actual value
73 maxHeap.push([currentFreq, timestamp, val]);
74
75 // Maintain heap property
76 heapifyUp(maxHeap.length - 1);
77}
78
79function pop(): number {
80 // Get the top element from the heap (most frequent, most recent)
81 const [freq, time, value] = maxHeap[0];
82
83 // Move last element to top and remove the last element
84 maxHeap[0] = maxHeap[maxHeap.length - 1];
85 maxHeap.pop();
86
87 // Maintain heap property if heap is not empty
88 if (maxHeap.length > 0) {
89 heapifyDown(0);
90 }
91
92 // Decrement the frequency count for this value
93 const currentFreq = frequency.get(value)! - 1;
94 if (currentFreq === 0) {
95 frequency.delete(value);
96 } else {
97 frequency.set(value, currentFreq);
98 }
99
100 return value;
101}
102
Time and Space Complexity
Time Complexity:
__init__()
:O(1)
- Initializing empty data structures takes constant time.push(val)
:O(log n)
- Incrementing the counter and timestamp areO(1)
operations, but inserting into the heap usingheappush
takesO(log n)
time wheren
is the number of elements in the heap.pop()
:O(log n)
- Removing the top element from the heap usingheappop
takesO(log n)
time, and decrementing the counter isO(1)
.
Space Complexity:
O(n)
wheren
is the total number of push operations performed.- The heap
q
stores one entry for each push operation, never removing entries until they are popped, so it can grow up to sizen
. - The counter dictionary
cnt
stores at mostO(m)
entries wherem
is the number of unique values pushed, which is bounded byn
. - The timestamp variable
ts
usesO(1)
space. - Overall space complexity is
O(n)
as the heap dominates the space usage.
Note: The heap stores tuples of (-frequency, -timestamp, value)
using negative values to achieve max-heap behavior since Python's heapq
implements a min-heap. This ensures elements with higher frequency are popped first, and among elements with the same frequency, the most recently pushed element (larger timestamp) is popped first.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Heap Pollution with Stale Entries
The Problem: The current implementation never removes outdated entries from the heap. Each time an element is pushed, a new entry is added to the heap with its current frequency, but when we pop an element and decrease its frequency, the old heap entries with higher frequencies remain. Over time, this leads to:
- Memory bloat as the heap grows unbounded
- Potential confusion when debugging, as the heap contains many "ghost" entries
- In extreme cases with many operations, this could cause memory issues
Example Scenario:
fs = FreqStack()
# Push the same element 1000 times
for _ in range(1000):
fs.push(5)
# Now heap has 1000 entries for element 5 with frequencies 1, 2, 3, ..., 1000
# Pop element 5 once
fs.pop() # Returns 5, frequency becomes 999
# But heap still contains the entry (-1000, timestamp, 5) which is now invalid
Solution Approach 1: Lazy Validation Instead of cleaning the heap, validate entries when popping:
def pop(self) -> int:
"""
Pop with validation to handle stale entries in the heap.
"""
while self.priority_queue:
neg_freq, neg_ts, value = heappop(self.priority_queue)
actual_freq = self.frequency_count[value]
# Check if this entry is still valid (frequency matches current count)
if -neg_freq == actual_freq and actual_freq > 0:
# Valid entry - decrease frequency and return
self.frequency_count[value] -= 1
return value
# Otherwise, this is a stale entry - continue to next
# Should not reach here if used correctly
raise IndexError("pop from empty FreqStack")
Solution Approach 2: Frequency Group Stacks A cleaner alternative design that avoids heap pollution entirely:
class FreqStack:
def __init__(self):
self.freq = {} # element -> frequency
self.group = defaultdict(list) # frequency -> stack of elements
self.max_freq = 0
def push(self, val: int) -> None:
freq = self.freq.get(val, 0) + 1
self.freq[val] = freq
self.group[freq].append(val)
self.max_freq = max(self.max_freq, freq)
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 val
Pitfall 2: Integer Overflow with Timestamps
The Problem: The timestamp counter continuously increments with each push operation. In a long-running application with millions of operations, this could theoretically overflow (though Python handles arbitrary precision integers, in other languages this would be a concern).
Solution: Use a relative ordering mechanism or periodically rebuild the heap when it gets too large:
def push(self, val: int) -> None:
# Rebuild heap if it gets too polluted
if len(self.priority_queue) > 10 * len(self.frequency_count):
self._rebuild_heap()
self.timestamp += 1
self.frequency_count[val] += 1
heappush(self.priority_queue,
(-self.frequency_count[val], -self.timestamp, val))
def _rebuild_heap(self):
"""Rebuild heap with only valid entries to prevent unbounded growth."""
new_heap = []
seen = set()
for neg_freq, neg_ts, val in self.priority_queue:
if val not in seen and self.frequency_count[val] > 0:
seen.add(val)
new_heap.append((-self.frequency_count[val], neg_ts, val))
self.priority_queue = new_heap
heapify(self.priority_queue)
Which technique can we use to find the middle of a linked list?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!