Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Maintain two heaps that split the numbers in half: a max-heap for the lower half and a min-heap for the upper half. Keep their sizes balanced (differ by at most one). The median is the top of the larger heap, or the average of both tops when sizes are equal. addNum is O(log n); findMedian is O(1).
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you can design a data structure (not just an algorithm) that supports two operations efficiently, and whether you can state and preserve invariants across mutations.
Where candidates slip
Re-sorting or re-scanning the whole collection on each findMedian (O(n log n) or O(n) per query), or letting the two heaps drift out of balance so the median read is wrong.
How to stand out
State the two invariants (order and balance) explicitly, show the push-transfer-rebalance sequence preserves both, and discuss follow-ups: bounded range → counting buckets, and the 99% in [0,100] variant.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewMedian of a growing streamStatic data or a stream?StaticStreamExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Sorted Insertion (Baseline)
1Hint 1Explore hintHide hint
If you kept the numbers sorted at all times, where would the median always be?
2Hint 2Explore hintHide hint
Insert each new number into its correct position in a sorted list (binary search to find the spot).
3Hint 3Explore hintHide hint
Finding the spot is O(log n), but shifting elements to insert is O(n) — that's the bottleneck for a long stream.
A · Core idea
Keep an array sorted as numbers arrive. To add, binary-search for the insertion index and splice the value in. The median is then trivially the middle index (odd count) or the average of the two middle indices (even count).
Why it's tempting. findMedian becomes a single index calculation — O(1). For a few inserts followed by many median queries, it's even reasonable.
Why it fails at scale. The insertion itself shifts elements: adding to the front of a 50,000-element array moves all of them. Across n adds that's O(n²) overall — too slow for the call volume. We need an insert that's logarithmic, which points to heaps.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
add 2, add 1, add 3, findMedianadd 2
insert → [2]
median 22.0C · Code with teaching comments
Reference implementation previewclass MedianFinder { private List<Integer> data = new ArrayList<>(); public void addNum(int num) { int lo = 0, hi = data.size();Explore the full codeHide the full code
D · Why it works and complexityO(n) per add, O(1) median timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
Keeping data fully sorted is overkill: the median only needs the boundary between the lower and upper halves, not a total order. Tracking just that boundary is what makes the two-heap solution fast.
ComplexityO(n) per add, O(1) median time, O(n) space — Binary search locates the insertion point in O(log n), but array insertion shifts up to n elements → O(n) per add. The median is then a direct index lookup, O(1).
E · Edge cases and pitfalls2Assuming list insertion is cheap — it shifts O(n) elements.Explore edge casesHide edge cases
Pitfalls
- Assuming list insertion is cheap — it shifts O(n) elements.
- Integer division when averaging the two middles — divide by 2.0, not 2.
Approach 2 of 2 · Optimal
Two Heaps — Max-Heap Low Half, Min-Heap High Half (Optimal)
1Hint 1Explore hintHide hint
The median only depends on the boundary between the smaller half and the larger half. What if you stored those two halves separately?
2Hint 2Explore hintHide hint
Keep a max-heap of the lower half (so its top is the largest small value) and a min-heap of the upper half (so its top is the smallest large value). The median lives at those two tops.
3Hint 3Explore hintHide hint
On each insert, push, then move the boundary element across so every low ≤ every high, then rebalance sizes so they differ by at most one. Median = the larger heap's top, or the average of both tops.
A · Core idea
Picture the sorted stream cut in two. The lower half lives in a max-heap low (top = the largest of the small numbers). The upper half lives in a min-heap high (top = the smallest of the big numbers). The median is precisely at this seam — that's the entire idea.
Two invariants do all the work.
- Order: every value in
low≤ every value inhigh. This guarantees the two tops straddle the true middle. - Balance: the heaps' sizes differ by at most one, and we let
lowbe the one allowed to hold the extra element on odd counts.
The insert dance — push, transfer, rebalance. Always push the new number onto low first. Then pop low's top and push it onto high. That transfer guarantees the order invariant: whatever the largest of the lower half was, it now sits in the upper half if it belongs there. But the transfer may leave high bigger than low, breaking balance — so if high is larger, move its top back to low. After this fixed sequence both invariants hold again.
Reading the median. If low has one more element than high (odd total), the median is low's top. If the sizes are equal (even total), the median is the average of the two tops. Both are O(1).
Python max-heap trick. heapq only offers a min-heap. To make low behave as a max-heap, push negated values; the smallest negative corresponds to the largest original value. Negate again when you read it back. Forgetting a negation is the #1 bug here.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
add 1, add 2, findMedian, add 3, findMedianArray state
after add 1
low=[1] high=[]
1.5 then 2.0C · Code with teaching comments
D · Why it works and complexityO(log n) addNum, O(1) findMedian timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
The transfer-then-rebalance pattern is a tiny, branch-free way to maintain a sorted partition under insertion. By always routing the newcomer through low and shipping the boundary across, you never have to compare the new value against the heap tops yourself — the heaps' own ordering decides where it lands.
ComplexityO(log n) addNum, O(1) findMedian time, O(n) space — Each addNum does a constant number of heap pushes/pops, each O(log n). findMedian just reads one or two heap tops — O(1). Both heaps together hold all n elements → O(n) space.
E · Edge cases and pitfalls6single addNum(5) — Push 5→low, move to high, high bigger → move back; low=[5], high=[]. findMedian returns 5.0.Explore edge casesHide edge cases
Edge cases handled
single addNum(5)Push 5→low, move to high, high bigger → move back; low=[5], high=[]. findMedian returns 5.0.duplicates: add 2, add 2low=[2], high=[2]; median (2+2)/2 = 2.0. Duplicates split across the seam fine.strictly decreasing inputEach smaller value enters low and the transfer keeps order; balance logic still yields the correct middle.
Pitfalls
- Forgetting to negate when reading low's top in Python — returns the wrong sign/value.
- Comparing the new number to the heap tops manually instead of using push-transfer-rebalance — error-prone branching.
- Letting balance drift (rebalancing on the wrong side) so the larger heap isn't consistently `low` — median read picks the wrong top.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewclass MedianFinder { // low: max-heap of the smaller half. high: min-heap of the larger half. private PriorityQueue<Integer> low = new PriorityQueue<>(Collections.reverseOrder()); private PriorityQueue<Integer> high = new PriorityQueue<>();Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Forgetting the max-heap negation in Python
heapq is a min-heap. Without negating, `low` keeps its smallest on top, so `low[0]` is not the largest of the lower half and the median read is wrong.
Explore the code comparisonHide the code comparison
Forgetting the max-heap negation in Python
heapq is a min-heap. Without negating, `low` keeps its smallest on top, so `low[0]` is not the largest of the lower half and the median read is wrong.
Don't write this
heapq.heappush(self.low, num) # min-heap, wrong end on top
heapq.heappush(self.high, heapq.heappop(self.low))
# findMedian reads self.low[0] expecting the MAX — it's the minWrite this instead
heapq.heappush(self.low, -num) # negate → behaves as max-heap
heapq.heappush(self.high, -heapq.heappop(self.low))
# read -self.low[0] to recover the true max2Rebalancing on the wrong side
If you let `high` be the heap that may hold the extra element but read the median from `low`, odd counts return the wrong value. Pick one heap (here `low`) to own the surplus and be consistent.
Explore the code comparisonHide the code comparison
Rebalancing on the wrong side
If you let `high` be the heap that may hold the extra element but read the median from `low`, odd counts return the wrong value. Pick one heap (here `low`) to own the surplus and be consistent.
Don't write this
// surplus goes to high, but findMedian reads low
if (low.size() > high.size()) high.offer(low.poll());
// ...
if (low.size() != high.size()) return low.peek();Write this instead
// surplus always goes to low; read low when sizes differ
low.offer(num);
high.offer(low.poll());
if (high.size() > low.size()) low.offer(high.poll());
// findMedian: if (low.size() > high.size()) return low.peek();Revise
Carry the pattern into the interview
Pattern to remember
Two balanced heaps (max-heap low half, min-heap high half)
Code pattern to memorize
addNum(x):
push x onto low (max-heap)
move low.top -> high # ensure every low ≤ every high
if len(high) > len(low): # rebalance sizes
move high.top -> low
findMedian():
if len(low) > len(high): return low.top
return (low.top + high.top) / 2Rules to remember
- 1
lowis a max-heap (its top is the largest of the small half);highis a min-heap (its top is the smallest of the big half). - 2Invariant 1 — order: every element in
low≤ every element inhigh. - 3Invariant 2 — balance:
0 ≤ len(low) − len(high) ≤ 1. Keeplowthe one that may be larger by one. - 4Python has only a min-heap — simulate a max-heap by negating values pushed to
low. - 5The 'push then transfer then rebalance' dance enforces BOTH invariants on every insert without sorting.
Use this pattern when
- Running median / running percentile over a stream of numbers
- You need the middle element repeatedly as data arrives
- Any 'maintain two halves and query the boundary' design problem
- Online algorithm where re-sorting on every query is too slow
Choose another pattern when
- Data is static and given upfront → just sort once, O(n log n)
- Values are bounded small integers → a counting/Fenwick approach can give O(1) updates
- You only need the median once → nth_element / quickselect is O(n)
Interview takeaway
Two heaps facing each other turn 'find the middle of a growing set' into O(log n) inserts and O(1) median reads. Whenever a problem needs the boundary between the smaller and larger halves of streaming data, reach for a max-heap/min-heap pair.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternTwo balanced heaps maintaining a median boundary. The signature design pattern for running median/percentile over a stream. For bounded values switch to a counting array; for windows add removal support (Sliding Window Median).
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Sliding Window MedianComing soonSame two heaps, but support removal as the window slides — use lazy deletion or a TreeMap/multiset.
- Kth Largest Element in an ArrayComing soonSingle size-k heap — a simpler cousin of the boundary-heap idea.
- Kth Largest Element in a StreamComing soonMaintain a min-heap of size k for the streaming Kth largest — design-problem sibling.
- Median of Two Sorted ArraysComing soonStatic median via binary search on the partition — the offline counterpart.