Problem overview
Numbers arrive one at a time, and after each one the class KthLargest must report the k-th largest of everything seen so far. Equal numbers count separately: among 8, 7, 7, 5 the 2nd largest is 7, and so is the 3rd. The constructor receives k and the numbers that have already arrived, and add(val) takes a new number and returns the current k-th largest. The constraints promise that at least k numbers have arrived whenever add returns, so the answer always exists.
In the first example, k = 3 and the starting numbers are [4, 5, 8, 2]. After add(3) the numbers are 2, 3, 4, 5, 8; the three largest are 8, 5 and 4, so the answer is 4. The five calls add(3), add(5), add(10), add(9), add(4) return 4 5 5 8 8.
The statement asks for O(log k) time per add and O(k) memory.
Approach: keep only the k largest, in a min-heap
The simple solution stores every number and sorts them again after each add. That costs O(n log n) per call, where n is how many numbers have arrived, and it keeps all of them in memory.
The better idea starts from an observation: a number that is not among the k largest right now can never become the answer. Numbers only arrive, so the k-th largest can only stay the same or go up, and a smaller number only falls further behind. So the class needs to keep just k numbers.
Which structure keeps "the k largest" and gives the smallest of them at once? A min-heap. The smallest of the k largest values is exactly the k-th largest, and a min-heap has its smallest value on top. When a new number arrives, push it. If the heap now holds k + 1 values, the smallest of them cannot be among the k largest, so remove the top. Both the push and the pop cost O(log k), because the heap never holds more than k + 1 values.
Here is the heap for k = 3 after the constructor receives [4, 5, 8, 2], and then after the first three calls (the values kept, smallest first):
after the constructor: 4 5 8
after add(3): 4 5 8
after add(5): 5 5 8
after add(10): 5 8 10
The 2 never stays in the heap, and 3 is pushed and immediately removed again because it is the smallest of four. The top of the heap, the first number in each row, is the answer: 4, 5, 5.
Step by step
- Declare the field
k.addneeds it to know when the heap has grown too big. - Declare
largestas apriority_queue<int, vector<int>, greater<int>>. By default apriority_queueis a max-heap. The third template argument,greater<int>, reverses the order so that the smallest value is on top. - In the constructor, store the parameter with
this->k = k. The parameter and the field have the same name, andthis->picks the field. - Loop over
numsand calladd(num)for each value. The starting numbers go through the same filter as every later one, so the constructor does not repeat the "keep only k" logic. The returned value is ignored; nobody has asked yet. - In
add, pushvalinto the heap. - If the heap holds more than
kvalues, pop. The smallest ofk + 1values is out of the top k for good. - Return
largest.top(). The heap holds exactly k values at this point, and its smallest is the k-th largest.
The solution in C++
class KthLargest {
public:
int k;
priority_queue<int, vector<int>, greater<int>> largest;
KthLargest(int k, vector<int>& nums) {
this->k = k;
for (int num : nums) {
add(num);
}
}
int add(int val) {
largest.push(val);
if (largest.size() > k) {
largest.pop();
}
return largest.top();
}
};
The C++ details worth a closer look:
- The three template arguments.
priority_queue<T, Container, Compare>stores values of typeTin aContainer(avectorhere) and orders them withCompare. The default comparison puts the largest value on top. Passinggreater<int>puts the smallest on top, and to name the comparison you must also name the container in the middle. pop()returns nothing. It only removes the top. That is fine here, because the removed value is thrown away.top()reads without removing.top()on an empty heap is undefined behaviour.addalways pushes before it callstop(), so the heap is never empty at that point, including when the constructor callsaddwith fewer thanknumbers.- Signed against unsigned.
largest.size()is unsigned, andkis anint. The comparison works becausekis at least 1, so converting it to unsigned changes nothing. - The constructor takes
vector<int>&. The vector is passed by reference, so a list of up to 100,000 numbers is not copied.
A run on the first example (k = 3, starting numbers [4, 5, 8, 2]) prints 4 5 5 8 8 for the five calls. After the constructor, the heap holds 3 values and its top is 4.
Complexity
Time: O(log k) per add. One push and at most one pop on a heap of at most k + 1 values, each O(log k), plus a constant-time top(). The constructor makes one add call per starting number, so building the object with m numbers costs O(m log k).
Space: O(k). The heap never holds more than k + 1 values, however many numbers have arrived. Sorting everything keeps all n.
Edge cases and common mistakes
- Starting with an empty list. With
k = 1and no numbers, the firstadd(-3)returns -3, and the callsadd(-3),add(-2),add(-4),add(0),add(4)return-3 -2 -2 0 4. - Duplicates. With
k = 4and starting numbers[7, 7, 7, 7, 8, 3], the heap holds 7, 7, 7, 8 and the callsadd(2),add(10),add(9),add(9)return7 7 7 8. Do not remove duplicates: equal values count separately. - A max-heap by mistake. With the default
priority_queue<int>, the top is the largest value, not the k-th. Thegreater<int>argument is what makes the top the one to compare against. - Forgetting the filter in the constructor. Pushing the starting numbers without popping would leave the heap with more than k values, and its top would be too small.
- Popping when the size equals
k. The condition is "more thank". Popping at exactlykwould drop a value that belongs to the top k. - Returning the largest. The answer is the smallest of the kept values, the top of a min-heap. With
k = 3, afteradd(10)the heap holds 5, 8, 10 and the answer is 5, not 10.
Testing
k = 3, numbers[4, 5, 8, 2], then add 3, 5, 10, 9, 4 gives4 5 5 8 8.k = 1, numbers[], then add -3, -2, -4, 0, 4 gives-3 -2 -2 0 4.k = 4, numbers[7, 7, 7, 7, 8, 3], then add 2, 10, 9, 9 gives7 7 7 8.k = 1, numbers[5], then add 5, 1, 6 gives5 5 6.
