Hello! Welcome to the next lesson in our journey through Python for competitive programming.
In our last lesson, we saw how a single min-heap could be cleverly used to solve "Top-K" problems by efficiently keeping track of the k largest elements in a collection. This taught us that heaps are powerful tools for maintaining an ordered subset of data.
Today, we're going to level up that concept. We will tackle another classic problem: finding the median of a data stream. This problem requires us to maintain the median of a list of numbers that grows over time. By the end of this lesson, you will be able to design and implement an efficient solution for this using two heaps, a fundamental and elegant pattern in algorithmic problem-solving.
1. The Challenge: Median of a Data Stream
Imagine you are receiving a continuous stream of numbers, one by one. At any point, someone can ask you for the median of all the numbers you've seen so far.
- The median is the middle value in a sorted list.
- If the list has an odd number of elements, the median is the single middle element. (e.g., in
[1, 2, 5, 8, 11], the median is 5). - If the list has an even number of elements, the median is the average of the two middle elements. (e.g., in
[1, 2, 5, 8], the median is(2 + 5) / 2 = 3.5).
A naive approach would be to store all numbers in a list, and every time findMedian() is called, sort the list and find the middle. With N numbers, sorting takes O(N log N) time, which is too slow for a real-time stream. We need a more efficient way to add numbers and find the median.
2. The Two-Heap Strategy
The key insight is that we don't need the entire list to be sorted. We only ever need access to the middle one or two elements. To get these, we can divide the stream of numbers into two halves:
- A "small" half, containing all the numbers less than or equal to the median.
- A "large" half, containing all the numbers greater than or equal to the median.

How can we efficiently manage these two halves? We need a data structure that can give us the "boundary" elements quickly:
- For the small half, we need its largest element. A max-heap is perfect for this.
- For the large half, we need its smallest element. A min-heap is perfect for this.
So, our strategy is to use two heaps:
- A max-heap (
small_half) to store the smaller half of the numbers. - A min-heap (
large_half) to store the larger half of the numbers.
With this setup, finding the median becomes an O(1) operation:
- If the total number of elements is odd, one heap will have one more element than the other. The median is simply the top of the larger heap.
- If the total number of elements is even, the heaps will be the same size. The median is the average of the top of the max-heap and the top of the min-heap.
The real challenge is in the addNum method: how do we add a new number while keeping the heaps balanced and ensuring that every number in small_half is less than or equal to every number in large_half?
To see a detailed walkthrough of this logic, let's watch a video that breaks down the problem and the two-heap solution.
Find Median from Data Stream - Heap & Priority Queue - Leetcode 295
This excellent video from NeetCode explains the problem from the ground up. It starts with the naive solution, builds the intuition for the two-heap approach, and walks through the balancing logic step-by-step.
Please watch from the beginning up to 15:05. This covers the core concepts: 00:00 - 04:20: Understanding the problem and the inefficiency of a sorted list. 04:20 - 09:34: The core idea of using two heaps (small and large) and how they help find the median. 09:34 - 15:05: The crucial part—a step-by-step example of adding numbers and rebalancing the heaps to maintain the necessary properties.
3. Implementation in Python
As you saw in the video, the logic for addNum involves adding a new number and then potentially moving an element from one heap to the other to maintain two key invariants:
- The Size Invariant: The heaps' sizes must differ by at most 1.
- The Value Invariant: Every element in the
small_half(max-heap) must be less than or equal to every element in thelarge_half(min-heap).
Now, let's look at how this is implemented in Python. A key detail is that Python's heapq module only provides a min-heap. To simulate a max-heap for our small_half, we use a simple but powerful trick: we store the negative of the numbers. This way, the smallest value in this heap (the root) corresponds to the largest actual number.
Find Median from Data Stream - Heap & Priority Queue - Leetcode 295
Let's continue with the same video to see the full Python implementation. Pay close attention to how the negative numbers are used for the max-heap.
Watch from 15:46 to the end (23:27). This part translates the logic into Python code, demonstrating the addNum and findMedian methods. Notice when values are negated before pushing and after popping from the 'small' heap.
Here is a clean implementation you can study, based on the logic from the video.
295. Find Median From Data Stream - Solution & Explanation
This article from NeetCode provides the complete Python solution. It serves as a great reference to solidify your understanding of the code structure.
Review the Python code under the 'Heap' section. Compare it with the video's implementation and make sure you understand each line, especially the handling of negative values for the max-heap.
Let's break down the logic from the code you just reviewed.
import heapq
class MedianFinder:
def __init__(self):
# self.small is a max-heap, storing the smaller half of numbers.
# We store negative values to simulate a max-heap with Python's min-heap.
self.small = []
# self.large is a min-heap, storing the larger half of numbers.
self.large = []
def addNum(self, num: int) -> None:
# 1. Add to max-heap (small half) by default.
heapq.heappush(self.small, -1 * num)
# 2. Check if the value invariant is violated.
# If the largest element in `small` is > the smallest in `large`.
if (self.small and self.large and
(-1 * self.small[0]) > self.large[0]):
val = -1 * heapq.heappop(self.small)
heapq.heappush(self.large, val)
# 3. Rebalance the sizes if needed.
# If `small` has more than one extra element.
if len(self.small) > len(self.large) + 1:
val = -1 * heapq.heappop(self.small)
heapq.heappush(self.large, val)
# If `large` has more than one extra element.
if len(self.large) > len(self.small) + 1:
val = heapq.heappop(self.large)
heapq.heappush(self.small, -1 * val)
def findMedian(self) -> float:
# If `small` has more elements, the median is its largest element.
if len(self.small) > len(self.large):
return -1 * self.small[0]
# If `large` has more elements, the median is its smallest element.
elif len(self.large) > len(self.small):
return self.large[0]
# If sizes are equal, median is the average of the two tops.
else:
return (-1 * self.small[0] + self.large[0]) / 2.0
Test your understanding!
Let's trace the state of the two heaps (small and large) as we call addNum for the sequence [5, 3, 8, 2]. What is the state of the heaps after adding 2, and what is the median at that point?
- Remember:
smallis a max-heap (stores negatives),largeis a min-heap. - Follow the logic: push to
small, check value invariant, then rebalance sizes.
Show answer
-
addNum(5):- Push
-5tosmall.small = [-5],large = []. - Sizes are unbalanced (
smallhas 1,largehas 0). This is acceptable (diff <= 1). - Heaps:
small = [-5],large = []. Median = 5.
- Push
-
addNum(3):- Push
-3tosmall.small = [-5, -3]. - Value invariant check:
smalltop is-5(value 5), nolargeto compare. - Rebalance sizes:
smallhas 2,largehas 0. Difference is > 1. - Pop
-5fromsmall(value 5), push5tolarge. - Heaps:
small = [-3],large = [5]. Median =(3 + 5) / 2 = 4.0.
- Push
-
addNum(8):- Push
-8tosmall.small = [-8, -3]. - Value invariant check:
smalltop is-8(value 8),largetop is5. Invariant violated (8 > 5). - Pop
-8fromsmall(value 8), push8tolarge. - Heaps after value fix:
small = [-3],large = [5, 8]. - Rebalance sizes:
smallhas 1,largehas 2. Difference is 1. This is acceptable. - Heaps:
small = [-3],large = [5, 8]. Median = 5.
- Push
-
addNum(2):- Push
-2tosmall.small = [-3, -2]. - Value invariant check:
smalltop is-3(value 3),largetop is5. Invariant holds (3 <= 5). - Rebalance sizes:
smallhas 2,largehas 2. Sizes are equal. This is acceptable. - Final Heaps:
small = [-3, -2],large = [5, 8]. - Median: Sizes are equal, so average of tops:
(-1 * -3 + 5) / 2 = (3 + 5) / 2 = 4.0.
- Push
4. Complexity and Pitfalls
- Time Complexity:
addNum: O(log N). Each call involves at most a few heap pushes and pops, each taking logarithmic time.findMedian: O(1). We just look at the top element(s) of the heap(s).
- Space Complexity: O(N). We need to store all
Nnumbers in the two heaps.
This is a highly efficient solution that meets the demands of competitive programming challenges. However, there are common mistakes to watch out for.
295. Find Median From Data Stream - Solution & Explanation
To make sure your implementation is robust, it's helpful to be aware of common errors. This section of the NeetCode article highlights several key pitfalls.
Read the 'Common Pitfalls' section. Pay special attention to 'Using Wrong Heap Types', 'Failing to Maintain Heap Balance', and 'Incorrect Median Calculation for Even Count'.
Conclusion
Congratulations! You've just learned a powerful and non-obvious technique for a common streaming problem. By combining two heaps, we created a data structure that can maintain the median of a dynamic dataset with very impressive time complexity.
Key Takeaways:
- Finding the median in a data stream can be solved efficiently using a max-heap for the smaller half of the numbers and a min-heap for the larger half.
- This two-heap structure allows for
findMedianin O(1) time. - The
addNumoperation takes O(log N) time, as it requires heap insertions/deletions to maintain the size and value invariants. - In Python, a max-heap can be simulated using the
heapq(min-heap) module by storing negated values.
Up Next:
We will continue exploring the power of heaps in our next lesson, where we'll tackle another interesting problem: "Merge K sorted arrays efficiently using a heap". This will further demonstrate how heaps can be used to manage and merge multiple sources of ordered data.