Finding MK Average
HardPrompt
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:
- If the number of the elements in the stream is less than
myou should consider the MKAverage to be-1. Otherwise, copy the lastmelements of the stream to a separate container. - Remove the smallest
kelements and the largestkelements from the container. - Calculate the average value for the rest of the elements rounded down to the nearest integer.
Implement the MKAverage class:
MKAverage(int m, int k)Initializes the MKAverage object with an empty stream and the two integersmandk.void addElement(int num)Inserts a new elementnuminto the stream.int calculateMKAverage()Calculates and returns the MKAverage for the current stream rounded down to the nearest integer.
Example 1:
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 <= 105- At most
105calls will be made toaddElementandcalculateMKAverage.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach directly simulates the process described in the problem. It maintains a sliding window of the last m elements using a queue. For each calculateMKAverage call, it copies these m elements into a temporary list, sorts the list, and then calculates the average of the middle elements. While simple to understand, its performance suffers due to the repeated sorting.
Algorithm
- Initialization: In the constructor, initialize a
Queue(e.g.,LinkedList) to store the stream elements, and store the values ofmandk. addElement(num):- Add the new element
numto the tail of the queue. - If the size of the queue is greater than
m, remove the element from the head of the queue (poll()). This maintains a sliding window of the lastmelements.
- Add the new element
calculateMKAverage():- Check if the number of elements in the queue is less than
m. If it is, return-1. - Create a temporary
ArrayListand copy all elements from the queue into it. - Sort the temporary list using a standard sorting algorithm (e.g.,
Collections.sort()). - Initialize a variable
sumto zero. - Iterate through the sorted list from index
ktom - 1 - k(inclusive) and add each element tosum. - Calculate the average by dividing
sumby the count of middle elements, which ism - 2*k. - Return the result, which is automatically floored due to integer division.
- Check if the number of elements in the queue is less than
Walkthrough
The core idea is to use a Queue data structure to keep track of the last m elements added to the stream. This naturally handles the sliding window requirement.
When addElement(num) is called, the new number is added to the queue. To ensure the queue only contains the last m elements, we check its size after adding. If it exceeds m, we remove the oldest element, which is at the front of the queue.
When calculateMKAverage() is called, the main work begins. First, a size check ensures we have enough elements. If not, -1 is returned. Otherwise, the current m elements from the queue are copied into a new list. This list is then sorted, which is the most time-consuming step (O(m log m)). After sorting, the smallest k elements are at the beginning of the list, and the largest k are at the end. We can simply ignore them by summing the elements from index k up to (but not including) index m - k. The final average is this sum divided by the number of elements considered (m - 2*k).
import java.util.ArrayList;import java.util.Collections;import java.util.LinkedList;import java.util.List;import java.util.Queue; class MKAverage { private int m, k; private Queue<Integer> stream; public MKAverage(int m, int k) { this.m = m; this.k = k; this.stream = new LinkedList<>(); } public void addElement(int num) { stream.offer(num); if (stream.size() > m) { stream.poll(); } } public int calculateMKAverage() { if (stream.size() < m) { return -1; } List<Integer> window = new ArrayList<>(stream); Collections.sort(window); long sum = 0; for (int i = k; i < m - k; i++) { sum += window.get(i); } return (int) (sum / (m - 2 * k)); }}Complexity
Time
`addElement`: O(1) Adding and removing from a queue are constant time operations. `calculateMKAverage`: O(m log m) The dominant operation is sorting the list of `m` elements.
Space
O(m) We need a queue to store the last `m` elements. The temporary list in `calculateMKAverage` also takes `O(m)` space.
Trade-offs
Pros
Easy to understand and implement.
The logic directly follows the problem statement.
Cons
The
calculateMKAveragemethod is inefficient, with a time complexity ofO(m log m).For streams with frequent average calculations, this approach can be very slow, potentially leading to a 'Time Limit Exceeded' error in a competitive programming context.
This approach improves upon the brute-force method by avoiding the expensive sorting step in every calculateMKAverage call. It maintains the sliding window of m elements in a sorted state using a TreeMap, which stores numbers and their frequencies. While adding elements is efficient (O(log m)), calculating the average still requires iterating through the map's elements, resulting in O(m) complexity for that step.
Algorithm
- Initialization: Use a
Queuefor the sliding window and aTreeMapto store the frequency of numbers within the window. TheTreeMapnaturally keeps the numbers sorted. addElement(num):- Add
numto the queue and update its count in theTreeMap. This is anO(log m)operation. - If the queue size exceeds
m, remove the oldest element (oldNum) from the queue. DecrementoldNum's count in theTreeMap, removing it entirely if the count becomes zero. This is alsoO(log m).
- Add
calculateMKAverage():- If the queue size is less than
m, return-1. - Iterate through the
TreeMap's entries (which are sorted by key). - Use a counter to skip the first
kelements (considering their frequencies). - Sum the next
m - 2*kelements. - Stop iterating once the required number of middle elements have been summed.
- Return the sum divided by
m - 2*k.
- If the queue size is less than
Walkthrough
To avoid re-sorting m elements every time, we can use a data structure that maintains sorted order as elements are added and removed. A TreeMap is perfect for this, as it stores key-value pairs sorted by key. We can use the numbers as keys and their frequencies as values.
We still use a Queue to know which element is the oldest and needs to be removed from the window. When addElement(num) is called, we add num to the queue and increment its count in the TreeMap. When an element is removed from the queue, its count in the TreeMap is decremented.
When calculateMKAverage() is called, we no longer need to sort. We can iterate through the TreeMap, which provides the elements in sorted order. We traverse the map, keeping track of how many elements we've seen. We skip the first k, sum the next m - 2*k, and then stop. This traversal takes time proportional to the number of distinct elements in the window, which is at most m.
import java.util.LinkedList;import java.util.Queue;import java.util.TreeMap;import java.util.Map; class MKAverage { private int m, k; private Queue<Integer> window; private TreeMap<Integer, Integer> sortedCounts; public MKAverage(int m, int k) { this.m = m; this.k = k; this.window = new LinkedList<>(); this.sortedCounts = new TreeMap<>(); } public void addElement(int num) { window.offer(num); sortedCounts.put(num, sortedCounts.getOrDefault(num, 0) + 1); if (window.size() > m) { int oldNum = window.poll(); sortedCounts.put(oldNum, sortedCounts.get(oldNum) - 1); if (sortedCounts.get(oldNum) == 0) { sortedCounts.remove(oldNum); } } } public int calculateMKAverage() { if (window.size() < m) { return -1; } long sum = 0; int count = 0; int elementsToSkip = k; int elementsToSum = m - 2 * k; for (Map.Entry<Integer, Integer> entry : sortedCounts.entrySet()) { int num = entry.getKey(); int freq = entry.getValue(); if (elementsToSkip > 0) { int canSkip = Math.min(elementsToSkip, freq); elementsToSkip -= canSkip; freq -= canSkip; } if (freq > 0 && elementsToSum > 0) { int canSum = Math.min(elementsToSum, freq); sum += (long) num * canSum; elementsToSum -= canSum; } if (elementsToSum == 0) { break; } } return (int) (sum / (m - 2 * k)); }}Complexity
Time
`addElement`: O(log m) `TreeMap` operations (put, get, remove) take logarithmic time in the size of the map, which is at most `m`. `calculateMKAverage`: O(m) In the worst case, we might have to iterate through all `m` elements if they are all distinct.
Space
O(m) Space is required for the queue (`O(m)`) and the `TreeMap` (at most `m` distinct elements, so `O(m)`).
Trade-offs
Pros
More efficient than the brute-force approach.
addElementis fast, with a logarithmic time complexity.
Cons
The
calculateMKAveragemethod still requires iterating through the elements in the sorted map, leading to a time complexity ofO(m)in the worst case (when all elements are distinct).While better than brute force, it may still be too slow if
mis very large.
This optimal approach achieves O(1) time for calculateMKAverage by continuously maintaining the sum of the middle elements. It partitions the m elements in the sliding window into three groups using three TreeMaps: the k smallest (low), the m - 2k middle (mid), and the k largest (high). The addElement operation involves adding the new element, removing the old one, and then rebalancing the three partitions, which takes O(log m) time. The average calculation then becomes a simple lookup.
Algorithm
- Initialization: Maintain a
Queuefor the sliding window, and threeTreeMaps:lowfor the smallestkelements,midfor the middlem-2kelements, andhighfor the largestkelements. Also, keep track of the sizes of these partitions and the sum of elements inmid(midSum). addElement(num): This is the main logic.- An incoming element
numis added, and if the window is full, anoldNumis removed. - First, handle the removal of
oldNumfrom whichever partition (low,mid, orhigh) it resides in. Update the corresponding size andmidSumif it was inmid. - Next, add the new element
numto one of the partitions. A simple initial placement is to add it tolowand then rebalance, or to place it based on the current boundaries (low.lastKey()andhigh.firstKey()). - Rebalance: After the addition and removal, the partition sizes might be incorrect. Move elements between adjacent partitions (
low<->mid,mid<->high) to restore the target sizes (k,m-2k,k). UpdatemidSumwhenever an element moves into or out of themidpartition. This rebalancing involves a constant number of moves.
- An incoming element
calculateMKAverage():- If the queue size is less than
m, return-1. - Since
midSumis always kept up-to-date, simply return(int) (midSum / (m - 2 * k)). This is anO(1)operation.
- If the queue size is less than
Walkthrough
The key insight for the most optimal solution is to realize that the sum needed for the average can be maintained incrementally. We can split the m elements into three logical, sorted groups.
low: A sorted container for theksmallest elements.mid: A sorted container for them - 2kmiddle elements.high: A sorted container for theklargest elements.
We use TreeMaps to implement these sorted containers, storing element frequencies. We also maintain a long midSum to hold the sum of elements currently in the mid container.
When addElement(num) is called, we add num and remove the oldest element from the window. This triggers a rebalancing act. An element leaving the window is removed from its respective TreeMap. A new element is added. Then, we shuffle elements between the three TreeMaps to restore their required sizes (k, m-2k, k). For example, if low has more than k elements, its largest element is moved to mid. If mid has too many, its largest is moved to high. This process is reversed if a partition is too small. Crucially, midSum is updated with every element that moves into or out of the mid container.
With this intricate bookkeeping, the calculateMKAverage() method becomes trivial. It just returns the pre-calculated midSum divided by m - 2*k.
import java.util.LinkedList;import java.util.Queue;import java.util.TreeMap; class MKAverage { private int m, k; private Queue<Integer> q; private TreeMap<Integer, Integer> low, mid, high; private int lowSize, highSize; private long midSum; public MKAverage(int m, int k) { this.m = m; this.k = k; this.q = new LinkedList<>(); this.low = new TreeMap<>(); this.mid = new TreeMap<>(); this.high = new TreeMap<>(); this.lowSize = 0; this.highSize = 0; this.midSum = 0; } public void addElement(int num) { q.offer(num); int oldNum = -1; if (q.size() > m) { oldNum = q.poll(); } // Remove old element if window was full if (oldNum != -1) { if (low.containsKey(oldNum)) { remove(low, oldNum); lowSize--; } else if (mid.containsKey(oldNum)) { remove(mid, oldNum); midSum -= oldNum; } else { remove(high, oldNum); highSize--; } } // Add new element if (!low.isEmpty() && num <= low.lastKey()) { add(low, num); lowSize++; } else if (!high.isEmpty() && num >= high.firstKey()) { add(high, num); highSize++; } else { add(mid, num); midSum += num; } // Rebalance partitions while (lowSize > k) { int val = low.lastKey(); remove(low, val); lowSize--; add(mid, val); midSum += val; } while (highSize > k) { int val = high.firstKey(); remove(high, val); highSize--; add(mid, val); midSum += val; } while (lowSize < k && !mid.isEmpty()) { int val = mid.firstKey(); remove(mid, val); midSum -= val; add(low, val); lowSize++; } while (highSize < k && !mid.isEmpty()) { int val = mid.lastKey(); remove(mid, val); midSum -= val; add(high, val); highSize++; } } public int calculateMKAverage() { if (q.size() < m) { return -1; } return (int) (midSum / (m - 2 * k)); } private void add(TreeMap<Integer, Integer> map, int val) { map.put(val, map.getOrDefault(val, 0) + 1); } private boolean remove(TreeMap<Integer, Integer> map, int val) { if (!map.containsKey(val)) return false; map.put(val, map.get(val) - 1); if (map.get(val) == 0) { map.remove(val); } return true; }}Complexity
Time
`addElement`: O(log m) Each addition/removal involves a few `TreeMap` operations, each taking `O(log m)` or `O(log k)` time. The rebalancing moves a constant number of elements. `calculateMKAverage`: O(1) The result is pre-computed and stored in `midSum`, so retrieval is constant time.
Space
O(m) Space is needed for the queue (`O(m)`) and the three `TreeMap`s, which together store `m` elements.
Trade-offs
Pros
Extremely fast
calculateMKAveragecalls, which areO(1).The most efficient solution for the given problem constraints, especially when average calculations are frequent.
Cons
The implementation is significantly more complex than the other approaches.
The rebalancing logic requires careful handling of many edge cases to ensure correctness.
Solutions
Solution
class MKAverage { private int m , k ; private long s ; private int size1 , size3 ; private Deque < Integer > q = new ArrayDeque <>(); private TreeMap < Integer , Integer > lo = new TreeMap <>(); private TreeMap < Integer , Integer > mid = new TreeMap <>(); private TreeMap < Integer , Integer > hi = new TreeMap <>(); public MKAverage ( int m , int k ) { this . m = m ; this . k = k ; } public void addElement ( int num ) { if ( lo . isEmpty () || num <= lo . lastKey ()) { lo . merge ( num , 1 , Integer: : sum ); ++ size1 ; } else if ( hi . isEmpty () || num >= hi . firstKey ()) { hi . merge ( num , 1 , Integer: : sum ); ++ size3 ; } else { mid . merge ( num , 1 , Integer: : sum ); s += num ; } q . offer ( num ); if ( q . size () > m ) { int x = q . poll (); if ( lo . containsKey ( x )) { if ( lo . merge ( x , - 1 , Integer: : sum ) == 0 ) { lo . remove ( x ); } -- size1 ; } else if ( hi . containsKey ( x )) { if ( hi . merge ( x , - 1 , Integer: : sum ) == 0 ) { hi . remove ( x ); } -- size3 ; } else { if ( mid . merge ( x , - 1 , Integer: : sum ) == 0 ) { mid . remove ( x ); } s -= x ; } } for (; size1 > k ; -- size1 ) { int x = lo . lastKey (); if ( lo . merge ( x , - 1 , Integer: : sum ) == 0 ) { lo . remove ( x ); } mid . merge ( x , 1 , Integer: : sum ); s += x ; } for (; size3 > k ; -- size3 ) { int x = hi . firstKey (); if ( hi . merge ( x , - 1 , Integer: : sum ) == 0 ) { hi . remove ( x ); } mid . merge ( x , 1 , Integer: : sum ); s += x ; } for (; size1 < k && ! mid . isEmpty (); ++ size1 ) { int x = mid . firstKey (); if ( mid . merge ( x , - 1 , Integer: : sum ) == 0 ) { mid . remove ( x ); } s -= x ; lo . merge ( x , 1 , Integer: : sum ); } for (; size3 < k && ! mid . isEmpty (); ++ size3 ) { int x = mid . lastKey (); if ( mid . merge ( x , - 1 , Integer: : sum ) == 0 ) { mid . remove ( x ); } s -= x ; hi . merge ( x , 1 , Integer: : sum ); } } public int calculateMKAverage () { return q . size () < m ? - 1 : ( int ) ( s / ( q . size () - k * 2 )); } } /** * Your MKAverage object will be instantiated and called as such: * MKAverage obj = new MKAverage(m, k); * obj.addElement(num); * int param_2 = obj.calculateMKAverage(); */class MKAverage { public: MKAverage ( int m , int k ) { this -> m = m ; this -> k = k ; } void addElement ( int num ) { if ( lo . empty () || num <= * lo . rbegin ()) { lo . insert ( num ); } else if ( hi . empty () || num >= * hi . begin ()) { hi . insert ( num ); } else { mid . insert ( num ); s += num ; } q . push ( num ); if ( q . size () > m ) { int x = q . front (); q . pop (); if ( lo . find ( x ) != lo . end ()) { lo . erase ( lo . find ( x )); } else if ( hi . find ( x ) != hi . end ()) { hi . erase ( hi . find ( x )); } else { mid . erase ( mid . find ( x )); s -= x ; } } while ( lo . size () > k ) { int x = * lo . rbegin (); lo . erase ( prev ( lo . end ())); mid . insert ( x ); s += x ; } while ( hi . size () > k ) { int x = * hi . begin (); hi . erase ( hi . begin ()); mid . insert ( x ); s += x ; } while ( lo . size () < k && mid . size ()) { int x = * mid . begin (); mid . erase ( mid . begin ()); s -= x ; lo . insert ( x ); } while ( hi . size () < k && mid . size ()) { int x = * mid . rbegin (); mid . erase ( prev ( mid . end ())); s -= x ; hi . insert ( x ); } } int calculateMKAverage () { return q . size () < m ? - 1 : s / ( q . size () - k * 2 ); } private: int m , k ; long long s = 0 ; queue < int > q ; multiset < int > lo , mid , hi ; }; /** * Your MKAverage object will be instantiated and called as such: * MKAverage* obj = new MKAverage(m, k); * obj->addElement(num); * int param_2 = obj->calculateMKAverage(); */from sortedcontainers import SortedList class MKAverage : def __init__ ( self , m : int , k : int ): self . m = m self . k = k self . s = 0 self . q = deque () self . lo = SortedList () self . mid = SortedList () self . hi = SortedList () def addElement ( self , num : int ) -> None : if not self . lo or num <= self . lo [ - 1 ]: self . lo . add ( num ) elif not self . hi or num >= self . hi [ 0 ]: self . hi . add ( num ) else : self . mid . add ( num ) self . s += num self . q . append ( num ) if len ( self . q ) > self . m : x = self . q . popleft () if x in self . lo : self . lo . remove ( x ) elif x in self . hi : self . hi . remove ( x ) else : self . mid . remove ( x ) self . s -= x while len ( self . lo ) > self . k : x = self . lo . pop () self . mid . add ( x ) self . s += x while len ( self . hi ) > self . k : x = self . hi . pop ( 0 ) self . mid . add ( x ) self . s += x while len ( self . lo ) < self . k and self . mid : x = self . mid . pop ( 0 ) self . lo . add ( x ) self . s -= x while len ( self . hi ) < self . k and self . mid : x = self . mid . pop () self . hi . add ( x ) self . s -= x def calculateMKAverage ( self ) -> int : return - 1 if len ( self . q ) < self . m else self . s // ( self . m - 2 * self . k ) # Your MKAverage object will be instantiated and called as such: # obj = MKAverage(m, k) # obj.addElement(num) # param_2 = obj.calculateMKAverage()Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.