Files
codetutor/backend/data/questions/maximum-frequency-stack.yaml

249 lines
12 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
function_signature: "class FreqStack: def __init__(self); def push(self, val: int) -> None; def pop(self) -> int"
test_cases:
visible:
- input:
operations: ["FreqStack", "push", "push", "push", "push", "push", "push", "pop", "pop", "pop", "pop"]
arguments: [[], [5], [7], [5], [7], [4], [5], [], [], [], []]
expected: [null, null, null, null, null, null, null, 5, 7, 5, 4]
- input:
operations: ["FreqStack", "push", "push", "push", "pop", "pop"]
arguments: [[], [1], [1], [1], [], []]
expected: [null, null, null, null, 1, 1]
hidden:
- input:
operations: ["FreqStack", "push", "pop"]
arguments: [[], [10], []]
expected: [null, null, 10]
- input:
operations: ["FreqStack", "push", "push", "push", "pop", "push", "pop"]
arguments: [[], [1], [2], [3], [], [1], []]
expected: [null, null, null, null, 3, null, 1]
- input:
operations: ["FreqStack", "push", "push", "push", "push", "pop", "pop"]
arguments: [[], [5], [5], [5], [5], [], []]
expected: [null, null, null, null, null, 5, 5]
- input:
operations: ["FreqStack", "push", "push", "push", "push", "push", "pop", "pop", "pop"]
arguments: [[], [1], [2], [1], [2], [1], [], [], []]
expected: [null, null, null, null, null, null, 1, 2, 1]
- input:
operations: ["FreqStack", "push", "push", "push", "push", "push", "push", "pop", "push", "pop", "pop"]
arguments: [[], [4], [0], [9], [3], [4], [2], [], [6], [], []]
expected: [null, null, null, null, null, null, null, 4, null, 6, 2]
- input:
operations: ["FreqStack", "push", "push", "pop", "push", "pop", "push", "pop"]
arguments: [[], [1], [1], [], [2], [], [2], []]
expected: [null, null, null, 1, null, 2, null, 2]
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
&nbsp;
**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
&nbsp;
**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
&nbsp;
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.