Heaps · Heap Fundamentals

Two-Heap Median Maintenance

Splitting a running stream of numbers across a max-heap (the smaller half) and a min-heap (the larger half), kept balanced in size, so the median is always available at one or both roots in O(1).

  • 5 min read
  • 8 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The approach
  3. A second flavor: two heaps as "locked" and "available"
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

Numbers arrive one at a time, and at any moment someone asks for the median. Re-sorting on every question is O(n log n) per query; a sorted list with insertion is O(n) per insert because elements shift. Two heaps give O(log n) inserts and O(1) medians.

This skill covers the two-heap split: a max-heap for the smaller half and a min-heap for the larger half, plus the two invariants that keep the median at their roots. The same two-heap shape also separates "locked" items from "available" ones, as in the IPO problem.

We'll go from simple to more technical:

  1. The pattern: lower half in a max-heap, upper half in a min-heap, rebalanced after each insert.
  2. A second flavor: locked and available items (IPO), with a greedy pick from the available heap.
  3. Where running medians show up (live p50 latency, smoothing, schedulers).
  4. What breaks it: the wrong half, drifting sizes, overflow in the average, deletions.

The approach

Java

PriorityQueue<Integer> lower = new PriorityQueue<>(Comparator.reverseOrder());  // max-heap: smaller half
PriorityQueue<Integer> upper = new PriorityQueue<>();                           // min-heap: larger half

void addNum(int num) {
    lower.offer(num);
    upper.offer(lower.poll());              // the largest of the lower half moves up
    if (upper.size() > lower.size()) {
        lower.offer(upper.poll());          // keep lower equal or one larger
    }
}

double findMedian() {
    if (lower.size() > upper.size()) return lower.peek();
    return ((long) lower.peek() + upper.peek()) / 2.0;   // widen before adding: MAX + MAX overflows an int
}

Python

import heapq


class MedianFinder:
    def __init__(self):
        self.lower = []                          # max-heap of the smaller half, stored NEGATED
        self.upper = []                          # min-heap of the larger half

    def add_num(self, num: int) -> None:
        heapq.heappush(self.lower, -num)
        heapq.heappush(self.upper, -heapq.heappop(self.lower))   # the largest of the lower half moves up
        if len(self.upper) > len(self.lower):
            heapq.heappush(self.lower, -heapq.heappop(self.upper))   # keep lower equal or one larger

    def find_median(self) -> float:
        if len(self.lower) > len(self.upper):
            return -self.lower[0]
        return (-self.lower[0] + self.upper[0]) / 2  # Python ints can't overflow

Two invariants make the roots the median: every value in lower is at most every value in upper, and lower has the same size as upper or one more. Routing each number through lower and passing its maximum up keeps the first invariant without comparing against both roots; the rebalance keeps the second. The average needs care: lower.peek() + upper.peek() adds two Integers as ints, and (MAX_VALUE + MAX_VALUE) / 2.0 comes out as -1.0. Python's ints can't overflow; its trap is the max-heap - heapq has only min-heap functions before 3.14, so lower stores -num and every read negates it back.

A second flavor: two heaps as "locked" and "available"

Two heaps can also separate items by whether they're usable yet. IPO: you may finish at most k projects; each needs capital[i] to start and adds profits[i] when done. Keep projects sorted by capital (the locked ones), move every project you can now afford into a max-heap of profits, and take the most profitable:

Java

int findMaximizedCapital(int k, int w, int[] profits, int[] capital) {
    Integer[] byCapital = /* indices 0..n-1 sorted by capital */;
    PriorityQueue<Integer> affordable = new PriorityQueue<>(Comparator.reverseOrder());
    int next = 0;
    for (int done = 0; done < k; done++) {
        while (next < byCapital.length && capital[byCapital[next]] <= w) affordable.add(profits[byCapital[next++]]);
        if (affordable.isEmpty()) break;    // nothing affordable: capital can't grow
        w += affordable.poll();
    }
    return w;
}

Python

import heapq


def find_maximized_capital(k: int, w: int, profits: list[int], capital: list[int]) -> int:
    by_capital = sorted(zip(capital, profits))
    affordable = []                              # max-heap of profits, stored negated
    nxt = 0
    for _ in range(k):
        while nxt < len(by_capital) and by_capital[nxt][0] <= w:
            heapq.heappush(affordable, -by_capital[nxt][1])
            nxt += 1
        if not affordable:
            break                                # nothing affordable: capital can't grow
        w -= heapq.heappop(affordable)
    return w

Capital never shrinks, so an affordable project stays affordable; taking the most profitable one now is never worse than saving it (an exchange argument). Taking the least profitable affordable project first ends at 7 instead of 106 with capital 1, k = 2 and projects (profit 5, needs 1), (profit 100, needs 6), (profit 1, needs 0).

Where you'll see it

Live dashboards showing the median (p50) latency of a stream, running medians in signal smoothing, and schedulers that hold "not yet eligible" and "ready" jobs in two priority queues (by release time and by priority) all use the two-heap split.

Which tool for which job?

You needUse
The median of a growing streamtwo heaps
Any fixed percentile of a streamtwo heaps, lower sized to about p of the count
The median of a sliding windowtwo heaps with lazy deletion, or two sorted sets
Percentiles over huge streams or many serversa mergeable histogram or sketch
The best item among those unlocked so farsort by unlock cost, plus a max-heap of what's available

Common mistakes

MistakeWhat happensDo this instead
Alternating inserts by size only10, 9, 8, 7, 1 reports 10.0 instead of 8.0insert through lower, move its max up
Sizes allowed to driftthe median formula reads the wrong rootslower equal to upper, or one larger
lower.peek() + upper.peek() in intMAX_VALUE + MAX_VALUE gives -1.0((long) a + b) / 2.0
PriorityQueue.remove(Object) per slideO(k) per removallazy deletion or sorted sets
Forgetting the negation in heapqthe lower half becomes a min-heappush -x, read -lower[0]

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. What two invariants make the heap roots give the median?

Show answer

Every value in the lower max-heap is <= every value in the upper min-heap, and the lower heap has the same size or one more. Then the median is lower's root (odd count) or the average of both roots (even count).

2. Why route every new number through the lower heap and move its maximum up?

Show answer

It keeps the order invariant without comparing the number to both roots: after passing lower's maximum to upper, everything in lower is <= everything in upper. A rebalance then fixes the sizes.

3. Why does (lower.peek() + upper.peek()) / 2.0 fail for large values?

Show answer

The addition happens in int before the division converts to double, so MAX_VALUE + MAX_VALUE wraps and the "median" is -1.0. Widen first: ((long) a + b) / 2.0.

Key takeaways

  • Lower half in a max-heap, upper half in a min-heap; lower's size equals upper's or is one more.
  • Insert through lower, move its max up, rebalance - O(log n); the median is O(1).
  • Widen before averaging the roots.
  • Two heaps also split "locked" from "available" items (IPO).

Quick revise

The whole skill on one page, for the night before an interview.

This page puts the lesson and the deep dive together. Skim it before practice or an interview, and go back to the full lesson for anything you don't recognize.

Pick the tool

You needUse
Stream mediantwo heaps
Stream percentiletwo heaps, lower ~ p of the count
Window medianlazy deletion, or sorted sets
Many serversmergeable histogram or sketch
Best unlocked itemsort by cost, max-heap of available

Snippets worth knowing by heart

Java

lower.offer(num);                         // max-heap
upper.offer(lower.poll());                // its max moves up
if (upper.size() > lower.size()) lower.offer(upper.poll());
double median = lower.size() > upper.size() ? lower.peek() : ((long) lower.peek() + upper.peek()) / 2.0;

Python

heapq.heappush(lower, -num)                            # lower is stored negated
heapq.heappush(upper, -heapq.heappop(lower))
if len(upper) > len(lower):
    heapq.heappush(lower, -heapq.heappop(upper))

The traps

  • Inserting by size only: alternating halves leaves big values in the lower heap. Insert through lower, then move its max up.
  • Sizes drifting: the median formula reads the wrong roots. Keep lower equal to upper, or one larger.
  • int overflow in the average: two MAX_VALUE roots sum to -2. Compute ((long) a + b) / 2.0.
  • O(k) removals from a PriorityQueue: each window slide costs O(k). Use lazy deletion or sorted sets.
  • Lazy deletion balanced by raw sizes: dead entries still count, so the halves drift. Balance by live counts.

Staff-level points, one line each

  • Lazy deletion must balance by live counts; balancing by raw heap sizes failed brute-force checks.
  • Window 1,000 over 100,000 values: lazy heaps peaked at 91,513 entries; rebuilding when dead entries outnumber live ones kept it at 2,003.
  • Two TreeSets of indices ordered by (value, index) give true O(log k) removal.
  • Lower heap sized ceil(p x n) gives the exact nearest-rank percentile.
  • Medians 3 and 6 can combine to 5.5 or 4.5; 2% log buckets merged by adding counts stayed within 1% at p50 to p999.

Say it in an interview

  • "Max-heap for the lower half, min-heap for the upper, sizes equal or lower one bigger."
  • "Each insert is O(log n) and the median is O(1)."
  • "For a window I'd use lazy deletion and balance on live counts."
  • "Across servers I'd use a mergeable sketch, because medians don't combine."

Practice

This skill comes with 8 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Two-Heap Median Maintenance free

Find Median from Data Stream

HardProCoding problemJava · Python

Design a data structure that supports adding integers from a data stream and finding the median of all integers added so far, at any point.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

IPO: Maximize Capital

HardProCoding problemJava · Python

You start with w dollars and can complete at most k projects, one after another. Project i needs at least capital[i] dollars in hand to start (that money isn't spent) and adds profits[i] when it's done.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Two-Heap Median Maintenance free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime