Problem overview
Numbers arrive one at a time, and after each one the class must report the k-th largest number seen so far. Equal numbers count separately: in [8, 7, 7, 5] the 2nd largest is 7 and so is the 3rd. The class is KthLargest. Its initializer takes k and the numbers that have already arrived, and add(_:) takes one new number and returns the current k-th largest.
Take KthLargest(3, [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) and add(4) return 4, 5, 5, 8, 8. The last call returns 8 because 4 is smaller than the current 3rd largest and changes nothing at the top.
The statement asks for O(log k) time per add and O(k) stored values, so the class can't keep everything and sort it again.
Approach: keep only the k largest
The simple way is to store every number and, on each add, sort them and read the k-th from the end. That costs O(n log n) per call, where n is how many numbers have arrived, and the storage grows without limit. With 100,000 starting numbers and 100,000 calls, that is far too slow.
The way out is to notice which numbers can still matter. Values only ever arrive, never leave, so the k-th largest can only stay the same or go up. A number that is below the current k-th largest will never be the answer again, and it can be forgotten. Only the k largest numbers need to be kept.
Among those k numbers, the one we need is the smallest, because the k-th largest is the smallest of the top k. That calls for a min-heap: a structure that keeps its smallest value on top, reads it in O(1), and inserts or removes in O(log size). With a heap of size k, those operations cost O(log k).
So each add does three things. It pushes the new value. If the heap now holds k + 1 values, its smallest cannot be among the k largest, so it is removed. Then the top is the answer. In the first example the heap holds exactly three values after the initializer and after every call, and the tops the calls return are 4, 5, 5, 8, 8.
Step by step
- Declare
kas aletproperty of typeInt, sinceaddneeds it later and it never changes. - Declare
largestas avarset toMinHeap(). The lesson providesMinHeapwithinsert,extract()(removes and returns the top),peek()(reads the top),countandisEmpty. It must be avarbecause it is a struct whose methods change it. - Store the parameter with
self.k = k. The parameter and the property share a name, andself.kis the property. - Loop over
nums: the starting numbers count like any later number, so they go through the same filter. - Call
add(num)and ignore its answer with_ =. This reuses the code that keeps the heap at k values, and nobody has asked a question yet. - In
add, insertvalintolargest. - If
largest.countis more thank, callextract(). The smallest of k + 1 values isn't among the k largest now, and since values only arrive, it won't be later. - Return
largest.peek(): the smallest of the k largest values is the k-th largest.
The solution in Swift
class KthLargest {
let k: Int
var largest = MinHeap()
init(_ k: Int, _ nums: [Int]) {
self.k = k
for num in nums {
_ = add(num)
}
}
func add(_ val: Int) -> Int {
largest.insert(val)
if largest.count > k {
largest.extract()
}
return largest.peek()
}
}
The Swift details worth a closer look:
- Calling a method in
init. The initializer callsadd(num), which is a method of the object being built. Swift allows this only after every stored property has a value. Herekis assigned on the line before the loop, andlargesthas its default, so the call is legal. Move the loop aboveself.k = kand the compiler reports thatselfis used in the method calladdbefore all stored properties are initialized. letversusvar.kis aletand can be set once, in the initializer.largestis avarbecause inserting changes the heap.- Structs and methods that change them.
MinHeapis a struct, a value type. Its mutating methods work onlargestin place becauselargestis a storedvarof a class, andaddcan change class properties without being markedmutating. _ = add(num).addreturns anInt. Assigning to_says the result is thrown away on purpose, so there is no unused-value warning.- The unused result of
extract(). The linelargest.extract()discards the removed value; only its removal matters.
Complexity
Time. Each add does one insert and at most one extract on a heap that never holds more than k + 1 values, so both cost O(log k). The initializer calls add once per starting number, so it takes O(m log k) for m numbers. Reading the top is O(1).
Space. O(k). The heap holds at most k values after each call, however many numbers have arrived. Storing and sorting all the numbers would take O(n) space and O(n log n) per call.
Edge cases and common mistakes
- Starting with an empty array.
KthLargest(1, [])works: the heap is empty, and the firstadd(-3)makes it[-3]. The adds -3, -2, -4, 0, 4 return -3, -2, -2, 0, 4, the running maximum. - Fewer than k numbers at the start. The lesson guarantees at least k numbers have arrived whenever
addreturns, so the heap holds exactly k by then. Without that guarantee,peek()would return the smallest value so far, which is not a k-th largest. - Duplicates. Equal values are separate entries.
KthLargest(4, [7, 7, 7, 7, 8, 3])followed byadd(2),add(10),add(9)andadd(9)returns 7, 7, 7, 8. Removing duplicates would give a wrong answer. - Using a max-heap. A max-heap holds the largest value on top, which is useless here. The answer sits at the bottom of the top k, so the heap must put the smallest first.
- Keeping everything. Pushing every value and never extracting stores all the numbers and gives the wrong top.
- A value smaller than everything in the heap. It is inserted and then immediately extracted, since it is the smallest of the k + 1. With
KthLargest(3, [4, 5, 8, 2]), callingadd(1)returns 4 and the heap still holds three values.
Testing
KthLargest(3, [4, 5, 8, 2])withadd(3),add(5),add(10),add(9),add(4)gives 4, 5, 5, 8, 8.KthLargest(1, [])withadd(-3),add(-2),add(-4),add(0),add(4)gives -3, -2, -2, 0, 4.KthLargest(4, [7, 7, 7, 7, 8, 3])withadd(2),add(10),add(9),add(9)gives 7, 7, 7, 8.KthLargest(2, [8, 7, 7, 5])withadd(1)gives 7.
