Kth Largest Element in a Stream
1. Problem & Core Objective
Design a class that reports the k-th largest element seen so far, as values stream in.
KthLargest(int k, int[] nums) // seed with an initial array
int add(int val) // add a value, return the kth largest so farKthLargest kth = new KthLargest(3, [4, 5, 8, 2]);
kth.add(3); → 4 // sorted: 8,5,4,3,2 → 3rd largest is 4
kth.add(5); → 5 // 8,5,5,4,3,2 → 5
kth.add(10); → 5
kth.add(9); → 8
kth.add(4); → 8Constraints: 1 <= k <= 10^4 · 0 <= nums.length <= 10^4 · up to 10^4 calls to add · it is guaranteed there are always at least k elements when add is called
What's actually being tested: the counterintuitive pairing — to track the k LARGEST, you use a MIN-heap. If that inversion doesn't feel obvious, this is the question to fix it on, because questions 3 and 4 rely on the same move.
2. First-Principles Thought Process
What "streaming" rules out
Values arrive one at a time and the answer is needed after each. So any approach that re-derives the answer from scratch pays that cost on every call.
Sorting is the obvious example: O(n log n) per add, with n growing. That's not a constant-factor problem, it's the wrong shape.
The observation that shrinks the problem
To report the k-th largest, you only ever need the top k values. Everything below them is irrelevant — it can never become the k-th largest, because k values already beat it and they're all still present.
So: keep exactly k elements, discard the rest permanently. Memory drops from O(n) to O(k).
Which k elements, and which one is the answer?
Keep the k largest. Then the k-th largest is the smallest of those k — and that's the one you need constant access to.
A structure whose root is the minimum is a min-heap.
Why a max-heap is exactly wrong
A max-heap puts the largest on top. But the largest is the element you most want to keep, and it's the one you'd never evict. The element you actually need — both to report and to discard — is the smallest of the kept set, which a max-heap buries at a leaf where finding it is O(k).
Min-heap for the k largest; max-heap for the k smallest. The heap is inverted relative to the question, and that inversion is the whole insight.
The maintenance rule
minHeap.offer(val);
if (minHeap.size() > k) minHeap.poll(); // drop the smallest
return minHeap.peek(); // the kth largestOffer unconditionally, then trim. If the new value belonged in the top k, something else gets pushed out; if it didn't, it is itself immediately evicted. Either way the invariant "the heap holds the k largest seen" is restored in one line.
3. Solution Paths
Approach 1 — Keep everything, sort on each query (brute force)
class KthLargest {
private final List<Integer> all = new ArrayList<>();
private final int k;
public KthLargest(int k, int[] nums) {
this.k = k;
for (int n : nums) all.add(n);
}
public int add(int val) {
all.add(val);
Collections.sort(all, Collections.reverseOrder());
return all.get(k - 1);
}
}- Time
O(n log n)peradd· SpaceO(n)
Counter-questions on this approach
⭐ "What's the total cost across all calls?"
With
10^4adds against a collection growing to2 × 10^4, each sort is about2 × 10^4 × 15 ≈ 3 × 10^5comparisons, so roughly3 × 10^9overall. Far too slow.The deeper problem is that it re-derives the full order every time to read one element. Sorting produces complete information; the question asks for one rank.
⭐ "You could insert into a sorted list instead of re-sorting. Does that fix it?"
It helps but doesn't solve it. Binary search finds the insertion point in
O(log n), but anArrayListinsert shifts elements —O(n)per add, so2 × 10^8overall. Borderline.And it still stores everything. The real saving is noticing that elements outside the top
kare permanently irrelevant and can be thrown away, which no sorted-list variant exploits.
"Is storing all n ever necessary?"
Only if the query could change — "give me the
j-th largest for arbitraryj" would need everything. Withkfixed at construction, the bottomn − kelements can never be the answer.
Approach 2 — Min-heap of size k (optimal)
class KthLargest {
private final PriorityQueue<Integer> minHeap = new PriorityQueue<>();
private final int k;
public KthLargest(int k, int[] nums) {
this.k = k;
for (int n : nums) add(n); // reuse the same maintenance rule
}
public int add(int val) {
minHeap.offer(val);
if (minHeap.size() > k) minHeap.poll();
return minHeap.peek();
}
}Trace — k = 3, seeded with [4,5,8,2], then add(3):
| Step | Heap after offer | Size > 3? | Poll | Heap | peek() |
|---|---|---|---|---|---|
| 4 | [4] | no | — | [4] | 4 |
| 5 | [4,5] | no | — | [4,5] | 4 |
| 8 | [4,5,8] | no | — | [4,5,8] | 4 |
| 2 | [2,4,5,8] | yes | 2 | [4,5,8] | 4 |
add(3) | [3,4,5,8] | yes | 3 | [4,5,8] | 4 ✓ |
Note that 2 and 3 are evicted the instant they arrive — they never displace anything.
- Time
O(log k)peradd,O(n log k)to seed · SpaceO(k)
Counter-questions on this approach
⭐ "Why a min-heap when the question asks for the largest?"
Because of what I need constant access to. I keep the
klargest values, and the answer — thek-th largest — is the smallest of those. A min-heap puts exactly that at the root, readable inO(1)and removable inO(log k).A max-heap would surface the biggest value, which is neither the answer nor the eviction candidate. The element I need would sit somewhere among the leaves, costing
O(k)to find.The rule to remember: min-heap for the k largest, max-heap for the k smallest. The heap is always inverted relative to the wording.
⭐ "Why offer first and then check the size, rather than comparing against the root first?"
Both work. The conditional version is:
Javaif (minHeap.size() < k) minHeap.offer(val); else if (val > minHeap.peek()) { minHeap.poll(); minHeap.offer(val); }That avoids one heap operation when
valis too small, which is a real saving if most values are. But it's three branches instead of two lines, and the offer-then-trim version restores the invariant unconditionally — harder to get wrong. I'd write the simple one and mention the optimisation.
"Why does the constructor call add rather than heapifying?"
Mostly for reuse — one maintenance rule, one place to get it right. Seeding via
addisO(n log k).If
numswere large you could do better: heapify all of it inO(n)withnew PriorityQueue<>(list), then poll down to sizek, which isO(n + (n−k) log n). That's worse thanO(n log k)whenkis small, so the simple version is usually right. Worth knowing both.
"What if add is called when fewer than k elements exist?"
peek()would return the smallest of however many there are, which isn't ak-th largest at all. The constraints guarantee it can't happen, but in real code I'd return a sentinel or throw rather than a misleading number.
"Can nums be empty?"
Yes — the constraint allows length 0. The heap starts empty, and the first few
addcalls just fill it. The guarantee thataddis only called oncekelements exist is what keepspeek()meaningful.
"Does the heap hold duplicates correctly?"
Yes.
PriorityQueuepermits duplicates, and duplicates matter here: with[5,5,5]andk = 3, the 3rd largest is 5. ATreeSetwould silently collapse them and return the wrong answer — a real trap if you reach for a sorted set instead.
Approach 3 — TreeMap as a multiset
private final TreeMap<Integer, Integer> counts = new TreeMap<>(); // value -> multiplicity
private int size;
public int add(int val) {
counts.merge(val, 1, Integer::sum);
size++;
if (size > k) {
int lowest = counts.firstKey();
if (counts.merge(lowest, -1, Integer::sum) == 0) counts.remove(lowest);
size--;
}
return counts.firstKey();
}- Time
O(log k)peradd· SpaceO(k)distinct values
Counter-questions on this approach
⭐ "Same complexity as the heap. Why prefer the heap?"
The heap is simpler and has better constants — an array with no node allocation, versus a red-black tree with an object per entry. And this version needs explicit multiplicity bookkeeping, because a
TreeMap's keys are unique while the problem's values are not.Where the
TreeMapwins is if the interface grew: it supportsfloorKey,ceilingKey, ordered iteration, and removal of an arbitrary value — all things a heap cannot do. For this interface none of that is needed.
"What breaks if you use a plain TreeSet instead?"
Duplicates collapse. With
k = 3and values[5,5,5], aTreeSetholds one 5 and reports the wrong rank. It's a silent wrong answer, which makes it worse than a crash — and it's the most common wrong turn on this problem.
Comparison
| Approach | add | Space | Notes |
|---|---|---|---|
| Store all, re-sort | O(n log n) | O(n) | Recomputes the full order for one rank |
| Sorted list, binary insert | O(n) | O(n) | Shifting dominates; still stores everything |
| Min-heap of size k | O(log k) | O(k) | The answer |
TreeMap multiset | O(log k) | O(k) | Same bound, more machinery |
4. Why the Optimal Wins
Two separate savings, and it's worth naming them apart.
Bounded memory. Elements outside the top k can never become the k-th largest, so they're discarded permanently. O(n) → O(k).
Bounded work. A heap maintains just enough order to expose one element — the minimum — rather than the full ordering. O(n log n) → O(log k) per add.
Together they turn a problem that gets slower as the stream grows into one whose per-element cost is constant in the stream length.
The framing worth keeping:
To track the
klargest, keep a MIN-heap of sizek. Its root is both the answer and the next thing to evict.
Questions 3 and 4 are the same move on different data.
5. Java Prerequisites
PriorityQueue is a min-heap by default
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());| Operation | Cost |
|---|---|
offer | O(log n) |
poll | O(log n) |
peek | O(1) |
remove(Object) | O(n) — no ordering to exploit |
size | O(1) |
It permits duplicates and forbids nulls. Both matter here — duplicates are required for correct ranks.
O(n) heapify from a collection
new PriorityQueue<>(existingList); // O(n), cheaper than n insertsComparator overflow — prefer Comparator.reverseOrder() or comparingInt over (a, b) -> b - a, which overflows for large values. See 04.
6. Interview Communication Guide
Clarifying questions: Is k fixed at construction (yes — that's what lets me discard)? Can values repeat (yes; this rules out a TreeSet)? How many add calls (10^4)? Is add guaranteed to be called only once k elements exist (yes)?
The pitch
"Because it's a stream, anything that recomputes from scratch pays that cost on every call — sorting would be
O(n log n)per add withngrowing.The observation that shrinks it: to report the
k-th largest I only ever need the topkvalues. Anything below them can never become the answer, becauseklarger values already exist and none of them disappear. So I can throw the rest away permanently —O(n)memory becomesO(k).Then: which of those
kdo I need fast access to? Thek-th largest is the smallest of thekI'm keeping — and that's also exactly the one to evict when a bigger value arrives. A structure whose root is the minimum is a min-heap.So it's a min-heap of size
k, which feels backwards given the question says 'largest'. A max-heap would surface the biggest value, which is the one I most want to keep and never want to remove — the element I actually need would be buried at a leaf.Maintenance is two lines: offer the value, and if the size exceeds
k, poll. Thenpeek()is the answer. A value too small to belong gets evicted immediately, so the invariant is restored either way.
O(log k)per add,O(k)space.One thing I'd avoid: a
TreeSet. Values can repeat, and a set would collapse duplicates and silently return the wrong rank."
Edge cases to volunteer:
| Scenario | Expected | Tests |
|---|---|---|
k = 1 | Always the maximum | Heap holds one element |
nums empty, then k adds | Works | Heap fills before the first meaningful query |
| All values identical | That value | Duplicates must be kept — rules out TreeSet |
| Values arrive in ascending order | Each add evicts the old root | Constant churn |
| Values arrive in descending order | Answer never changes after the first k | Every new value is evicted immediately |
k equals the total count | The minimum overall | Nothing is ever evicted |
Name the all-identical case. It's the cheapest check that duplicates are preserved, and a TreeSet-based solution fails it while passing almost everything else.
7. Follow-Up Questions — Modified Constraints
⭐ "Support the k-th SMALLEST instead."
Flip the heap: a max-heap of size
k, whose root is the largest of theksmallest — again both the answer and the eviction candidate. The symmetry is exact, and being able to state it confirms you understood the inversion rather than memorised it.
⭐ "Allow k to change after construction."
Growing
kis impossible — you already discarded the elements you'd now need. That's the cost of the space saving, and it's worth stating plainly. Shrinkingkis easy: poll until the size matches. To support growth you'd have to retain everything, which puts you back to an order-statistic tree atO(log n)for arbitrary ranks.
"Support removing a previously added value."
PriorityQueue.remove(Object)isO(n)because a heap has no way to locate an arbitrary element. Options: aTreeMapmultiset, which removes inO(log k); or lazy deletion — keep a count of pending removals and discard stale entries as they surface at the root. Lazy deletion is the standard trick and it's what question 6 uses in spirit.
"Return the top k values, not just the k-th."
The heap already holds exactly them — drain it,
O(k log k), or iterate it unordered inO(k)if order doesn't matter. Note that iterating aPriorityQueuedoes not yield sorted order; only repeatedpolldoes.
"What if k were close to n — say k = n/2?"
The heap holds half the stream, so space is
O(n)and thelog kislog n. At that point the space saving evaporates and sorting once is competitive. The heap wins whenk ≪ n, which is worth saying rather than presenting it as universally better.
"What if the stream were distributed across machines?"
Each node keeps its own size-
kmin-heap, then the coordinator merges theklocals — because the global topkmust be in the union of the per-node topks. That'sO(machines × k)to merge and it's the standard distributed top-k pattern.