You are given two integers, m and k, and a stream of integers. You are tasked to implement a data structure that calculates the MKAverage for the stream.
The MKAverage can be calculated using these steps:
m you should consider the MKAverage to be -1. Otherwise, copy the last m elements of the stream to a separate container.k elements and the largest k elements from the container.Implement the MKAverage class:
MKAverage(int m, int k) Initializes the MKAverage object with an empty stream and the two integers m and k.void addElement(int num) Inserts a new element num into the stream.int calculateMKAverage() Calculates and returns the MKAverage for the current stream rounded down to the nearest integer.Example 1:
Input
["MKAverage", "addElement", "addElement", "calculateMKAverage", "addElement", "calculateMKAverage", "addElement", "addElement", "addElement", "calculateMKAverage"]
[[3, 1], [3], [1], [], [10], [], [5], [5], [5], []]
Output
[null, null, null, -1, null, 3, null, null, null, 5]
Explanation
MKAverage obj = new MKAverage(3, 1);
obj.addElement(3); // current elements are [3]
obj.addElement(1); // current elements are [3,1]
obj.calculateMKAverage(); // return -1, because m = 3 and only 2 elements exist.
obj.addElement(10); // current elements are [3,1,10]
obj.calculateMKAverage(); // The last 3 elements are [3,1,10].
// After removing smallest and largest 1 element the container will be [3].
// The average of [3] equals 3/1 = 3, return 3
obj.addElement(5); // current elements are [3,1,10,5]
obj.addElement(5); // current elements are [3,1,10,5,5]
obj.addElement(5); // current elements are [3,1,10,5,5,5]
obj.calculateMKAverage(); // The last 3 elements are [5,5,5].
// After removing smallest and largest 1 element the container will be [5].
// The average of [5] equals 5/1 = 5, return 5
Constraints:
3 <= m <= 1051 < k*2 < m1 <= num <= 105105 calls will be made to addElement and calculateMKAverage.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:
The straightforward approach is to maintain a running history of all the numbers added. Each time we need to find the MK average, we'll perform a fresh calculation by looking at only the most recent group of numbers, ordering them, removing the smallest and largest ones, and then averaging what's left.
Here's how the algorithm would work step-by-step:
class MKAverage:
def __init__(self, m: int, k: int):
self.m_value = m
self.k_value = k
# A list is used to maintain the full history of numbers in their original order.
self.number_history = []
def addElement(self, num: int) -> None:
self.number_history.append(num)
def calculateMKAverage(self) -> int:
# The MK Average is undefined until we have at least 'm' numbers to consider.
if len(self.number_history) < self.m_value:
return -1
recent_m_numbers = self.number_history[-self.m_value:]
# Sorting is the core of this simple approach to easily find and remove the extremes.
sorted_recent_numbers = sorted(recent_m_numbers)
# We slice the sorted list to remove the k smallest and k largest elements.
numbers_for_average = sorted_recent_numbers[self.k_value:-self.k_value]
sum_of_middle_elements = sum(numbers_for_average)
count_of_middle_elements = self.m_value - (2 * self.k_value)
return sum_of_middle_elements // count_of_middle_elementsInstead of repeatedly sorting all the recent numbers every time, the optimal approach keeps them organized into three separate groups: the smallest, the largest, and the middle ones. By constantly keeping these groups balanced as new numbers arrive and old ones depart, the numbers needed for the average are always ready for a quick calculation.
Here's how the algorithm would work step-by-step:
class MKAverage:
def __init__(self, m: int, k: int):
self.window_size_m = m
self.trim_size_k = k
self.recent_numbers_queue = collections.deque()
self.smalls_box = []
self.middles_box = []
self.bigs_box = []
self.middle_elements_sum = 0
def addElement(self, new_element: int) -> None:
# First, handle removing the oldest element to maintain the m-sized sliding window.
if len(self.recent_numbers_queue) == self.window_size_m:
element_to_remove = self.recent_numbers_queue.popleft()
if self.smalls_box and element_to_remove <= self.smalls_box[-1]:
self.smalls_box.pop(bisect.bisect_left(self.smalls_box, element_to_remove))
elif self.bigs_box and element_to_remove >= self.bigs_box[0]:
self.bigs_box.pop(bisect.bisect_left(self.bigs_box, element_to_remove))
else:
self.middles_box.pop(bisect.bisect_left(self.middles_box, element_to_remove))
self.middle_elements_sum -= element_to_remove
self.recent_numbers_queue.append(new_element)
# The new number is added to a box based on the boundaries of the sorted groups.
if self.smalls_box and new_element <= self.smalls_box[-1]:
bisect.insort_left(self.smalls_box, new_element)
elif self.bigs_box and new_element >= self.bigs_box[0]:
bisect.insort_left(self.bigs_box, new_element)
else:
bisect.insort_left(self.middles_box, new_element)
self.middle_elements_sum += new_element
# After addition and removal, the boxes must be rebalanced to their correct sizes.
while len(self.smalls_box) > self.trim_size_k:
element_to_move = self.smalls_box.pop()
bisect.insort_left(self.middles_box, element_to_move)
self.middle_elements_sum += element_to_move
while len(self.bigs_box) > self.trim_size_k:
element_to_move = self.bigs_box.pop(0)
bisect.insort_left(self.middles_box, element_to_move)
self.middle_elements_sum += element_to_move
# Conversely, if boundary boxes are undersized, they are replenished from the middles box.
while len(self.smalls_box) < self.trim_size_k and self.middles_box:
element_to_move = self.middles_box.pop(0)
bisect.insort_left(self.smalls_box, element_to_move)
self.middle_elements_sum -= element_to_move
while len(self.bigs_box) < self.trim_size_k and self.middles_box:
element_to_move = self.middles_box.pop()
bisect.insort_left(self.bigs_box, element_to_move)
self.middle_elements_sum -= element_to_move
def calculateMKAverage(self) -> int:
if len(self.recent_numbers_queue) < self.window_size_m:
return -1
middle_box_expected_size = self.window_size_m - 2 * self.trim_size_k
return self.middle_elements_sum // middle_box_expected_size| Case | How to Handle |
|---|---|
| Calculating the average before m elements have been added. | The solution must check the stream size and return -1 as specified in the problem description. |
| The sliding window contains many or all duplicate values. | Data structures must correctly handle multisets to ensure the k smallest and k largest elements are identified properly, including their duplicates. |
| The sum of the middle m - 2k elements overflows a standard 32-bit integer. | The running sum must be maintained using a 64-bit integer type to prevent overflow, as the sum can reach up to 10^10. |
| Maximum constraints are hit for m, k, and the number of calls. | An efficient solution using balanced data structures is required for addElement to achieve logarithmic time complexity and avoid a timeout. |
| An element is added when the window is full, causing the oldest element to be removed. | A queue must be used to track the window's elements and correctly remove the outgoing element from the sorted data structures. |
| The input stream is a strictly increasing or decreasing sequence. | The rebalancing logic must efficiently handle the constant migration of elements between the small, middle, and large groups as the window slides. |
| A newly added or removed element is a boundary value between partitions. | The logic must carefully rebalance elements between the three groups to maintain their size and ordering invariants. |
| The parameters m and k are at their minimum valid values, such as m=3, k=1. | The partitioning logic must be robust enough to handle the middle group containing just a single element. |