The Hardest Part

You have built the machine. You understand the routing rule, the balance invariant, lazy deletion, and top pruning. You can implement a streaming median from scratch. You can extend it to sliding windows. You can explain why two heaps outperform a sorted array, and exactly what breaks when the balance invariant fails. The how is solid.

But here is a truth about patterns: knowing how they work is not the same as knowing when to use them. In an interview, nobody hands you a problem labeled “Two Heaps.” The problem says something about scheduling tasks, tracking percentiles in a data stream, or selecting investments under a budget constraint. The words “heap” and “median” may never appear. The pattern is hiding behind domain language, and your job is to see through the disguise.

This is the gap between understanding and fluency. Understanding means you can execute the pattern when someone tells you to use it. Fluency means you can recognize the pattern in a problem you have never seen before — and equally important, recognize when a problem looks like it fits but actually does not. The second skill is harder, and it is the one interviewers are really testing.

Three Families

The two-heap pattern shows up in three distinct families of problems. Each family uses two heaps for a different structural reason, and the clues that signal each family are different. Understanding the families — not just memorizing example problems — is what lets you classify new problems on sight.

Three families. Each uses two heaps differently. Tap to see how before the test begins.

0/3 families explored

The Imposters

Not every problem that involves heaps needs two heaps. This is the most common false positive, and it catches people who have just learned the pattern and are eager to apply it everywhere.

A problem that asks for the kth largest element in a stream? That is a single min-heap of size k. The heap maintains the top-k elements, and the root is always the kth largest. No second heap is needed — because you are not maintaining a boundary between two dynamic halves. You are maintaining a fixed-size cutoff. Elements below the cutoff are discarded; elements above it enter the heap and push the current minimum out. One heap, one direction, done.

A problem that asks you to merge k sorted lists? That is a single min-heap used as a merge buffer. The heap tracks the smallest unprocessed element across all k lists, and you extract-min repeatedly to produce the merged output. No partition, no boundary, no second heap.

The distinguishing question is: do I need to track both sides of a divide? If you only need one extreme (the max, the min, the kth element), a single heap suffices. If you only need to merge or schedule from one direction, a single heap suffices. Two heaps earn their keep only when the answer lives at the boundary between two groups — when you need the largest of the small half AND the smallest of the large half, or when you need to move elements between two competing pools as conditions change. The moment you realize the problem only cares about one side, the second heap is wasted complexity.

Problem One

Time for the real test. Read the problem description below and classify it into a family based on structural characteristics — not surface wording. After classification, assign heap roles to prove you understand why it belongs to that family.

Classify this problem. Which family does it belong to?
— There it is:Numbers arrive in a stream. Report the score separating the top 50% from the bottom 50%.

Which family does this belong to?

Problem Two

Another problem. Same challenge — classify by structure, then assign roles. The wording is different, the domain is different, but the pattern recognition skill is the same.

Classify this problem. Which family does it belong to?
— There it is:Startup capital W. Projects need min capital, yield profit. Maximize capital after k projects.

Which family does this belong to?

The Trap

One more. But be careful — not every problem with heaps in the solution uses two heaps. Sometimes the pattern does not apply at all.

Classify this problem. Which family does it belong to?
— There it is:Array + window of size k. Find the kth largest element in each sliding window.

Which family does this belong to?

The Recognition Heuristic

You have now classified problems across all three families and spotted the imposter. Let's crystallize what your brain was doing — the heuristic that lets you classify in seconds rather than minutes.

The universal signal for Two Heaps is: the answer lives at the boundary between two dynamic groups, and both groups change as data arrives. If the groups are static (already sorted, already partitioned), you do not need heaps — a pointer or binary search handles it. If only one group matters (the max, the top-k, the merge front), a single heap is sufficient. Two heaps are the right tool specifically when you need both sides of a moving divide.

Within that universal signal, the family-specific triggers refine your classification. “Streaming + rank-based query” points to partition boundary. “Pool + threshold + greedy selection” points to gating. “Window + median” points to sliding window with lazy deletion. And “single extreme + dynamic data” points away from two heaps — toward a simpler, cheaper single-heap solution.

There is one more heuristic worth internalizing: the operation count test. In a two-heap solution, every element eventually interacts with both heaps — it enters one and may be rebalanced to the other. If you cannot imagine a scenario where an element would need to cross the boundary, you probably do not need a boundary at all.

In Code: IPO

The gating pattern in real code. IPO (LeetCode 502) is the canonical example — a gate heap sorts by minimum capital required, a selection heap sorts by maximum profit. Walk through each section to see how the two heaps collaborate.

IPO: Maximize Capital. See the gating pattern in real code.
Explore each section of the IPO algorithm
1
// IPO — Maximize Capital (LeetCode 502)
2
function findMaxCapital(
3
  k: number, w: number,
4
  profits: number[], capital: number[]
5
): number {
6
  // Build gate heap: all projects by min capital
7
  const gate = new MinHeap(capital)
8
  // Selection heap: affordable projects by max profit
9
  const best = new MaxHeap()
10
11
  let currentCapital = w
12
  for (let i = 0; i < k; i++) {
13
    // Open gate: move affordable projects to selection
14
    while (gate.peek() <= currentCapital) {
15
      best.push(gate.extractWithProfit())
16
    }
17
    // Pick best: take highest-profit affordable project
18
    if (best.size > 0) {
19
      currentCapital += best.extractMax()
20
    }
21
  }
22
  return currentCapital
23
}

0/4 sections explored

The Complete Machine

Look at what you have built across this module. You started with a stream and a question — “what is the median?” — and discovered that maintaining full sorted order was quadratic overkill. The question only cared about the middle, so you invented a structure that maintains just enough order: two heaps, meeting at the boundary, each offering O(1) access to the element nearest the divide.

You learned that the partition is fragile without the balance invariant, and that a two-operation rebalance — extract from the heavy side, insert into the light side — restores correctness at O(log n) cost. You extended the structure to sliding windows by inventing lazy deletion: marking departed elements as ghosts, tolerating them in the interior, and pruning them from the root only when they threatened the answer. And you tested your recognition fluency by classifying unfamiliar problems into families and rejecting the ones that did not fit.

The two-heap pattern is deceptively small. Two data structures, a routing comparison, a size check, and (for windows) a ghost map. But the idea it encodes is profound: you do not need total order to answer positional questions. Partial order — knowing the boundary between the halves — is sufficient. That insight transfers far beyond heaps: into quickselect, order-statistic trees, and the broader principle that the cheapest way to answer a question is to maintain exactly the structure the question demands, and nothing more.

Two Heaps Complete

The GrindThe SplitThe BalanceGhost in the MachineSpot the Split
The GrindFelt O(n²) insertion pain, discovered heap splitting
The SplitBuilt the max/min partition and median formula
The BalanceConstructed the rebalancing invariant via CodeFill
Ghost in the MachineDiscovered lazy deletion with ghost map tracking
Spot the SplitClassified problems into partition familiesDone

MedianFinder -- tap badges to explore

1
class MedianFinder {
2
  maxHeap: MaxHeap  // lower half
3
  minHeap: MinHeap  // upper half
4
5
  addNum(num: number): void {
6
    // Always insert into maxHeap first
7
    this.maxHeap.push(num)
8
    // Ensure ordering: max top <= min top
9
    if (this.maxHeap.peek() > this.minHeap.peek()) {
10
      this.minHeap.push(this.maxHeap.extractMax())
11
    }
12
    // Rebalance sizes: differ by at most 1
13
    if (this.maxHeap.size < this.minHeap.size) {
14
      this.maxHeap.push(this.minHeap.extractMin())
15
    }
16
  }
17
18
  findMedian(): number {
19
    if (this.maxHeap.size > this.minHeap.size) {
20
      return this.maxHeap.peek()
21
    }
22
    return (this.maxHeap.peek() + this.minHeap.peek()) / 2
23
  }
24
}
You felt the pain of sorted insertion, then discovered how splitting into maxHeap and minHeap eliminates shifting entirely. You mastered the rebalancing invariant that keeps sizes within 1, uncovered how lazy deletion uses a ghostMap to avoid O(n) interior removal, and classified real problems by their partition structure. The two-heaps pattern is yours.