Problem overview
Numbers arrive one at a time, and after each one the class must report the k-th largest of everything seen so far. Equal numbers count separately: in [8, 7, 7, 5] the 2nd largest and the 3rd largest are both 7.
The lesson asks for a class KthLargest with a constructor constructor(k: number, nums: number[]) that receives the numbers that have arrived already, and a method add(val: number): number that takes in a new number and returns the current k-th largest. The lesson provides a ready-to-use MinHeap class with insert, extract, peek, size and isEmpty.
Take k = 3 and starting numbers [4, 5, 8, 2]. After add(3) the numbers are 2, 3, 4, 5, 8, so the three largest are 8, 5 and 4 and the answer is 4. Running the five calls add(3), add(5), add(10), add(9), add(4) on that object returns:
4 5 5 8 8
The last call, add(4), changes nothing at the top: 4 is smaller than the current 3rd largest (8), so it can't enter the top three.
Approach: keep only the k largest
The simple approach stores every number, sorts them on each add and reads the element at position k from the top. Sorting costs O(n log n) per call, where n is how many numbers have arrived, and the cost grows with the stream.
The better idea starts from an observation: a number that is below the k-th largest can never matter again. Numbers only arrive, never leave, so the k-th largest can only stay the same or go up. Only the k largest numbers are worth keeping.
Keep those k numbers in a min-heap. A min-heap gives the smallest of its values in O(1) and inserts or removes in O(log size). The smallest of the k largest numbers is exactly the k-th largest, so it sits on top, and the answer to every add is one peek().
To add a value: insert it, and if the heap now holds more than k values, extract the top. The smallest of k + 1 values is not among the k largest, so dropping it is always right. The heap never holds more than k + 1 values, so each add costs O(log k), and the object stores O(k) values, however long the stream gets.
Step by step
- Declare the field
kas anumber:addneeds it later, and a constructor parameter disappears when the constructor ends. - Declare
largestas aMinHeap, set tonew MinHeap(). It will hold the k largest values seen so far. - In the constructor, store
kinthis.k. - Loop over
numswithfor (const num of nums). The starting numbers count like any other number. - Call
this.add(num)inside the loop.addalready keeps the heap at k values, so the constructor reuses it. The returned value is ignored, because nobody has asked yet. - In
add, insertvalintothis.largest. - Trim the heap: if
this.largest.size()is greater thanthis.k, callextract()to drop the smallest value. - Return
this.largest.peek(), the smallest of the k largest values.
The solution in TypeScript
class KthLargest {
k: number;
largest: MinHeap = new MinHeap();
constructor(k: number, nums: number[]) {
this.k = k;
for (const num of nums) {
this.add(num);
}
}
add(val: number): number {
this.largest.insert(val);
if (this.largest.size() > this.k) {
this.largest.extract();
}
return this.largest.peek();
}
}
A few details:
- The fields are declared with types, and
largestis initialized right in the declaration, so everyKthLargestobject gets its own heap. The types are checked when the file is compiled and erased afterwards; they do nothing at run time. this.add(num)in the constructor is an ordinary method call: by that pointthis.kandthis.largestalready exist.peek()reads the top and leaves it in the heap.extract()removes the top and returns it, and the code above discards what it returns.- With the starting numbers
[4, 5, 8, 2]andk = 3, the constructor ends with a heap of size 3 and the value 4 on top.
Complexity
Each add does one insert and at most one extract on a heap of at most k + 1 values, so it takes O(log k) time. The constructor calls add once per starting number, so it takes O(m log k) for m starting numbers. The object keeps at most k values: O(k) space.
Compare that with sorting on every call: O(n log n) per add and O(n) space, both growing with the stream.
Edge cases and common mistakes
- Duplicates count separately. With
k = 4and starting numbers[7, 7, 7, 7, 8, 3], the callsadd(2),add(10),add(9),add(9)return7 7 7 8. Removing duplicates would give wrong answers. - Starting with an empty array.
new KthLargest(1, [])followed byadd(-3),add(-2),add(-4),add(0),add(4)returns-3 -2 -2 0 4. The heap simply starts empty. - Fewer than k values so far. The lesson promises that at least k values exist when
addreturns. Inside the constructor, the heap may hold fewer than k values, and that is fine: the check only extracts when the size goes above k. Fornew KthLargest(2, [5]),add(1)returns 1, the 2nd largest of 5 and 1. - Using a max-heap by mistake. A max-heap would put the largest value on top, which is not what
addmust return. The top of a min-heap of the k largest is the k-th largest. - Sorting with no comparator. If you try the sorting approach on numbers, remember that
sort()with no comparator sorts as strings:[10, 9, 1].sort()gives[1, 10, 9]. Pass(a, b) => a - b.
