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.
Example 1:
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 is 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 is the most frequent, but 4 is closest to the top. The stack becomes [5,7].
Constraints:
0 <= val <= 109
2 * 104
calls will be made to push
and pop
.pop
.When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
We want to simulate a stack that always returns the most frequent element. The brute force way means, for every request, we re-calculate everything from scratch. We won't be clever or save any work from before.
Here's how the algorithm would work step-by-step:
class FrequencyStack:
def __init__(self):
self.stack = []
def push(self, value):
self.stack.append(value)
def pop(self):
# Need to calculate frequencies for each pop
frequency_counts = {}
for number in self.stack:
frequency_counts[number] = frequency_counts.get(number, 0) + 1
maximum_frequency = 0
for number in frequency_counts:
maximum_frequency = max(maximum_frequency, frequency_counts[number])
most_frequent_numbers = []
for number in frequency_counts:
if frequency_counts[number] == maximum_frequency:
most_frequent_numbers.append(number)
# Find the closest to the top
index_to_remove = -1
for i in range(len(self.stack) - 1, -1, -1):
if self.stack[i] in most_frequent_numbers:
index_to_remove = i
# Pick the last one
break
# Store the value so that we can return it
removed_value = self.stack[index_to_remove]
# Modify the stack, removing the value we want
self.stack.pop(index_to_remove)
return removed_value
The goal is to mimic a stack but prioritize elements that appear more frequently. We keep track of the frequency of each number and use that information to simulate popping from different stacks depending on the frequency.
Here's how the algorithm would work step-by-step:
class FrequencyStack:
def __init__(self):
self.frequency_of_number = {}
self.stacks_by_frequency = {}
self.maximum_frequency = 0
def push(self, number):
frequency = self.frequency_of_number.get(number, 0) + 1
self.frequency_of_number[number] = frequency
# We need to update the maximum frequency
self.maximum_frequency = max(self.maximum_frequency, frequency)
if frequency not in self.stacks_by_frequency:
self.stacks_by_frequency[frequency] = []
self.stacks_by_frequency[frequency].append(number)
def pop(self):
# Pop from the stack that contains the most frequent numbers
number = self.stacks_by_frequency[self.maximum_frequency].pop()
if not self.stacks_by_frequency[self.maximum_frequency]:
self.maximum_frequency -= 1
self.frequency_of_number[number] -= 1
return number
Case | How to Handle |
---|---|
Pushing and popping from an empty FreqStack | The frequencies and groups maps must be initialized correctly and the pop operation should gracefully handle an empty stack by possibly returning a specific value or throwing an exception. |
Large number of push operations leading to significant memory usage | The implementation needs to consider the memory footprint of storing frequencies and groups of elements to avoid out-of-memory errors. |
Extreme values of 'val' (very large positive or negative integers) | The solution should be robust enough to handle large integer values without causing integer overflows or other issues related to data type limits. |
Popping from a stack where all elements have the same frequency | The stack should correctly pop the element closest to the top, following the LIFO principle in cases of equal frequency. |
Pushing the same 'val' multiple times consecutively | The frequency counter should correctly increment and decrement even with repeated values, and the groups mapping should be updated accurately. |
Interleaved push and pop operations resulting in complex frequency distributions | The frequency tracking and group management must be able to handle a variety of push/pop sequences without losing accuracy. |
A pop operation when the stack is not empty, but the most frequent element has been completely removed | The frequency and group mappings need to be cleaned up appropriately during pops to prevent the algorithm from getting stuck or returning incorrect values after an element's frequency drops to zero. |
Maximum number of operations performed leading to performance bottlenecks. | The time complexity of push and pop operations should be efficient enough to handle a large number of operations within a reasonable time frame using data structures like hash maps and stacks. |