Problem overview
Numbers arrive one at a time, and after each one you must report the k-th largest of everything seen so far. The class KthLargest has a constructor KthLargest(int k, int[] nums), which receives the numbers that arrived before the first question, and a method add(int val), which takes a new number and returns the current k-th largest. Equal numbers count separately: among 8, 7, 7, 5 the 2nd largest is 7, and so is the 3rd.
Take k = 3 and nums = [4, 5, 8, 2]. The five calls add(3), add(5), add(10), add(9) and add(4) return the values below. After add(3) the numbers are 2, 3, 4, 5, 8, whose three largest are 8, 5 and 4, so the answer is 4.
4 5 5 8 8
The constraints guarantee that at least k numbers have arrived whenever add returns, so the k-th largest always exists.
Approach: keep only the k largest
The simple approach stores every number in a list and sorts it on each add, then 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 with up to 100,000 numbers and 100,000 calls that is far too slow.
The key observation is that most numbers can be forgotten. Values only ever arrive; none leave. So a number that is not among the k largest now can never become the k-th largest later, because the k numbers above it stay above it. Only the k largest values matter.
Which structure keeps the k largest values and tells you the smallest of them quickly? A min-heap. Its top is the smallest value it holds, and since it holds the k largest values, that smallest one is exactly the k-th largest. Java's PriorityQueue is a min-heap when you give it no comparator, so new PriorityQueue<>() is what the lesson needs.
Each add then does two things. It pushes the new value, and if the heap now holds k + 1 values, it removes the smallest. The removed value cannot be among the k largest, now or later. The heap never holds more than k + 1 values, so a push and a pop each cost O(log k), and reading the top is O(1).
Step by step
- Declare the field
k.addneeds it long after the constructor has finished, so it is a field, not just a parameter. - Declare the heap
largestas aPriorityQueue<Integer>that starts empty. - Store the parameter in the field with
this.k = k. The parameter and the field share a name, andthis.knames the field. - Loop over
nums. The starting numbers count like any later one, so they must pass the same filter: kept only while they are among the k largest. - Call
add(num)in the loop. It already keeps the heap at size k, so the constructor reuses it instead of repeating that logic. The returned value is ignored, because nobody has asked a question yet. - In
add, putvalin the heap. - If the heap holds more than
kvalues, callpoll(). It removes the smallest, which cannot be one of the k largest. - Return
peek(). It reads the top without removing it. With exactly k values in the heap, the top is the k-th largest.
The solution in Java
class KthLargest {
int k;
PriorityQueue<Integer> largest = new PriorityQueue<>();
public KthLargest(int k, int[] nums) {
this.k = k;
for (int num : nums) {
add(num);
}
}
public int add(int val) {
largest.add(val);
if (largest.size() > k) {
largest.poll();
}
return largest.peek();
}
}
A few details about the Java code:
- Boxing. A
PriorityQueue<Integer>holdsIntegerobjects, notint.largest.add(val)boxes theint, andreturn largest.peek()unboxes it back to theintthataddpromises. addversuspollandpeek.addinserts,poll()removes and returns the smallest, andpeek()returns the smallest without removing it. On an empty heap,poll()andpeek()returnnull, and unboxing thatnullinto anintwould throw aNullPointerException. The constraints rule that case out.- Calling a method from the constructor. The constructor calls
add, a method of the same class. That works because the fieldskandlargestare set before the loop runs:largestis initialised in its declaration, andthis.k = kcomes first in the constructor. - The heap's order is not sorted order. Only the top is guaranteed to be the smallest. Printing a
PriorityQueueor iterating over it shows the internal array, not a sorted list. Use onlypeek()andpoll()to read it.
Seeing it work
Create the object and call add the way the lesson's tests do. After the constructor has processed [4, 5, 8, 2] with k = 3, the heap holds three values: 2 was pushed out, and the top is 4. Each add then returns the heap's top.
KthLargest kl = new KthLargest(3, new int[]{4, 5, 8, 2});
for (int v : new int[]{3, 5, 10, 9, 4}) {
System.out.print(kl.add(v) + " ");
}
4 5 5 8 8
With k = 1 the heap holds a single value, the largest so far. Starting from an empty array, the five adds of -3, -2, -4, 0, 4 give:
-3 -2 -2 0 4
Duplicates matter. With k = 4 and starting numbers [7, 7, 7, 7, 8, 3], the adds of 2, 10, 9, 9 give:
7 7 7 8
The first add returns 7, because the four largest of 8, 7, 7, 7, 7, 3, 2 are 8 and three 7s. After 10 and 9 arrive the four largest are 10, 9, 8, 7. The second 9 pushes the last 7 out, leaving 10, 9, 9, 8, so the answer becomes 8.
Complexity
Time per add: O(log k). The heap holds at most k + 1 values. A push and a pop each move a value up or down a tree of height about log k, and peek() is constant time. The constructor makes one add per starting number, so it takes O(m log k) for m starting numbers.
Space: O(k). The object keeps only the k largest values, however many numbers have arrived. Re-sorting everything, by comparison, takes O(n log n) time per call and O(n) space.
Edge cases and common mistakes
- Using a max-heap. A max-heap would put the largest value on top, which is not the answer. The k-th largest is the smallest of the k largest, so the min-heap is the right order. In Java the default
PriorityQueueis already a min-heap, so no comparator is needed. - Removing the wrong end. After the push, the value to drop is the smallest of
k + 1. Dropping the largest would throw away exactly the values that matter. - Forgetting the starting numbers. Skipping the loop in the constructor leaves the heap empty, and the first
addthen answers from too few values. - Using
>=instead of>. The condition issize() > k. With>=the heap would hold onlyk - 1values and its top would be the (k-1)-th largest. - Treating equal numbers as one. Each copy takes its own place in the heap. In the third run above, the three 7s each count.
k = 1. The heap holds one value, and every add keeps the larger of the old value and the new one.
