Facebook Pixel

895. Maximum Frequency Stack

HardStackDesignHash TableOrdered Set
Leetcode Link

Problem Description

You need to design a special stack data structure called FreqStack that supports two operations:

  1. Push: Add an element to the stack
  2. 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 and 7 now appear 2 times (tied for highest frequency)
  • Since 7 was pushed more recently than the remaining 5, it returns 7

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Hash Table cnt: A defaultdict(int) that maintains the current frequency count for each element in the stack.

  2. Priority Queue q: A max heap implemented using Python's heapq (which is a min heap by default, so we use negative values). Each entry is a tuple (-frequency, -timestamp, value).

  3. 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):

  1. Increment the timestamp: self.ts += 1
  2. Update the frequency count: self.cnt[val] += 1
  3. 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):

  1. 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
  2. Decrement the frequency count: self.cnt[val] -= 1
  3. 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
  • 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 Evaluator

Example 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), so val = 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
  • 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 are O(1) operations, but inserting into the heap using heappush takes O(log n) time where n is the number of elements in the heap.
  • pop(): O(log n) - Removing the top element from the heap using heappop takes O(log n) time, and decrementing the counter is O(1).

Space Complexity:

  • O(n) where n 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 size n.
  • The counter dictionary cnt stores at most O(m) entries where m is the number of unique values pushed, which is bounded by n.
  • The timestamp variable ts uses O(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)
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More