Pattern

Two Heaps

Split elements into two balanced groups using a max-heap and a min-heap to track the running median in O(log n) per insertion.

Roadmap

  1. What Is This Pattern?
  2. When to Use It
  3. Variant 1 — Find Median from Data Stream
  4. Variant 2 — Sliding Window Median
  5. Template Code
  6. Visual Walkthrough
  7. Common Problems
  8. Interactive Demo
  9. Complexity Analysis
  10. Key Takeaways
Step 01

What Is This Pattern?

The two heaps pattern maintains two heaps that together partition a stream of numbers into a lower half and an upper half. A max-heap stores the smaller half (so we can quickly access the largest of the small numbers), and a min-heap stores the larger half (so we can quickly access the smallest of the large numbers). The median is always at the top of one or both heaps.

The Core Idea

We keep two heaps balanced so their sizes differ by at most 1. The max-heap top and min-heap top are always the two middle elements. The median is either one of them or their average.

MAX-HEAP (lower half)
5
3
1
median = 5
MIN-HEAP (upper half)
8
11
9
Sorted order: [1, 3, 5, 8, 9, 11]. Odd count, so median = max-heap top = 5.
💡

Why two heaps? A single sorted array would need O(n) to insert. A balanced BST would give O(log n) but is complex. Two heaps give us O(log n) insertion and O(1) median retrieval with a simple, elegant invariant: max-heap.size() == min-heap.size() or max-heap.size() == min-heap.size() + 1.

Step 02

When to Use It

Look for these recognition signals in the problem statement. If you spot one, the two heaps pattern is very likely the intended approach.

"Find the median" or "running median"
The classic two heaps problem. Maintain the median as numbers stream in one at a time.
"Balance two groups" or "partition into halves"
Split elements so the largest of the small half and smallest of the large half are accessible in O(1).
"Sliding window median" or "rolling percentile"
Extend the basic pattern with lazy deletion to handle a window that slides across the array.
"Maximize minimum" or "schedule with profit/capital"
Use one heap for available items and another for constraints, greedily selecting the best option.

The key clue: You need to repeatedly access the middle element(s) of a dynamic collection. A brute-force sort after each insertion is O(n log n). Two heaps reduce each insertion to O(log n) and each median query to O(1).

Step 03

Variant 1 — Find Median from Data Stream

LC #295

The most direct application of two heaps. We maintain a max-heap for the lower half and a min-heap for the upper half. After each insertion, we rebalance so the heaps differ in size by at most 1.

Walkthrough: Insert [5, 3, 8, 1, 9]

Watch the two heaps grow and rebalance after each number.

Insert 5
First element. Add to max-heap. Max-heap: [5], Min-heap: []. Median = 5
max
5
min
empty
Insert 3
3 ≤ 5 (max-heap top), so add to max-heap. Max-heap has 2, min-heap has 0 — difference is 2 > 1. Move 5 to min-heap. Median = (3+5)/2 = 4
max
3
min
5
Insert 8
8 > 5 (min-heap top), so add to min-heap. Min-heap has 2, max-heap has 1 — difference is 1, OK. But we prefer max-heap ≥ min-heap, so move 5 to max-heap. Median = 5
max
5
3
min
8
Insert 1
1 ≤ 5 (max-heap top), so add to max-heap. Max-heap has 3, min-heap has 1 — difference is 2 > 1. Move 5 to min-heap. Median = (3+5)/2 = 4
max
3
1
min
5
8
Insert 9
9 > 5 (min-heap top), so add to min-heap. Min-heap has 3, max-heap has 2 — difference is 1, but min > max. Move 5 to max-heap. Max-heap: [5,3,1], Min-heap: [8,9]. Median = 5
max
5
3
1
min
8
9
Step 04

Variant 2 — Sliding Window Median

LC #480

Extend the two heaps pattern with lazy deletion. When the window slides, we add the new element and mark the outgoing element for removal. We only physically remove elements from a heap when they appear at the top. This avoids O(n) removal costs.

Example: nums=[1,3,-1,5,3,6,7], k=3

Sliding window of size 3. For each window, report the median.

Window [1,3,-1]: Sorted: [-1,1,3]. Median = 1
Window [3,-1,5]: Remove 1, add 5. Sorted: [-1,3,5]. Median = 3
Window [-1,5,3]: Remove 3, add 3. Sorted: [-1,3,5]. Median = 3
Window [5,3,6]: Remove -1, add 6. Sorted: [3,5,6]. Median = 5
Window [3,6,7]: Remove 5, add 7. Sorted: [3,6,7]. Median = 6
medians:
1
3
3
5
6
🎯

Lazy deletion trick: Instead of searching the heap for the outgoing element (O(n)), we keep a hash map of "pending deletions." When the top of a heap matches a pending deletion, we pop and decrement the count. This keeps each operation at O(log n) amortized.

Step 05

Template Code

Here is the annotated Java template for the two heaps pattern applied to "Find Median from Data Stream."

MedianFinder

// Two Heaps: Find Median from Data Stream
class MedianFinder {
    // max-heap for the lower half
    PriorityQueue<Integer> lo = new PriorityQueue<>(Collections.reverseOrder());
    // min-heap for the upper half
    PriorityQueue<Integer> hi = new PriorityQueue<>();

    void addNum(int num) {
        lo.offer(num);                   // always add to max-heap first
        hi.offer(lo.poll());              // move max of lower to upper

        if (lo.size() < hi.size()) {     // keep lo.size ≥ hi.size
            lo.offer(hi.poll());
        }
    }

    double findMedian() {
        if (lo.size() > hi.size()) {
            return lo.peek();              // odd count: median is max-heap top
        }
        return (lo.peek() + hi.peek()) / 2.0; // even: average of both tops
    }
}

Rebalancing Logic (Generalized)

// Ensure heaps are balanced after any add/remove
void rebalance() {
    if (lo.size() > hi.size() + 1) {
        hi.offer(lo.poll());     // move max of lower to upper
    } else if (hi.size() > lo.size()) {
        lo.offer(hi.poll());     // move min of upper to lower
    }
}
📝

The "add-then-rebalance" trick: Always insert into the max-heap first, then immediately move its top to the min-heap. Then, if the min-heap is larger, move its top back. This elegant 3-line logic guarantees both the ordering invariant (all elements in lo ≤ all elements in hi) and the size invariant (lo.size ≥ hi.size, difference ≤ 1).

Step 06

Visual Walkthrough

Let us trace through a larger example, showing both heaps and the median at every stage.

Stream: [6, 2, 10, 3, 7, 1]

After each insertion, the median updates.

INSERT MAX-HEAP (lower) MIN-HEAP (upper) MEDIAN
6 [6] [] 6
2 [2] [6] 4
10 [6, 2] [10] 6
3 [3, 2] [6, 10] 4.5
7 [6, 3, 2] [7, 10] 6
1 [3, 2, 1] [6, 7, 10] 4.5
Final sorted order: [1, 2, 3, |, 6, 7, 10]. Even count: median = (3 + 6) / 2 = 4.5
💡

Notice the invariant: After every insertion, every element in the max-heap is ≤ every element in the min-heap. The two heap tops are always the two middle elements of the sorted order. This is maintained automatically by the "add to lo, move top to hi, rebalance" procedure.

Step 07

Common Problems

These are the classic LeetCode problems that use the two heaps pattern, listed in rough order of difficulty.

#295 Find Median from Data Stream Hard
#480 Sliding Window Median Hard
#502 IPO Hard
#436 Find Right Interval Medium
#630 Course Schedule III Hard
#871 Minimum Number of Refueling Stops Hard
💡

Practice tip: Start with #295 (the pure two heaps template). Then tackle #480 which adds the sliding window + lazy deletion complexity. For a different flavor, try #502 (IPO) where the two heaps represent "available projects" and "affordable projects" — the same split-and-pick-best idea applied to a greedy optimization.

Step 08

Interactive Demo

Enter numbers one at a time or provide a list. Watch each number get distributed between the max-heap (lower half) and min-heap (upper half), with the running median displayed after each insertion.

⚙ Two Heaps Median Visualizer


STREAM
MEDIAN
MAX-HEAP
MIN-HEAP
Press "Step →" or "Run All" to begin.
Step 09

Complexity Analysis

Add Number
O(log n)
Find Median
O(1)

Why O(log n) Insertion?

Each insertion involves at most 3 heap operations: one offer to the max-heap, one poll + offer to the min-heap, and potentially one more poll + offer to rebalance. Each heap operation on a heap of size n/2 is O(log n).

Finding the median is O(1) because it only requires peeking at the top of one or both heaps.

Space: Both heaps together store all n elements, so space is O(n).

Sliding Window Variant

For the sliding window median (#480), each window step involves an add and a remove. With lazy deletion, the amortized cost per window step is O(log k) where k is the window size. Total time for an array of length n: O(n log k).

Comparison with alternatives: A sorted array gives O(1) median but O(n) insertion. A balanced BST (like a red-black tree) gives O(log n) for both but is far more complex to implement. Two heaps hit the sweet spot: O(log n) insertion, O(1) median, and the code is simple enough to write in an interview.

Step 10

Key Takeaways

01

Max-heap for lower half, min-heap for upper half. This is the fundamental split. The max-heap top is the largest small element, and the min-heap top is the smallest large element. Together they frame the median.

02

Keep sizes balanced within 1. After every insertion, rebalance so that the max-heap has either the same number of elements as the min-heap (even total) or exactly one more (odd total). This invariant is what makes O(1) median work.

03

The "add, move, rebalance" 3-step is your template. Always add to the max-heap first, move its top to the min-heap, then move back if needed. This 3-line procedure maintains both the ordering and size invariants automatically.

04

Lazy deletion for sliding windows. When elements leave the window, mark them for lazy deletion instead of searching the heap. Clean up lazily when a marked element reaches the top. This keeps the sliding window variant at O(log k) per step.

05

The pattern extends beyond medians. The same two-heap idea applies to problems like IPO (#502), where one heap holds available options and the other holds filtered candidates. Whenever you need to repeatedly pick the best from a dynamically partitioned collection, think two heaps.