In our previous lessons, we've explored how heaps can efficiently solve problems involving the "top K" elements and merging k sorted streams. We saw that a single heap excels at maintaining a small, ordered subset of a larger collection. Now, we'll expand on this by using two heaps in tandem to solve a classic and highly relevant problem for real-time systems.
This lesson introduces the Two Heaps pattern. You will learn how to use a max-heap and a min-heap together to efficiently partition a set of numbers and find the median of a data stream. This is a common requirement in monitoring and analytics systems, where you might need to calculate the median latency of API requests or the median transaction value in real-time, without storing and re-sorting data constantly. This pattern is a staple of technical interviews for roles in scalable systems engineering.
1. The Challenge: Finding the Median in a Stream
Imagine you are responsible for a service that processes thousands of transactions per second. A key health metric is the median transaction processing time, updated every few seconds. How would you design a system to calculate this efficiently?
The problem, formally known as "Find Median from Data Stream," asks us to design a data structure with two primary operations:
addNum(num): Adds a new number from a stream to our data structure.findMedian(): Returns the median of all numbers added so far.
The median is the middle value in a sorted list. If the list has an odd number of elements, it's the single middle element. If it's even, it's the average of the two middle elements.
A naive approach would be to keep all numbers in a list, and for every findMedian() call, sort the list and find the middle. This is extremely inefficient. A slightly better approach is to keep the list sorted upon insertion, but this still requires shifting elements, making each addNum operation slow, taking time.
The following video introduces the problem and discusses this simple, but inefficient, brute-force solution, setting the stage for a more optimized approach.
Find Median from Data Stream - Heap & Priority Queue - Leetcode 295
This video from NeetCode clearly defines the 'Find Median from Data Stream' problem and analyzes the naive approach.
Watch the initial segment from the introduction to the problem and the explanation of the sorted-array method. Pay attention to why the addNum operation becomes the bottleneck with a time complexity of O(N).
2. The Two Heaps Insight
The key realization is that to find the median, we don't need the entire collection to be sorted. We only need to know the number(s) at the boundary between the smaller half and the larger half of the data.
This suggests we can partition the numbers into two "buckets":
- A "lowers" bucket containing the smaller half of the numbers.
- A "highers" bucket containing the larger half of the numbers.
To find the median, we would then need the largest number from the "lowers" bucket and the smallest number from the "highers" bucket. This is a perfect use case for heaps:
- We can use a Max-Heap to represent the "lowers" bucket, allowing us to find its largest element in time.
- We can use a Min-Heap to represent the "highers" bucket, allowing us to find its smallest element in time.
This structure is maintained by two strict rules:
- Ordering Invariant: Every number in the max-heap (lowers) is less than or equal to every number in the min-heap (highers).
- Size Invariant: The heaps must be kept balanced, with their sizes being either equal or differing by at most one.
The video below gives an excellent conceptual overview of this "two buckets" idea and why heaps are the ideal data structure for implementing it.
Data Structures: Solve 'Find the Running Median' Using Heaps
This video from HackerRank explains the core concept of the Two Heaps pattern for the running median problem.
Watch the segments that introduce the continuous median problem and then explain the two-bucket approach using heaps.
3. The Algorithm in Action
With our two heaps set up, let's define how the addNum and findMedian operations work while preserving our two invariants.
The addNum(num) Operation
When a new number arrives, we must add it to one of the heaps and then rebalance them to maintain the invariants. A robust way to do this is:
- Add to a Heap: Always add the new number to the max-heap (the "lowers").
- Maintain Order: The largest element in the max-heap might now be larger than the smallest element in the min-heap, violating the ordering invariant. To fix this, we pop the largest element from the max-heap and push it onto the min-heap.
- Maintain Balance: After the previous step, the min-heap might have grown too large, violating the size invariant. If the min-heap has more than one element more than the max-heap, we pop the smallest element from the min-heap and push it onto the max-heap.
This sequence ensures that both the ordering and size invariants are always restored after adding a new number.
The following resource provides a clear, written explanation of this logic.
295. Find Median from Data Stream - In-Depth Explanation
This Algo.monster article gives a detailed breakdown of the intuition and solution approach.
Read the sections on Intuition and Solution Approach. This text will solidify your understanding of the invariants and the rebalancing logic.
Let's visualize the process. The image below shows a snapshot of adding the number 7 to our heaps.

The most crucial part of this is the rebalancing step, which ensures the heaps don't drift apart in size.

The findMedian() Operation
This part is simple and efficient. Thanks to our invariants, calculating the median just involves looking at the tops of the heaps:
- If the heap sizes are equal, it means we have an even number of elements. The median is the average of the top of the max-heap and the top of the min-heap.
- If the heap sizes are unequal, one heap will have exactly one more element. The median is simply the top of the larger heap.
The following video segment provides a full walkthrough of adding several numbers, showing the rebalancing logic and median calculation at each step.
Find Median from Data Stream - Heap & Priority Queue - Leetcode 295
The same NeetCode video now explains the Two Heaps solution in detail.
Watch from the explanation of the two-heaps approach, through the step-by-step example, and concluding with the median retrieval logic. This detailed walkthrough will help you trace the state of the heaps as numbers are added.
4. Implementation and Complexity
Now let's turn to implementation. As you're proficient in Go and JavaScript, we can look at how this pattern is realized in both languages.
A common challenge is that many standard libraries provide a min-heap but not a max-heap. This is easily solved by negating numbers before pushing them to the heap that we want to act as a max-heap. When you pop a value, you negate it again to get the original number.
The following resource provides full implementations in several languages, including JavaScript, and also covers complexity and common pitfalls.
295. Find Median from Data Stream - In-Depth Explanation
This article from Algo.monster provides code examples and discusses practical implementation issues.
Review the Solution Implementation section, focusing on the JavaScript or Python examples. Then, read the analysis of Time and Space Complexity and the list of Common Pitfalls, which are invaluable for avoiding bugs.
For a Go-specific implementation, the following resource tackles the equivalent "Prefix Medians" problem. Note that its definition of the median for an even-sized array is the lower of the two middle elements, a common variation in competitive programming. The underlying Two Heaps logic is identical.
Finding Prefix Medians Using Heaps in Go | CodeSignal Learn
This CodeSignal article demonstrates how to implement the Two Heaps pattern in Go using the container/heap package.
Read the introduction to the problem and heaps in Go, and then study the implementation logic. This will show you how to apply the pattern in a language you're comfortable with.
Complexity Analysis
The efficiency of the Two Heaps pattern is what makes it so powerful:
-
Time Complexity:
addNum(num): . Each operation involves at most a few heap pushes and pops, which take logarithmic time.findMedian(): . This operation only requires peeking at the top elements of the heaps.
-
Space Complexity: . We need to store all the numbers that have been added to the stream.
This is a massive improvement over the naive sorting-based approaches and is ideal for the high-throughput, low-latency requirements of scalable systems.
Conclusion
In this lesson, we dissected the Two Heaps pattern, a clever technique for maintaining a partitioned dataset to find a running median. By balancing a max-heap for the smaller half of numbers and a min-heap for the larger half, we can achieve highly efficient addNum and findMedian operations.
Key Takeaways:
- Problem: Efficiently find the median from a continuous stream of data.
- Pattern: Use two heaps—a max-heap for the "lowers" and a min-heap for the "highers".
- Invariants: The pattern relies on maintaining an ordering invariant (max of lowers ≤ min of highers) and a size invariant (heap sizes differ by at most one).
- Complexity: Achieves for adding numbers and for finding the median, making it highly suitable for real-time applications.
- Application: A core pattern for real-time analytics, monitoring systems, and a frequent topic in senior-level technical interviews.
In our next lesson, we will shift from heap-based patterns to advanced searching techniques. We'll start by exploring Modified Binary Search, learning how to apply it to problems involving rotated or non-traditionally sorted arrays.