How do you find the median of a stream of numbers as they arrive?
Split the numbers into two halves with two heaps: a max heap holding the smaller half and a min heap holding the larger half. Keep the sizes balanced so that the max heap holds either the same number of elements as the min heap or exactly one more.
With that invariant, the median is the root of the max heap when the total count is odd, and the average of the two roots when it is even. Each insertion is one push into a heap plus at most one move across the boundary, so it is logarithmic, and the query itself is constant time.
import heapq
class MedianFinder:
def __init__(self):
self.lower = []
self.upper = []
def add(self, value):
if not self.lower or value <= -self.lower[0]:
heapq.heappush(self.lower, -value)
else:
heapq.heappush(self.upper, value)
if len(self.lower) > len(self.upper) + 1:
heapq.heappush(self.upper, -heapq.heappop(self.lower))
else:
if len(self.upper) > len(self.lower):
heapq.heappush(self.lower, -heapq.heappop(self.upper))
def median(self):
if len(self.lower) > len(self.upper):
return -self.lower[0]
return (-self.lower[0] + self.upper[0]) / 2
finder = MedianFinder()
for value in [5, 15, 1, 3]:
finder.add(value)
print(finder.median())The output is 4.0. The lower heap holds 1 and 3 and the upper heap holds 5 and 15, so with four values the median is the average of the two middle ones.
The mechanism is that the ordering between the two heaps is maintained by the insertion rule. A new value is compared with the root of the lower heap, which is the largest of the smaller half, and it goes to whichever side it belongs on. The rebalance then moves one element across so that the sizes stay right, and because only one element ever moves, the ordering between the halves is preserved.
The trade-off to name is the constant factors and the memory, since every value is stored in one of the heaps and the negation doubles the arithmetic on the lower side. It also matters that you decide what to return for an empty stream, because the median does not exist then. The advantage over keeping all the values in a sorted list is that you never pay to insert into the middle, so the cost per element is logarithmic instead of linear.