Start learning free

LESSON 402 OF 573 · HEAP

Kth Largest Element in a Stream in Kotlin: O(log k) with a Min-Heap

Practice O(log k) Read the full explanation

Numbers arrive one at a time, like the scores of a game coming in live, and after each one you must say which number is in k-th place: the k-th largest of all the numbers so far. Equal numbers count separately: in [8, 7, 7, 5] the 2nd largest is 7, and so is the 3rd.

Implement the class KthLargest:

  • KthLargest(k, nums) creates the object with the integer k and the numbers nums that have arrived already.
  • add(val) adds the number val and returns the k-th largest of all the numbers so far.

The input describes one new KthLargest and a list of add calls, applied in order. The output lists what the add calls return, in order. The starter code spells these names the way your language does.

 

Example 1:

Input:
    KthLargest(3, [4, 5, 8, 2])
    add(3)
    add(5)
    add(10)
    add(9)
    add(4)
Output: 4 5 5 8 8
Explanation:
After add(3) the numbers are [2, 3, 4, 5, 8]: the three largest are 8, 5 and 4, so the 3rd largest is 4.
After add(5): 8, 5, 5 are the three largest, so the answer is 5.
After add(10): 10, 8, 5, answer 5. After add(9): 10, 9, 8, answer 8.
add(4) changes nothing at the top: 4 is smaller than the 3rd largest, so the answer stays 8.

Example 2:

Input:
    KthLargest(1, [])
    add(-3)
    add(-2)
    add(-4)
    add(0)
    add(4)
Output: -3 -2 -2 0 4
Explanation: With k = 1, add returns the largest number so far. No numbers have arrived when the object is created, so after the first add, -3 is the only one, and the largest.

Example 3:

Input:
    KthLargest(4, [7, 7, 7, 7, 8, 3])
    add(2)
    add(10)
    add(9)
    add(9)
Output: 7 7 7 8
Explanation: Equal numbers count separately: in [8, 7, 7, 7, 7, 3, 2] the 4th largest is the third 7. After 10 and 9 arrive the four largest are 10, 9, 8, 7. The second 9 pushes the last 7 out: 10, 9, 9, 8, so the answer becomes 8.

 

Constraints:

  • 1 <= k <= 105
  • 0 <= nums.length <= 105
  • -104 <= nums[i], val <= 104
  • At most 105 calls are made to add.
  • When add returns, at least k numbers have arrived, so the k-th largest always exists.

Note:

Each add should run in O(log k) time, and the object should keep only O(k) values. Sorting all the numbers again on every add would take O(n log n) time per call, where n is how many numbers have arrived so far.

STEP 1 OF 6
    Ctrl + Enter to check

    KOTLIN · HEAP

    Kth Largest Element In A Stream in Kotlin, explained

    Problem overview

    You write a class KthLargest. Its constructor receives k and the numbers nums that have already arrived. Each call to add(value) adds one more number and returns the k-th largest of everything seen so far. Equal numbers count separately: in [8, 7, 7, 5] the 2nd largest is 7, and so is the 3rd.

    Take k = 3 and the starting numbers 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 3rd largest is 4. The five calls add(3), add(5), add(10), add(9), add(4) return 4, 5, 5, 8, 8.

    Approach

    The simple solution keeps every number in a list. On each add it appends the value, sorts the list in descending order and reads position k - 1. That works, but a sort costs O(n log n), where n is how many numbers have arrived, and it runs again on every call. The list also grows without limit.

    The better idea starts from a question: which numbers can ever be the answer? New numbers only push the k-th largest up, never down. A number that is not among the k largest now will stay outside them forever, so it can be forgotten. It is enough to remember the k largest numbers.

    Among those k numbers, the one you need is the smallest: the k-th largest is the smallest of the top k. A min-heap gives the smallest value in O(1) and lets you add or remove one in O(log size). So the class keeps a min-heap of at most k values:

    • add the new value to the heap;
    • if the heap now holds k + 1 values, remove the smallest, since it cannot be among the k largest;
    • the top of the heap is the answer.

    The heap never holds more than k + 1 values, so each add takes O(log k) time and the object uses O(k) space.

    Step by step

    1. Declare the heap. A val largest holds a PriorityQueue<Int>(). In Kotlin this is Java's priority queue, and with no comparator it is a min-heap: the smallest value is on top, which is the order the idea needs.
    2. Feed the starting numbers. The numbers in nums arrived before any call, but they count like any other. An init block loops over nums. It runs when the object is created, after the properties above it, so largest already exists.
    3. Reuse add. The loop calls add(num) and ignores the result. add already keeps the heap at k values, so the constructor does not repeat that logic.
    4. Insert the new value into largest.
    5. Trim to k. If largest.size is above k, call poll(), which removes and returns the smallest value.
    6. Return the top with peek(), which reads the smallest value without removing it. The constraints promise at least k numbers when add returns, so the heap holds exactly k and its top is the k-th largest.

    The solution in Kotlin

    class KthLargest(private val k: Int, nums: IntArray) {
        val largest = PriorityQueue<Int>()
    
        init {
            for (num in nums) {
                add(num)
            }
        }
    
        fun add(value: Int): Int {
            largest.add(value)
            if (largest.size > k) {
                largest.poll()
            }
            return largest.peek()
        }
    }
    

    Some Kotlin details that matter here:

    • k is a property, nums is not. The header says private val k, so k stays available in add after the constructor ends. nums has no val, so it is a plain parameter, usable only in the property initializers and init blocks.
    • largest.add is not the class's add. Inside the class, add(num) calls the method being written, and largest.add(value) calls the heap's method. The dot decides.
    • poll() versus peek(). Both look at the smallest value; poll() also removes it. Both come from Java's PriorityQueue.
    • IntArray versus List. The constructor takes an IntArray, which is iterated with for (num in nums) like any collection.

    Seeing it run

    This code creates the object, adds values and prints the heap's contents in sorted order next to each answer:

    fun main() {
        val kth = KthLargest(3, intArrayOf(4, 5, 8, 2))
        println(kth.largest.sorted())
        for (v in intArrayOf(3, 5, 10, 9, 4)) {
            println("add($v) = ${kth.add(v)}, heap = ${kth.largest.sorted()}")
        }
        val single = KthLargest(1, intArrayOf())
        println(listOf(-3, -2, -4, 0, 4).map { single.add(it) })
        val ties = KthLargest(4, intArrayOf(7, 7, 7, 7, 8, 3))
        println(ties.largest.sorted())
        println(listOf(2, 10, 9, 9).map { ties.add(it) })
    }
    

    It prints:

    [4, 5, 8]
    add(3) = 4, heap = [4, 5, 8]
    add(5) = 5, heap = [5, 5, 8]
    add(10) = 5, heap = [5, 8, 10]
    add(9) = 8, heap = [8, 9, 10]
    add(4) = 8, heap = [8, 9, 10]
    [-3, -2, -2, 0, 4]
    [7, 7, 7, 8]
    [7, 7, 7, 8]
    

    The heap holds the same three values as the list of the three largest numbers, and its smallest entry is always the answer. With k = 1 the heap holds one value, so every call returns the largest number so far. In the last example the 3 in the starting numbers was dropped as soon as the object was built, because four larger values were already there.

    Complexity

    • Time per add: O(log k). The heap has at most k + 1 values, so one insertion and at most one removal cost O(log k) each.
    • Constructor: n starting numbers, each added in O(log k), so O(n log k).
    • Space: O(k), because only the k largest values are kept.

    Re-sorting everything costs O(n log n) per call and O(n) space, which is why the heap is the better design as the stream grows.

    Edge cases and common mistakes

    • Duplicates. Do not turn the heap into a set. With k = 4 and the numbers 7, 7, 7, 7, 8, 3, the answer is 7 because four separate 7s can sit in the top four with the 8 above them.
    • Keeping a max-heap of everything. It would give the largest value quickly, not the k-th, and it would grow with the stream.
    • Removing before adding. If you poll first and insert after, you throw away the smallest of the old top k even when the new value is smaller still, and the answer can be wrong. Insert first, then trim: the smallest of k + 1 values is the right one to drop.
    • Trimming at the wrong size. The heap must be cut back when it holds more than k values. Cutting at another size leaves it one value off, and peek() then returns the wrong rank.
    • Few starting numbers. The heap may hold fewer than k values after the constructor, for example KthLargest(1, intArrayOf()). That is fine, because the constraints promise at least k values by the time add returns.
    Keep going with Kotlin500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free