703. Kth Largest Element in a Stream

Maintain the kth largest value in a dynamic stream by keeping a min-heap of exactly k elements. The heap's root is always the smallest of the top-k values — exactly the kth largest seen so far.

EasyMin-HeapDesign / ClassStream ProcessingTypeScript

PROBLEM What we're solving

Design a class that tracks the kth largest element in a live stream of integers. constructor(k, nums) seeds it with an initial array, and add(val) inserts a new value and returns the current kth largest.

Worked example: k = 3, nums = [4, 5, 8, 2]. After construction the top-3 are [4, 5, 8]; the 3rd largest is 4. Calling add(3) → top-3 become [3, 4, 5] (8 stays above, 2 is still outside), so it returns 3. Calling add(5) → top-3 become [4, 5, 5], returns 4.

KEY IDEA A min-heap of exactly k elements IS the answer

Insight → if you keep only the k largest values seen so far in a min-heap, the root of that heap is the smallest of those k values — which is exactly the kth largest overall. Every add pushes the new value, then pops the root if the heap exceeds size k (evicting the smallest). The root is then the answer.

RECIPE Build once, add in O(log k)

  • 0 · Constructor. Store k, then call add() for every element in nums — so construction reuses the same logic.
  • 1 · Push. Insert the new value into the min-heap (sift up in O(log k)).
  • 2 · Trim. If heap.length > k, pop the minimum (the value that just fell out of the top-k). This keeps the heap at exactly size k.
  • 3 · Return root. heap[0] is the minimum of the k-heap, i.e. the kth largest overall.
Classic confusion → people reach for a max-heapbecause the problem says "kth largest", then get confused about what to pop. Use a min-heap of size k. The root is the smallest of the top-k — that is, the kth largest. A max-heap would give you the 1st largest at the root, which is not useful here.

COST Complexity & alternatives

Sort on every add
O(n log n)
Re-sort the whole stream each call.
Min-heap of size k
O(log k) per add
O(k) space; push + conditional pop.

Constructor costs O(n log k) for seeding with n initial values. Space is O(k) — only k elements ever live in the heap.

Pattern transfer → the "bounded min-heap" pattern appears in K Closest Points to Origin (max-heap of size k on distance), Top K Frequent Elements (min-heap keyed on frequency), and Find Median from Data Stream (two heaps: max of lower half + min of upper half).

RUN IT Min-heap of size k — root is the kth largest

step 0 / 30
STARTInitialize with k = 3 and seed array [4, 5, 8, 2]. We'll build a min-heap of exactly 3 elements — the root is always the kth largest seen so far.
1class KthLargest {
2 private heap: number[] = [];
3 private k: number;
4
5 constructor(k: number, nums: number[]) {
6 this.k = k;
7 for (const n of nums) this.add(n);
8 }
9
10 add(val: number): number {
11 this._heapPush(val);
12 if (this.heap.length > this.k) this._heapPop();
13 return this.heap[0]; // min of the k-heap = kth largest overall
14 }
15
16 // --- min-heap helpers ---
17 private _heapPush(val: number): void {
18 this.heap.push(val);
19 let i = this.heap.length - 1;
20 while (i > 0) {
21 const parent = (i - 1) >> 1;
22 if (this.heap[parent] <= this.heap[i]) break;
23 [this.heap[parent], this.heap[i]] = [this.heap[i], this.heap[parent]];
24 i = parent;
25 }
26 }
27
28 private _heapPop(): number {
29 const top = this.heap[0];
30 const last = this.heap.pop()!;
31 if (this.heap.length > 0) {
32 this.heap[0] = last;
33 let i = 0;
34 while (true) {
35 const l = 2 * i + 1, r = 2 * i + 2;
36 let smallest = i;
37 if (l < this.heap.length && this.heap[l] < this.heap[smallest]) smallest = l;
38 if (r < this.heap.length && this.heap[r] < this.heap[smallest]) smallest = r;
39 if (smallest === i) break;
40 [this.heap[smallest], this.heap[i]] = [this.heap[i], this.heap[smallest]];
41 i = smallest;
42 }
43 }
44 return top;
45 }
46}
State
3
k
0
size
val
evicted
heap[0]
val (newly added)evicted (was min)new root after popkth largest answer
slowfast

TYPESCRIPT The solution, annotated

kthLargest.ts
class KthLargest {
  private heap: number[] = [];
  private k: number;

  constructor(k: number, nums: number[]) {
    this.k = k;
    for (const n of nums) this.add(n);
  }

  add(val: number): number {
    this._heapPush(val);
    if (this.heap.length > this.k) this._heapPop();
    return this.heap[0];   // min of the k-heap = kth largest overall
  }

  // --- min-heap helpers ---
  private _heapPush(val: number): void {
    this.heap.push(val);
    let i = this.heap.length - 1;
    while (i > 0) {
      const parent = (i - 1) >> 1;
      if (this.heap[parent] <= this.heap[i]) break;
      [this.heap[parent], this.heap[i]] = [this.heap[i], this.heap[parent]];
      i = parent;
    }
  }

  private _heapPop(): number {
    const top = this.heap[0];
    const last = this.heap.pop()!;
    if (this.heap.length > 0) {
      this.heap[0] = last;
      let i = 0;
      while (true) {
        const l = 2 * i + 1, r = 2 * i + 2;
        let smallest = i;
        if (l < this.heap.length && this.heap[l] < this.heap[smallest]) smallest = l;
        if (r < this.heap.length && this.heap[r] < this.heap[smallest]) smallest = r;
        if (smallest === i) break;
        [this.heap[smallest], this.heap[i]] = [this.heap[i], this.heap[smallest]];
        i = smallest;
      }
    }
    return top;
  }
}

Reading it block by block

Lines 2–3 — instance state. heap is a plain array managed as a min-heap; k is stored so every add knows the cap. No library needed — we implement push/pop inline.
Lines 5–8 — constructor. Store k, then delegate each seed element to add(). This reuses the trimming logic rather than duplicating it, and means the heap is never larger than k even after seeding.
Lines 10–13 — add. Push the new value, then pop if over capacity. The heap is now the k largest values seen. Return heap[0] — the smallest of those k values — which is the kth largest overall.
Lines 16–26 — _heapPush (sift up). Append to the end, then swap with parent while the parent is larger. The >> bit-shift is an efficient way to compute (i - 1) / 2 | 0.
Lines 28–46 — _heapPop (sift down). Swap root with the last element, shrink the array, then restore the heap property by sifting the new root down: at each step swap with the smaller child. Stops when no child is smaller.
Complexity → Each add call does one push + at most one pop, both O(log k). The constructor seeds with n elements → total O(n log k). Space is O(k).

INTERVIEWFollow-ups they'll ask

  • "What if k = 1?" The heap holds a single element — the running maximum. Each add still just compares and possibly replaces.
  • "Can you find the kth smallest instead?" Flip to a max-heap of size k. The root — the largest of the k smallest — is the kth smallest.
  • "What if the stream is infinite and memory is tight?" The heap already solves this: O(k) space regardless of how many values have streamed through.
  • "Follow-up: return the k largest, not just the kth?" heap.slice() gives all k elements but in heap order, not sorted; sort them additionally in O(k log k) if needed.
  • "Can the initial nums array be empty?" Yes. The heap starts empty; the first add populates it. LeetCode guarantees at least one call toadd before reading the result.

OPTIMAL Min-Heap

kthLargest.ts
class KthLargest {
  private heap: number[] = [];
  private k: number;

  constructor(k: number, nums: number[]) {
    this.k = k;
    for (const n of nums) this.add(n);
  }

  add(val: number): number {
    this._heapPush(val);
    if (this.heap.length > this.k) this._heapPop();
    return this.heap[0];   // min of the k-heap = kth largest overall
  }

  // --- min-heap helpers ---
  private _heapPush(val: number): void {
    this.heap.push(val);
    let i = this.heap.length - 1;
    while (i > 0) {
      const parent = (i - 1) >> 1;
      if (this.heap[parent] <= this.heap[i]) break;
      [this.heap[parent], this.heap[i]] = [this.heap[i], this.heap[parent]];
      i = parent;
    }
  }

  private _heapPop(): number {
    const top = this.heap[0];
    const last = this.heap.pop()!;
    if (this.heap.length > 0) {
      this.heap[0] = last;
      let i = 0;
      while (true) {
        const l = 2 * i + 1, r = 2 * i + 2;
        let smallest = i;
        if (l < this.heap.length && this.heap[l] < this.heap[smallest]) smallest = l;
        if (r < this.heap.length && this.heap[r] < this.heap[smallest]) smallest = r;
        if (smallest === i) break;
        [this.heap[smallest], this.heap[i]] = [this.heap[i], this.heap[smallest]];
        i = smallest;
      }
    }
    return top;
  }
}
Complexity → Each add call does one push + at most one pop, both O(log k). The constructor seeds with n elements → total O(n log k). Space is O(k).

ALT 1 Brute force — store everything and re-sort on each add

O(n log n) per add · O(n) space

Keep every value ever seen in an array. On each add, push the new value, sort the whole array descending, and read off index k - 1.

approach-2.ts
class KthLargest {
  private nums: number[];
  private k: number;

  constructor(k: number, nums: number[]) {
    this.k = k;
    this.nums = [...nums];
  }

  add(val: number): number {
    this.nums.push(val);
    this.nums.sort((a, b) => b - a);   // descending
    return this.nums[this.k - 1];      // kth largest
  }
}
Note → Re-sorting the entire history every call is O(n log n) per add and stores all n values. A size-k min-heap keeps only the top k and answers each add in O(log k) time, O(k) space.

MNEMONIC The one-liner

"Keep a bucket of the top-k fish. When a new fish swims in, toss out the runt if the bucket is full — the runt is now the kth largest."

TRIGGERS When you see ___ → reach for ___

"kth largest / smallest in a stream"bounded min/max-heap of size k
top-k elements, online / dynamicmin-heap, pop when size > k
"median from data stream"two heaps: max-heap + min-heap
k closest / most frequent elementsheap keyed on distance/frequency

SKELETON The reusable shape

skeleton.ts
class KthLargest {
  private heap: number[] = [];
  private k: number;
  constructor(k: number, nums: number[]) {
    this.k = k;
    for (const n of nums) this.add(n);
  }
  add(val: number): number {
    // push onto min-heap
    // if size > k, pop the smallest (it's outside the top-k)
    // heap[0] is the kth largest
    return this.heap[0];
  }
}

FLASHCARDS Tap to flip

Why use a min-heap (not max-heap) for kth largest?
A min-heap of size k keeps the k largest values; its root is the smallestof those k — that's the kth largest. A max-heap root gives you the 1st largest.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 9
QUESTION 01
For k = 3 and stream so far [4, 5, 8, 2], what does add(3) return?
QUESTION 02
Why use a min-heap rather than a max-heap for this problem?
QUESTION 03
What is the time complexity of each add() call?
QUESTION 04
What happens when heap.length > k after a push?
QUESTION 05
With k = 2, after seeding with [1, 10] and calling add(5), what is in the heap and what is returned?
QUESTION 06
What is the space complexity of this data structure?
QUESTION 07
If you wanted the kth smallest instead of largest, what single change is needed?
QUESTION 08
#703 · Kth Largest Element in a StreamMaintain a min-heap of size k: its root is always the k-th largest value seen. Each add() pushes the new value and pops the smallest if the heap exceeds size k, giving O(log k) per call.Which algorithmic approach does this primarily use?
QUESTION 09
#703 · Kth Largest Element in a StreamMaintain a min-heap of size k: its root is always the k-th largest value seen. Each add() pushes the new value and pops the smallest if the heap exceeds size k, giving O(log k) per call.Which implementation correctly solves it?