211 lines
10 KiB
YAML
211 lines
10 KiB
YAML
title: Maximum Frequency Stack
|
|
slug: maximum-frequency-stack
|
|
difficulty: hard
|
|
leetcode_id: 895
|
|
leetcode_url: https://leetcode.com/problems/maximum-frequency-stack/
|
|
categories:
|
|
- stack
|
|
- hash-tables
|
|
patterns:
|
|
- heap
|
|
|
|
description: |
|
|
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 integer `val` onto 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.
|
|
|
|
constraints: |
|
|
- `0 <= val <= 10^9`
|
|
- At most `2 * 10^4` calls will be made to `push` and `pop`.
|
|
- It is guaranteed that there will be at least one element in the stack before calling `pop`.
|
|
|
|
examples:
|
|
- 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 are 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 are the most frequent, but 4 is closest to the top. The stack becomes [5,7].
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you're managing a priority queue at a busy restaurant, but with a twist: instead of "first come, first served," you want to serve the customer who has visited most often. And if two customers have visited equally often, you serve the one who arrived most recently.
|
|
|
|
The key insight is to think about **frequency as a level**. Each time an element appears, it "graduates" to a higher frequency level. When we pop, we want the element at the highest frequency level, and if there are multiple elements at that level, we want the most recently pushed one.
|
|
|
|
Think of it like this: imagine having a **stack of stacks**, where each inner stack holds all elements that have reached a particular frequency. When you push `5` for the first time, it goes on the frequency-1 stack. Push `5` again, and a copy goes on the frequency-2 stack. The beauty is that popping from the highest frequency stack automatically gives you the most recent element at that frequency!
|
|
|
|
This "stack of stacks" model elegantly handles both requirements: maximum frequency (track the highest level) and recency (each level is a stack, so LIFO within that level).
|
|
|
|
approach: |
|
|
We solve this using **two hash maps and a stack-of-stacks structure**:
|
|
|
|
**Step 1: Design the data structures**
|
|
|
|
- `freq`: A hash map mapping each value to its current frequency count
|
|
- `group`: A hash map mapping each frequency level to a stack of values at that level
|
|
- `max_freq`: An integer tracking the current maximum frequency
|
|
|
|
|
|
|
|
**Step 2: Implement push(val)**
|
|
|
|
- Increment the frequency of `val` in the `freq` map
|
|
- Get the new frequency `f` for this value
|
|
- Append `val` to the stack at `group[f]`
|
|
- Update `max_freq` if `f` exceeds it
|
|
|
|
|
|
|
|
**Step 3: Implement pop()**
|
|
|
|
- Look at the stack at `group[max_freq]` — this contains all elements with the highest frequency
|
|
- Pop from this stack to get the most recent element at max frequency
|
|
- Decrement that element's frequency in the `freq` map
|
|
- If the stack at `max_freq` is now empty, decrement `max_freq`
|
|
- Return the popped element
|
|
|
|
|
|
|
|
The elegance of this approach is that we never need to remove elements from middle of structures — we only ever pop from the top of stacks, making all operations O(1).
|
|
|
|
common_pitfalls:
|
|
- title: Using a Heap Instead of Stack-of-Stacks
|
|
description: |
|
|
A natural instinct is to use a max-heap keyed by (frequency, timestamp) to always get the most frequent, most recent element.
|
|
|
|
While this works, it results in **O(log n) operations** instead of O(1). The heap needs to rebalance on every push and pop. With up to `2 * 10^4` operations, this is acceptable but suboptimal.
|
|
|
|
The stack-of-stacks approach achieves O(1) by cleverly avoiding the need for sorting — stacks naturally maintain insertion order within each frequency level.
|
|
wrong_approach: "Max-heap with (frequency, timestamp) tuples"
|
|
correct_approach: "Stack-of-stacks grouped by frequency"
|
|
|
|
- title: Forgetting to Track max_freq
|
|
description: |
|
|
Without tracking `max_freq`, you'd need to scan through all frequency levels on each pop to find the highest non-empty stack.
|
|
|
|
This would degrade pop to O(n) in the worst case, where n is the number of distinct frequencies. By maintaining `max_freq` and decrementing it only when the top stack empties, we keep pop at O(1).
|
|
wrong_approach: "Scanning all frequency levels to find max"
|
|
correct_approach: "Track max_freq and decrement when needed"
|
|
|
|
- title: Removing Elements from the Frequency Map
|
|
description: |
|
|
When popping, don't remove the element from the `freq` map even if its frequency drops to zero. This creates unnecessary complexity.
|
|
|
|
Simply decrement the frequency. If the element is pushed again later, its count will increment from where it left off. The `group` stacks handle the "active" elements at each level.
|
|
wrong_approach: "Removing entries when frequency reaches zero"
|
|
correct_approach: "Just decrement frequency, let it reach zero"
|
|
|
|
key_takeaways:
|
|
- "**Stack-of-stacks pattern**: When you need both priority ordering and recency, consider grouping stacks by priority level"
|
|
- "**O(1) design**: The key insight is that we only ever interact with the *top* of stacks, avoiding expensive search or rebalancing operations"
|
|
- "**Frequency as a level**: This mental model — elements 'graduating' to higher frequency levels — helps visualise the structure"
|
|
- "**Design problem strategy**: Break down the requirements (max frequency + recency) and find a data structure that satisfies both simultaneously"
|
|
|
|
time_complexity: "O(1) for both `push` and `pop`. Each operation involves constant-time hash map lookups and stack operations."
|
|
space_complexity: "O(n) where n is the number of elements pushed. We store each element in the frequency map and potentially in multiple stacks (once per occurrence)."
|
|
|
|
solutions:
|
|
- approach_name: Stack of Stacks
|
|
is_optimal: true
|
|
code: |
|
|
from collections import defaultdict
|
|
|
|
|
|
class FreqStack:
|
|
def __init__(self):
|
|
# Maps each value to its current frequency
|
|
self.freq = defaultdict(int)
|
|
# Maps each frequency to a stack of values at that frequency
|
|
self.group = defaultdict(list)
|
|
# Track the current maximum frequency
|
|
self.max_freq = 0
|
|
|
|
def push(self, val: int) -> None:
|
|
# Increment frequency for this value
|
|
self.freq[val] += 1
|
|
f = self.freq[val]
|
|
|
|
# Add to the stack at this frequency level
|
|
self.group[f].append(val)
|
|
|
|
# Update max frequency if this is a new high
|
|
if f > self.max_freq:
|
|
self.max_freq = f
|
|
|
|
def pop(self) -> int:
|
|
# Pop from the highest frequency stack (most recent at that level)
|
|
val = self.group[self.max_freq].pop()
|
|
|
|
# Decrement this value's frequency
|
|
self.freq[val] -= 1
|
|
|
|
# If no more elements at max frequency, decrease max_freq
|
|
if not self.group[self.max_freq]:
|
|
self.max_freq -= 1
|
|
|
|
return val
|
|
explanation: |
|
|
**Time Complexity:** O(1) for both push and pop — all operations are hash map lookups and stack push/pop.
|
|
|
|
**Space Complexity:** O(n) — we store each pushed element in the group stacks, and each unique value in the freq map.
|
|
|
|
This solution uses the "frequency as a level" insight. Each value at frequency f appears in `group[f]`. Popping from the highest level gives us the most frequent element, and since each level is a stack, we automatically get the most recently pushed element at that frequency.
|
|
|
|
- approach_name: Heap-Based
|
|
is_optimal: false
|
|
code: |
|
|
import heapq
|
|
from collections import defaultdict
|
|
|
|
|
|
class FreqStack:
|
|
def __init__(self):
|
|
# Maps each value to its current frequency
|
|
self.freq = defaultdict(int)
|
|
# Max-heap storing (-freq, -timestamp, val)
|
|
self.heap = []
|
|
# Timestamp counter for tie-breaking
|
|
self.timestamp = 0
|
|
|
|
def push(self, val: int) -> None:
|
|
# Increment frequency and timestamp
|
|
self.freq[val] += 1
|
|
self.timestamp += 1
|
|
|
|
# Push to heap with negative values for max-heap behavior
|
|
# (-frequency, -timestamp, val)
|
|
heapq.heappush(self.heap, (-self.freq[val], -self.timestamp, val))
|
|
|
|
def pop(self) -> int:
|
|
# Pop the element with max frequency (and most recent if tied)
|
|
_, _, val = heapq.heappop(self.heap)
|
|
|
|
# Decrement frequency
|
|
self.freq[val] -= 1
|
|
|
|
return val
|
|
explanation: |
|
|
**Time Complexity:** O(log n) for both push and pop due to heap operations.
|
|
|
|
**Space Complexity:** O(n) — the heap stores every pushed element.
|
|
|
|
This approach uses a max-heap (simulated with negative values in Python's min-heap). Each push adds a tuple of (-frequency, -timestamp, value). The heap automatically orders by maximum frequency, then by most recent timestamp.
|
|
|
|
While simpler to conceptualize, this is less efficient than the stack-of-stacks approach. It's included to show an alternative design and illustrate why the optimal solution is preferred.
|