Start learning free

LESSON 402 OF 573 · HEAP

Kth Largest Element in a Stream in Scala: A Min-Heap of Size k

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

    SCALA · HEAP

    Kth Largest Element In A Stream in Scala, explained

    Problem overview

    Numbers arrive one at a time, and after each one you must report 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.

    The lesson asks for a class KthLargest(k: Int, nums: Array[Int]) with one method, add(value: Int): Int. The constructor receives the numbers that arrived before any call, and each add stores a new number and returns the current k-th largest. The statement promises that at least k numbers have arrived whenever add returns.

    Example: KthLargest(3, Array(4, 5, 8, 2)) followed by add(3), add(5), add(10), add(9) and add(4) returns 4, 5, 5, 8 and 8. After add(3) the numbers are 2, 3, 4, 5, 8, the three largest are 8, 5 and 4, and the 3rd largest is 4.

    Approach

    The simple way and its cost

    Keep every number in a buffer, sort it on each add, and read the element k places from the top. It works, but a sort costs O(n log n), where n is how many numbers have arrived, and the cost grows with every call. The buffer also grows without limit. The lesson asks for O(log k) time per add and O(k) memory.

    Only the k largest matter

    A number that is not among the k largest can never become the answer. New numbers only push the k-th largest up, never down, so a number that falls behind stays behind. That means the object can forget everything except the k largest values.

    Among those k values, the answer is the smallest one. A min-heap gives the smallest of its values in O(1) and removes it in O(log k), which is exactly what is needed:

    • push the new value into the heap;
    • if the heap now holds k + 1 values, remove the smallest, which cannot be among the k largest;
    • the smallest value that is left is the k-th largest.

    The heap never holds more than k + 1 values, so each push and each removal costs O(log k).

    A min-heap in Scala

    Scala's scala.collection.mutable.PriorityQueue is a max-heap: head is the largest value. To get a min-heap, hand it the reversed ordering in a second parameter list: PriorityQueue[Int]()(Ordering[Int].reverse). The first pair of parentheses is the (empty) list of starting elements, and the second one holds the ordering. With the reversed ordering, "largest" means "smallest number", so head is the smallest value.

    Step by step

    1. Declare the heap. A val named largest, a PriorityQueue[Int] with the reversed ordering, holds the k largest values so far, smallest on top.
    2. Loop over nums. Statements in a class body run when the object is created, in order, so largest already exists when the loop starts.
    3. Call add(num) for each starting number. The starting numbers must go through the same filter as every later one, and add already keeps the heap at k values. The constructor ignores the returned value, because nobody has asked for it yet.
    4. In add, enqueue the value. It might be one of the k largest; the next step finds out.
    5. Trim the heap. If largest.size is greater than k, call dequeue() to drop the smallest value.
    6. Return largest.head. It is the smallest of the k largest values, which is the k-th largest.

    The solution in Scala

    class KthLargest(k: Int, nums: Array[Int]) {
        val largest = PriorityQueue[Int]()(Ordering[Int].reverse)
        for (num <- nums) {
            add(num)
        }
    
        def add(value: Int): Int = {
            largest.enqueue(value)
            if (largest.size > k) {
                largest.dequeue()
            }
            return largest.head
        }
    }
    

    Some Scala details:

    • Class parameters are visible in the whole class. k and nums have no val, yet the methods and the constructor loop can use them. Scala keeps a parameter as a private field when a method needs it.
    • The loop is the constructor. A for loop in the class body is not inside any method, so it runs once, when new KthLargest(...) is evaluated. It calls add, which is defined further down; that is fine because methods exist before the body runs. The order of fields matters, though: largest is declared above the loop because add uses it.
    • enqueue and dequeue(). enqueue adds a value, and dequeue() removes and returns the top. The call to dequeue() in add throws its result away; only the removal matters.
    • head reads without removing. On an empty queue, head throws NoSuchElementException.
    • The lesson's code uses PriorityQueue without a prefix. In a file of your own, add import scala.collection.mutable.PriorityQueue first.

    Complexity

    Time per add: O(log k). One enqueue and at most one dequeue(), each on a heap of at most k + 1 values. Reading head is O(1). The constructor calls add once per starting number, so it costs O(m log k) for m numbers.

    Space: O(k). The heap holds at most k + 1 values at any moment and k between calls, however many numbers arrive.

    Edge cases and common mistakes

    • Using a max-heap. Without Ordering[Int].reverse the top is the largest value, which is the 1st largest, not the k-th.
    • Starting numbers. They count like any other number. Skipping the loop gives wrong answers whenever nums is not empty.
    • Empty nums. KthLargest(1, Array()) starts with an empty heap. Because at least k numbers have arrived whenever add returns, head is safe.
    • Duplicates. They are separate values in the heap. With k = 4 and starting numbers 7, 7, 7, 7, 8, 3, the heap keeps four values, 7, 7, 7 and 8, and after add(2) the answer is 7, the smallest of them.
    • Trimming with >= instead of >. The test is size > k. With >=, the heap shrinks to k - 1 values and head becomes the (k-1)-th largest: for KthLargest(3, Array(4, 5, 8, 2)), add(3) returns 5 instead of 4. With k = 1 the heap is empty after trimming, and head throws NoSuchElementException.
    • Fewer than k numbers. The heap then holds everything seen, and head is the smallest of them, not a k-th largest. For KthLargest(3, Array(5)), add(1) returns 1. The statement rules this case out.

    Testing

    • KthLargest(3, Array(4, 5, 8, 2)) with add(3), add(5), add(10), add(9), add(4) returns 4 5 5 8 8.
    • KthLargest(1, Array()) with add(-3), add(-2), add(-4), add(0), add(4) returns -3 -2 -2 0 4.
    • KthLargest(4, Array(7, 7, 7, 7, 8, 3)) with add(2), add(10), add(9), add(9) returns 7 7 7 8.
    • KthLargest(2, Array()) with add(9) returns 9, which is outside the promise: only one number has arrived, so 9 is not a 2nd largest.
    Keep going with Scala500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free