AlgoMaster Logo

Maximum Frequency Stack

Last Updated: April 4, 2026

hard
4 min read

Understanding the Problem

We need to build a special stack where pop doesn't just remove the top element. Instead, it removes the element that appears most frequently. If multiple elements share the highest frequency, we break the tie by choosing the one that was pushed most recently (closest to the top).

This is tricky because a normal stack only cares about insertion order, and a normal priority queue only cares about some priority. Here we need both: frequency as the primary sort key, and recency as the tiebreaker. The question is how to combine these two dimensions efficiently.

The key observation is that frequency naturally forms "layers." When an element is pushed for the first time, it appears at frequency 1. When pushed again, it also appears at frequency 2. Think of each push as adding the element to a new frequency level. When we pop, we always pop from the highest frequency level, and within that level we want the most recently added element. That sounds a lot like maintaining a stack per frequency level.

Key Constraints:

  • 0 <= val <= 10^9 -> Values can be very large, so we can't use a fixed-size array indexed by value. A hash map is needed for tracking frequencies.
  • At most 2 * 10^4 calls to push and pop -> With 20,000 operations, even an O(n) approach per call would finish in time. But the problem is a design question, so interviewers expect O(1) per operation.
  • At least one element before pop -> We don't need to handle empty-stack edge cases for pop.

Approach 1: Brute Force with Sorting

Intuition

The most straightforward idea: every time we pop, scan through all elements in the stack, find the one with the highest frequency, and among ties pick the most recently pushed one. We can simulate this by keeping the full push history and computing frequencies on the fly.

Think of it as replaying the stack's history each time we need to make a decision. It's simple to reason about, and it gets the job done for small inputs.

Algorithm

  1. Maintain a list stack that stores elements in push order.
  2. For push(val): append val to the end of the list.
  3. For pop(): count the frequency of each element in the list. Find the maximum frequency. Among all elements with that frequency, find the one that appears latest in the list (highest index). Remove it from the list and return it.

Example Walkthrough

Input:

0
push(5)
1
push(7)
2
push(5)
3
push(7)
4
push(4)
5
push(5)
6
pop()
operations

For pop(): scan stack [5,7,5,7,4,5]. Frequencies: 5->3, 7->2, 4->1. Max frequency is 3 (only element 5). Rightmost 5 is at index 5. Remove it.

0
5
result

Code

This approach recomputes frequencies from scratch on every pop. What if we maintained frequency counts incrementally and used a data structure that keeps elements sorted by frequency?

Approach 2: Heap-Based Priority Queue

Intuition

The brute force recomputes everything on each pop. A natural improvement is to maintain a max-heap (priority queue) that keeps elements sorted by frequency, with recency as a tiebreaker.

Every time we push a value, we increment its frequency and insert a new entry into the heap with (frequency, timestamp, value). The heap naturally gives us the element with the highest frequency. If two elements have the same frequency, the one with the higher timestamp (pushed more recently) comes out first.

Algorithm

  1. Maintain a hash map freq that tracks the current frequency of each value.
  2. Maintain a max-heap where each entry is (frequency, timestamp, value).
  3. Keep a global timestamp counter that increments on each push.
  4. For push(val): increment freq[val], increment timestamp, and add (freq[val], timestamp, val) to the heap.
  5. For pop(): extract the max element from the heap. This gives the element with the highest frequency (and highest timestamp among ties). Decrement freq[val] for the popped value. Return the value.

Example Walkthrough

1Initial: empty heap
[]
1/8
1Frequency map empty
1/8

Code

The heap approach works but adds an unnecessary log n factor. Since we only ever need the maximum frequency, what if we grouped elements by frequency level and tracked the max directly?

Approach 3: Frequency-Grouped Stacks (Optimal)

Intuition

Here's the key insight that makes O(1) possible. When you push an element and its frequency increases from, say, 2 to 3, think of it as that element now "existing" at frequency level 3. If you maintain a separate stack for each frequency level, then popping becomes trivial: just pop from the stack at the highest frequency level.

Each frequency level's stack preserves the push order for elements at that level. The most recently pushed element at the max frequency level is exactly the element we want to pop. And we don't need to remove elements from lower frequency stacks, because those copies represent the element's presence at lower frequencies, which becomes relevant again after we've popped the higher-frequency occurrence.

Algorithm

  1. Maintain a hash map freq mapping each value to its current frequency.
  2. Maintain a hash map freqToStack mapping each frequency level to a stack (list) of values at that level.
  3. Track maxFreq, the current maximum frequency across all elements.
  4. For push(val): increment freq[val], append val to freqToStack[freq[val]], update maxFreq.
  5. For pop(): pop the top value from freqToStack[maxFreq], decrement freq[val], if the stack is now empty decrement maxFreq. Return the popped value.

Example Walkthrough

1Initial: empty frequency stacks, maxFreq=0
1/9
1Frequency map empty
1/9

Code