Median of Data Stream Using Two Heaps in DSA Python - Time & Space Complexity
We want to understand how fast we can find the middle value when numbers keep coming in one by one.
How does the time needed change as more numbers arrive?
Analyze the time complexity of the following code snippet.
import heapq
class MedianFinder:
def __init__(self):
self.small = [] # max heap (store negative values)
self.large = [] # min heap
def addNum(self, num: int) -> None:
heapq.heappush(self.small, -num)
if self.large and (-self.small[0]) > self.large[0]:
val = -heapq.heappop(self.small)
heapq.heappush(self.large, val)
if len(self.small) > len(self.large) + 1:
val = -heapq.heappop(self.small)
heapq.heappush(self.large, val)
elif len(self.large) > len(self.small) + 1:
val = heapq.heappop(self.large)
heapq.heappush(self.small, -val)
def findMedian(self) -> float:
if len(self.small) > len(self.large):
return -self.small[0]
elif len(self.large) > len(self.small):
return self.large[0]
else:
return (-self.small[0] + self.large[0]) / 2
This code keeps two heaps to quickly find the median as new numbers come in.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Adding a number involves pushing and possibly popping from heaps.
- How many times: Each addNum call does a few heap operations, each taking time related to heap size.
Each new number causes a few heap operations that depend on the heap size, which grows with the number of inputs.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 heap pushes/pops, each on heaps of size up to 5 |
| 100 | About 100 heap pushes/pops, each on heaps of size up to 50 |
| 1000 | About 1000 heap pushes/pops, each on heaps of size up to 500 |
Pattern observation: Each operation takes longer as heaps grow, but only by a small amount related to the heap height.
Time Complexity: O(log n) per addNum call
This means adding each number takes a small amount of time that grows slowly as more numbers come in.
[X] Wrong: "Adding a number takes constant time because it's just one push."
[OK] Correct: Heaps must keep order, so pushing or popping takes time proportional to the heap height, which grows with input size.
Understanding this helps you handle data that arrives over time and still find middle values quickly, a useful skill in many real tasks.
"What if we used a balanced binary search tree instead of two heaps? How would the time complexity change?"
Practice
Solution
Step 1: Understand the role of two heaps
The max heap stores the smaller half of the numbers, and the min heap stores the larger half.Step 2: Explain median calculation
By balancing these heaps, the median can be found quickly from the top elements of the heaps.Final Answer:
To efficiently keep track of the lower half and upper half of the numbers for median calculation -> Option AQuick Check:
Two heaps split data halves = efficient median [OK]
- Thinking heaps sort all data fully
- Using one heap only
- Confusing heaps with arrays
Solution
Step 1: Understand heap initialization in Python
Python's heapq module only supports min heaps, so to simulate a max heap, we store negative values in a list.Step 2: Check code correctness
max_heap = [] # will store negative values for max heap behavior min_heap = [] correctly initializes empty lists for both heaps; max heap will store negative values to simulate max heap behavior.Final Answer:
max_heap = [] # will store negative values for max heap behavior min_heap = [] -> Option CQuick Check:
Empty lists + negative values = max heap simulation [OK]
- Calling heapq.heapify on empty list returns None
- Trying to create max heap directly
- Confusing heapq.heapify return value
import heapq
class MedianFinder:
def __init__(self):
self.max_heap = []
self.min_heap = []
def add_num(self, num):
if not self.max_heap or num <= -self.max_heap[0]:
heapq.heappush(self.max_heap, -num)
else:
heapq.heappush(self.min_heap, num)
if len(self.max_heap) > len(self.min_heap) + 1:
heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))
elif len(self.min_heap) > len(self.max_heap):
heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))
def get_median(self):
if len(self.max_heap) > len(self.min_heap):
return float(-self.max_heap[0])
else:
return (-self.max_heap[0] + self.min_heap[0]) / 2
finder = MedianFinder()
for number in [5, 15, 1, 3]:
finder.add_num(number)
print(finder.get_median())Solution
Step 1: Insert numbers and balance heaps
Insert 5: max_heap=[-5], min_heap=[]; median=5 Insert 15: max_heap=[-5], min_heap=[15]; median=(5+15)/2=10 Insert 1: max_heap=[-5,-1], min_heap=[15]; median=5 Insert 3: max_heap=[-3,-1], min_heap=[5,15]; median=(3+5)/2=4.0Step 2: Calculate final median
After all insertions, median is 4.0 as heaps are balanced and median is average of tops.Final Answer:
4.0 -> Option DQuick Check:
Median after 4 inserts = 4.0 [OK]
- Not balancing heaps after insertion
- Forgetting to invert max heap values
- Calculating median incorrectly for even count
def add_num(self, num):
if not self.max_heap or num < -self.max_heap[0]:
heapq.heappush(self.max_heap, -num)
else:
heapq.heappush(self.min_heap, num)
if len(self.max_heap) > len(self.min_heap):
heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))
elif len(self.min_heap) > len(self.max_heap) + 1:
heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))Solution
Step 1: Analyze balancing conditions
The code moves elements when max_heap size > min_heap size, but it should allow max_heap to be larger by at most 1.Step 2: Identify correct balancing logic
Correct logic: if max_heap size > min_heap size + 1, move element to min_heap; if min_heap size > max_heap size, move element to max_heap.Final Answer:
The balancing conditions are reversed; max_heap can grow too large -> Option AQuick Check:
Balance heaps with correct size difference [OK]
- Swapping balancing conditions
- Not allowing max_heap to be bigger by one
- Incorrect comparison operators
Solution
Step 1: Understand removal challenges with heaps
Heaps do not support efficient arbitrary removal; rebuilding heaps after each removal is costly.Step 2: Identify suitable data structure
Balanced binary search trees support insertion, removal, and median retrieval in O(log n) time.Step 3: Compare other options
Sorted lists have O(n) removal; hash maps do not maintain order; rebuilding heaps is inefficient.Final Answer:
Use balanced binary search trees (like AVL or Red-Black trees) instead of heaps -> Option BQuick Check:
Balanced BSTs support fast insert/remove/median [OK]
- Assuming heaps support fast removal
- Using sorted lists with slow removal
- Ignoring order maintenance
Ready to prove it?
Pass the test and this pattern moves unknown
