TL;DR
Size-k min-heap: add is O(log k) time, O(k) space.
Approach 1 β Brute force (re-sort on every add)
Store every value; on each add, sort descending and return index k - 1.
from typing import List
class KthLargest:
def __init__(self, k: int, nums: List[int]):
self.k = k
self.nums = list(nums)
def add(self, val: int) -> int:
self.nums.append(val)
self.nums.sort(reverse=True)
return self.nums[self.k - 1]
Each add costs O(n log n) with n the stream length so far; with up to 10^4 adds on a stream that grows to 2Β·10^4 elements, that is hundreds of millions of comparison steps β the per-call sort is what the constraints are designed to punish.
Approach 2 β Min-heap of size k
The insight: the k-th largest element is the minimum of the k largest. So keep only the k largest values seen so far in a min-heap; anything that falls out of that top-k club can never re-enter it, so it is safe to throw away. The heap root is the answer at all times.
A binary min-heap is a complete binary tree (stored as an array, Pythonβs heapq) where every parent is β€ its children, giving O(1) peek-min and O(log size) push/pop.
import heapq
from typing import List
class KthLargest:
def __init__(self, k: int, nums: List[int]):
self.k = k
self.heap = list(nums)
heapq.heapify(self.heap)
while len(self.heap) > k:
heapq.heappop(self.heap)
def add(self, val: int) -> int:
heapq.heappush(self.heap, val)
if len(self.heap) > self.k:
heapq.heappop(self.heap)
return self.heap[0]
Walkthrough of KthLargest(3, [4, 5, 8, 2]):
- Init: heapify
[4, 5, 8, 2], then pop once (size 4 > 3) removing 2 β heap holds {4, 5, 8}, root 4.
add(3): push 3 β {3, 4, 5, 8} β pop min 3 β root 4. Return 4.
add(5): push 5 β {4, 5, 5, 8} β pop 4 β {5, 5, 8}. Return 5.
add(10): push 10 β pop 5 β {5, 8, 10}. Return 5.
add(9): push 9 β pop 5 β {8, 9, 10}. Return 8.
add(4): 4 is pushed then immediately popped (it is the min) β {8, 9, 10}. Return 8.
Matches the expected outputs 4, 5, 5, 8, 8.
Complexity: __init__ O(n log n) (or O(n + (nβk) log n) with heapify-then-pop); each add O(log k). Space O(k).
Approach 3 β Slightly slicker add (guarded push)
The insight: once the heap already has k elements, a new value only matters if it beats the current root; heapq.heappushpop does push-then-pop in one sift, and skipping values <= root avoids even that.
import heapq
from typing import List
class KthLargest:
def __init__(self, k: int, nums: List[int]):
self.k = k
self.heap = heapq.nlargest(k, nums) # at most k items
heapq.heapify(self.heap)
def add(self, val: int) -> int:
if len(self.heap) < self.k:
heapq.heappush(self.heap, val)
elif val > self.heap[0]:
heapq.heappushpop(self.heap, val)
return self.heap[0]
Same asymptotics β O(log k) per add, O(k) space β but each add does at most one sift instead of two, and often zero. On the walkthrough above the behavior is identical (e.g. add(4) is rejected up front because 4 <= 8).
Common pitfalls
- Using a max-heap of everything: peek gives the 1st largest, not the k-th, and finding the k-th costs k pops per query.
- Forgetting that
nums may start with fewer than k elements β donβt index heap[0] before trimming logic runs; the guarantee is only that add is never asked before k elements exist.
- Treating βk-th largestβ as k-th distinct value; duplicates each occupy a slot (
[7, 7, 7] with k = 2 β 7).
- Rebuilding or re-sorting per call β the whole point of the streaming setup is amortized O(log k) updates.
Pattern takeaway
When a stream question asks for βthe k-th largest/smallest so far,β keep the opposite-ordered heap capped at size k: a min-heap for k-largest (root = answer), a max-heap for k-smallest. Elements evicted from the top-k can never return, so the cap is safe, and every update is O(log k) regardless of stream length.