Kth Largest Element in a Stream
EasyPrompt
You are part of a university admissions office and need to keep track of the kth highest test score from applicants in real-time. This helps to determine cut-off marks for interviews and admissions dynamically as new applicants submit their scores.
You are tasked to implement a class which, for a given integer k, maintains a stream of test scores and continuously returns the kth highest test score after a new score has been submitted. More specifically, we are looking for the kth highest score in the sorted list of all scores.
Implement the KthLargest class:
KthLargest(int k, int[] nums)Initializes the object with the integerkand the stream of test scoresnums.int add(int val)Adds a new test scorevalto the stream and returns the element representing thekthlargest element in the pool of test scores so far.
Example 1:
Input:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
Output: [null, 4, 5, 5, 8, 8]
Explanation:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3); // return 4
kthLargest.add(5); // return 5
kthLargest.add(10); // return 5
kthLargest.add(9); // return 8
kthLargest.add(4); // return 8
Example 2:
Input:
["KthLargest", "add", "add", "add", "add"]
[[4, [7, 7, 7, 7, 8, 3]], [2], [10], [9], [9]]
Output: [null, 7, 7, 7, 8]
Explanation:
KthLargest kthLargest = new KthLargest(4, [7, 7, 7, 7, 8, 3]);kthLargest.add(2); // return 7
kthLargest.add(10); // return 7
kthLargest.add(9); // return 7
kthLargest.add(9); // return 8
Constraints:
0 <= nums.length <= 1041 <= k <= nums.length + 1-104 <= nums[i] <= 104-104 <= val <= 104- At most
104calls will be made toadd.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach uses a dynamic list to store all the numbers from the stream. Each time a new number is added via the add method, the entire list is sorted to find the kth largest element. While simple to conceptualize and implement, its performance degrades quickly as the stream size increases.
Algorithm
- Initialization (
KthLargest(k, nums)):- Store the integer
k. - Create a dynamic list (e.g.,
ArrayListin Java). - Add all elements from the initial
numsarray into this list.
- Store the integer
- Adding an element (
add(val)):- Add the new value
valto the list. - Sort the entire list in ascending order.
- The
kth largest element is located at indexlist.size() - k. - Return the element at this index.
- Add the new value
Walkthrough
The core idea is to maintain a collection of all numbers encountered so far. When a new number arrives, we add it to our collection and then sort the entire collection to re-establish order. Once sorted, finding the kth largest element is a simple matter of accessing the correct index from the end of the sorted list.
For example, if we have the numbers [4, 5, 8, 2] and k=3, and we add 3, our list becomes [4, 5, 8, 2, 3]. Sorting this gives [2, 3, 4, 5, 8]. The 3rd largest element is at index 5 - 3 = 2, which is 4.
import java.util.ArrayList;import java.util.Collections;import java.util.List; class KthLargest { private int k; private List<Integer> stream; public KthLargest(int k, int[] nums) { this.k = k; this.stream = new ArrayList<>(); for (int num : nums) { this.stream.add(num); } } public int add(int val) { this.stream.add(val); Collections.sort(this.stream); return this.stream.get(this.stream.size() - k); }}Complexity
Time
O(L log L) for each `add` call, where L is the current size of the list. The constructor takes O(1) if we don't sort initially, or O(I log I) if we do (where I is the initial number of elements). The dominant cost comes from repeatedly sorting in the `add` method.
Space
O(N), where N is the total number of elements in the stream. We need to store every number that has been added.
Trade-offs
Pros
Very simple to understand and implement.
Requires minimal data structure knowledge.
Cons
Highly inefficient due to re-sorting the entire list on every
addoperation.Poor scalability as the number of elements in the stream grows.
This approach is an improvement over the first one. Instead of re-sorting the entire list every time, we maintain a sorted list. When a new element is added, we find its correct sorted position and insert it there. This avoids a full sort but still requires shifting elements, leading to linear time complexity for each addition.
Algorithm
- Initialization (
KthLargest(k, nums)):- Store the integer
k. - Create a dynamic list and populate it with elements from
nums. - Sort the list once.
- Store the integer
- Adding an element (
add(val)):- Find the correct position to insert
valinto the sorted list to maintain its order. This can be done with a linear scan or binary search. - Insert
valat that position. This operation takes linear time in an array-based list as it requires shifting elements. - Return the element at index
list.size() - k.
- Find the correct position to insert
Walkthrough
We start by sorting the initial array of numbers. When a new number val is added, we don't re-sort everything. Instead, we find the index where val should be inserted to keep the list sorted. While finding this index can be done efficiently in O(log L) time using binary search, the insertion into an ArrayList at a specific index takes O(L) time because all subsequent elements must be shifted one position to the right. After insertion, the kth largest element is found at the same list.size() - k index.
import java.util.ArrayList;import java.util.Collections;import java.util.List; class KthLargest { private int k; private List<Integer> stream; public KthLargest(int k, int[] nums) { this.k = k; this.stream = new ArrayList<>(); for (int num : nums) { this.stream.add(num); } Collections.sort(this.stream); } public int add(int val) { // A simple linear scan to find the insertion point. // A binary search would be faster to find the index, but the // list.add(index, value) operation is the O(L) bottleneck. int i = 0; while (i < stream.size() && stream.get(i) < val) { i++; } stream.add(i, val); return stream.get(stream.size() - k); }}Complexity
Time
O(L) for each `add` call, where L is the current size of the list. The constructor takes O(I log I) to perform the initial sort on I elements. This is better than the brute-force approach but still not optimal for a large number of `add` calls.
Space
O(N), where N is the total number of elements in the stream. All numbers are stored in the list.
Trade-offs
Pros
More efficient than the full sorting approach, improving
addfrom O(L log L) to O(L).Still relatively easy to implement.
Cons
The
addoperation is still slow with a linear time complexity.Space complexity is O(N), as all elements are stored, which is not optimal.
The most efficient solution involves using a min-heap of a fixed size k. The heap stores the k largest elements encountered so far. The smallest of these k elements (which is the root of the min-heap) is the kth largest element in the entire stream. This approach provides optimal time and space complexity.
Algorithm
- Data Structure: Use a Min-Heap (Priority Queue in Java) to store the
klargest elements. - Initialization (
KthLargest(k, nums)):- Store
k. - Initialize an empty min-heap.
- Iterate through the initial
numsarray and call theaddmethod for each number to populate the heap correctly.
- Store
- Adding an element (
add(val)):- If the heap's size is less than
k, addvalto the heap. - If the heap's size is
k, comparevalwith the smallest element in the heap (the root, accessible viapeek()). - If
valis larger than the root, remove the root (poll()) and addvalto the heap (offer()). - If
valis smaller or equal, do nothing. - The
kth largest element in the stream is now the root of the min-heap. Returnheap.peek().
- If the heap's size is less than
Walkthrough
The key insight is that we only need to keep track of the k largest numbers, not all of them. A min-heap is perfect for this. It's a priority queue that always keeps the smallest element at the root. By maintaining a min-heap of size k, the root will always be the kth largest element seen so far.
When we add a new element val:
- If the heap isn't full (size <
k), we addval. - If the heap is full, we check if
valis larger than the currentkth largest element (heap.peek()). If it is,valdeserves to be in our set ofklargest numbers. We remove the smallest member of this set (heap.poll()) and addval. Otherwise,valis smaller than the currentkth largest, so we can ignore it.
After this process, the root of the heap (heap.peek()) is the answer.
import java.util.PriorityQueue; class KthLargest { private final int k; private final PriorityQueue<Integer> minHeap; public KthLargest(int k, int[] nums) { this.k = k; this.minHeap = new PriorityQueue<>(); for (int num : nums) { // Use the add method's logic to build the initial heap add(num); } } public int add(int val) { if (minHeap.size() < k) { minHeap.offer(val); } else if (val > minHeap.peek()) { minHeap.poll(); minHeap.offer(val); } return minHeap.peek(); }}Complexity
Time
O(log k) for each `add` call. Heap operations (offer, poll, peek) on a heap of size `k` take logarithmic time. The constructor takes O(N log k) to process the initial N elements.
Space
O(k). The heap stores at most `k` elements, regardless of the total number of elements in the stream.
Trade-offs
Pros
Optimal time complexity for adding elements.
Optimal space complexity, as it only stores
kelements.Highly scalable for large streams of data.
Cons
Slightly more complex to understand if not familiar with the heap data structure.
Solutions
Solution
class KthLargest { private PriorityQueue < Integer > q ; private int size ; public KthLargest ( int k , int [] nums ) { q = new PriorityQueue <>( k ); size = k ; for ( int num : nums ) { add ( num ); } } public int add ( int val ) { q . offer ( val ); if ( q . size () > size ) { q . poll (); } return q . peek (); } } /** * Your KthLargest object will be instantiated and called as such: * KthLargest obj = new KthLargest(k, nums); * int param_1 = obj.add(val); *//** * @param {number} k * @param {number[]} nums */ var KthLargest = function ( k, nums,) { this.k = k; this.heap = new MinHeap(); for (let num of nums) { this.add(num); }};/** * @param {number} val * @return {number} */ KthLargest.prototype.add = function (val) { this.heap.offer(val); if (this.heap.size() > this.k) { this.heap.poll(); } return this.heap.peek(); };class MinHeap { constructor(data = []) { this.data = data; this.comparator = (a, b) => a - b; this.heapify(); } heapify() { if (this.size() < 2) return; for (let i = 1; i < this.size(); i++) { this.bubbleUp(i); } } peek() { if (this.size() === 0) return null; return this.data[0]; } offer(value) { this.data.push(value); this.bubbleUp(this.size() - 1); } poll() { if (this.size() === 0) { return null; } const result = this.data[0]; const last = this.data.pop(); if (this.size() !== 0) { this.data[0] = last; this.bubbleDown(0); } return result; } bubbleUp(index) { while (index > 0) { const parentIndex = (index - 1) >> 1; if (this.comparator(this.data[index], this.data[parentIndex]) < 0) { this.swap(index, parentIndex); index = parentIndex; } else { break; } } } bubbleDown(index) { const lastIndex = this.size() - 1; while (true) { const leftIndex = index * 2 + 1; const rightIndex = index * 2 + 2; let findIndex = index; if ( leftIndex <= lastIndex && this.comparator(this.data[leftIndex], this.data[findIndex]) < 0 ) { findIndex = leftIndex; } if ( rightIndex <= lastIndex && this.comparator(this.data[rightIndex], this.data[findIndex]) < 0 ) { findIndex = rightIndex; } if (index !== findIndex) { this.swap(index, findIndex); index = findIndex; } else { break; } } } swap(index1, index2) { [this.data[index1], this.data[index2]] = [ this.data[index2], this.data[index1], ]; } size() { return this.data.length; }} /** * Your KthLargest object will be instantiated and called as such: * var obj = new KthLargest(k, nums) * var param_1 = obj.add(val) */class KthLargest { public: priority_queue < int , vector < int > , greater < int >> q ; int size ; KthLargest ( int k , vector < int >& nums ) { size = k ; for ( int num : nums ) add ( num ); } int add ( int val ) { q . push ( val ); if ( q . size () > size ) q . pop (); return q . top (); } }; /** * Your KthLargest object will be instantiated and called as such: * KthLargest* obj = new KthLargest(k, nums); * int param_1 = obj->add(val); */class KthLargest : def __init__ ( self , k : int , nums : List [ int ]): self . q = [] self . size = k for num in nums : self . add ( num ) def add ( self , val : int ) -> int : heappush ( self . q , val ) if len ( self . q ) > self . size : heappop ( self . q ) return self . q [ 0 ] # Your KthLargest object will be instantiated and called as such: # obj = KthLargest(k, nums) # param_1 = obj.add(val)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.