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, which the constraints are set up to rule out.
Approach 2 — Min-heap of size k
The insight: the k-th largest element is the minimum of the k largest. Keep only the k largest values seen so far in a min-heap. Any value that drops out of the top k can never re-enter it, so it is safe to discard. 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. The root holds the smallest of the k retained values, which is exactly the k-th largest of the stream:
graph TD
A["4 (root = 3rd largest)"] --> B["5"]
A --> C["8"]
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.