-
Notifications
You must be signed in to change notification settings - Fork 273
Running Median
TIP103 Unit 12 Session 2 (Click for link to problem statements)
A monitoring service streams sensor values one at a time and must report the median of everything seen so far at any moment.
Implement a class that supports adding a number and querying the current median.
class MedianFinder:
def __init__(self):
pass
def add_num(self, num):
pass
def find_median(self):
pass- 💡 Difficulty: Hard
- ⏰ Time to complete: 30-40 mins
- 🛠️ Topics: Heaps (Priority Queues), Two Heaps Pattern, Data Streams
Understand what the interviewer is asking for by using test cases and questions about the problem.
- Established a set (2-3) of test cases to verify their own solution later.
- Established a set (1-2) of edge cases to verify their solution handles complexities.
- Have fully understood the problem and have no clarifying questions.
- Have you verified any Time/Space Constraints for this problem?
- Q: What is the median when an even number of values has been seen?
- A: The average of the two middle values in sorted order. With an odd count, it is the single middle value.
- Q: Should
find_median()return a float even when the count is odd?- A: Yes. The example prints
2.0after three values, so the median is always reported as a float.
- A: Yes. The example prints
- Q: Can
add_num()andfind_median()calls be interleaved in any order?- A: Yes. Values stream in one at a time, and the current median may be queried at any moment, possibly many times.
HAPPY CASE
Input: add_num(1), add_num(2), find_median(), add_num(3), find_median()
Output: 1.5, then 2.0
Explanation: After [1, 2] the two middle values are 1 and 2, so the median is (1 + 2) / 2 = 1.5. After [1, 2, 3] the middle value is 2, reported as 2.0.
EDGE CASE
Input: add_num(7), find_median()
Output: 7.0
Explanation: With a single value, that value is the median.
Input: add_num(5), add_num(5), add_num(-1), add_num(-3), find_median()
Output: 2.0
Explanation: Duplicates and negative values arriving in decreasing order still sort to [-3, -1, 5, 5], so the median is (-1 + 5) / 2 = 2.0.
Match what this problem looks like to known categories of problems, e.g. Linked List or Dynamic Programming, and strategies or patterns in those categories.
For Streaming/Data Stream Problems that ask for an order statistic, we can consider the following approaches:
- Two Heaps Pattern: Maintain a max-heap for the lower half of the values and a min-heap for the upper half. The median is always available from the heap tops.
-
Sorted List with Binary Insertion: Keep all values sorted and insert each new value in place; simpler to reason about, but each insertion costs
O(N)for the shift. - Re-sorting on every query: Works, but sorting the entire stream each time is far too slow for repeated queries.
Plan the solution with appropriate visualizations and pseudocode.
General Idea:
Split the values seen so far into two halves. A max-heap small holds the lower half, and a min-heap large holds the upper half. Keep them balanced so that small has either the same number of elements as large or exactly one more. Then the median is either the top of small (odd count) or the average of the two tops (even count). Python's heapq is a min-heap, so small stores negated values to simulate a max-heap.
1) Initialize two empty heaps: `small` (max-heap via negation) and `large` (min-heap).
2) add_num(num):
a) Push `num` onto `small`.
b) Pop the largest value from `small` and push it onto `large`. This guarantees every value in `small` <= every value in `large`.
c) If `large` now has more elements than `small`, move the smallest value of `large` back to `small`.
3) find_median():
a) If `small` has more elements, return the top of `small` as a float.
b) Otherwise, return the average of the top of `small` and the top of `large`.
- Forgetting to negate values when pushing to or popping from the max-heap, corrupting the ordering.
- Pushing directly onto whichever heap is smaller without routing through the other heap first, which can leave a lower-half value stranded above an upper-half value.
- Letting the heap sizes drift more than one apart, so the heap tops no longer straddle the median.
- Returning an integer for odd counts instead of a float (the expected output is
2.0, not2).
Implement the code to solve the algorithm.
import heapq
class MedianFinder:
def __init__(self):
self.small = [] # max-heap via negation: lower half of the numbers
self.large = [] # min-heap: upper half of the numbers
def add_num(self, num):
# Push onto the lower half, then move its largest value to the upper half
heapq.heappush(self.small, -num)
heapq.heappush(self.large, -heapq.heappop(self.small))
# Rebalance so small always holds the extra element on odd counts
if len(self.large) > len(self.small):
heapq.heappush(self.small, -heapq.heappop(self.large))
def find_median(self):
if len(self.small) > len(self.large):
return float(-self.small[0])
return (-self.small[0] + self.large[0]) / 2Review the code by running specific example(s) and recording values (watchlist) of your code's variables along the way.
-
Input:
add_num(1),add_num(2),find_median(),add_num(3),find_median()-
add_num(1): 1 passes throughlargeand is rebalanced back, sosmall = {1},large = {}. -
add_num(2): 2 is the current max ofsmall, so it moves tolarge:small = {1},large = {2}. -
find_median(): even count, average of tops = (1 + 2) / 2 = 1.5 ✓ -
add_num(3): 3 moves tolarge, thenlargeis one too big, so 2 moves back:small = {1, 2},large = {3}. -
find_median(): odd count, top ofsmall= 2.0 ✓
-
-
Input:
add_num(7),find_median()-
small = {7},large = {}; odd count, so the median is 7.0 ✓
-
Evaluate the performance of your algorithm and state any strong/weak or future potential work.
Assume N is the number of values added to the stream so far.
-
Time Complexity:
O(log N)peradd_numcall, since each of the constant number of heap pushes/pops costsO(log N).find_medianisO(1)because it only peeks at the heap tops. -
Space Complexity:
O(N)to store every value across the two heaps.