090. Running Median
Problem
Return the median after every integer insertion. For an odd prefix length, use the middle value; for an even length, use the mean of the two middle values. Results are floats. Values are bounded by absolute magnitude 10^9 so these integer and half-integer medians are exactly representable.
Examples
Example 1
running_medians([1,2,3,4])
=> [1.0, 1.5, 2.0, 2.5]The exposed middle roots give each prefix median.
Example 2
running_medians([5,5,5])
=> [5.0, 5.0, 5.0]Duplicates may occupy either side without breaking order.
Example 3
running_medians([])
=> []No prefix requires a median.
Reveal the worked solution and reasoning
Why this mechanism works
Split the observations into a lower half and an upper half. A max-oriented heap exposes the largest lower value; a min-heap exposes the smallest upper value. Maintain the lower half with either equal size or one extra element so the median is always at the heap roots.
State and boundaries
lower stores negated lower-half values. upper stores upper-half values. Every lower value is <= every upper value; len(lower) is len(upper) or one larger.
Protocol
- Insert into the appropriate half by comparing with the lower maximum.
- Rebalance by moving one root if necessary.
- Read the lower root for odd size.
- Average the two roots for even size.
Visualization
lower (max side) <= upper (min side)
[1,2] | [3,4] -> median (2+3)/2
[1,2,3] | [4,5] -> median 3Python implementation
import heapq
def running_medians(values):
lower, upper, output = [], [], []
for value in values:
if not lower or value <= -lower[0]:
heapq.heappush(lower, -value)
else:
heapq.heappush(upper, value)
if len(lower) > len(upper) + 1:
heapq.heappush(upper, -heapq.heappop(lower))
elif len(upper) > len(lower):
heapq.heappush(lower, -heapq.heappop(upper))
if len(lower) == len(upper):
output.append((-lower[0] + upper[0]) / 2)
else:
output.append(float(-lower[0]))
return output
Worked execution
running_medians([1,2,3,4])
L4 running_medians: lower, upper, output = [], [], [] values=[1, 2, 3, 4]; lower=[]; upper=[]; output=[] L5 running_medians: for value in values: values=[1, 2, 3, 4]; lower=[]; upper=[]; output=[]; value=1 L7 running_medians: heapq.heappush(lower, -value) values=[1, 2, 3, 4]; lower=[-1]; upper=[]; output=[]; value=1 L17 running_medians: output.append(float(-lower[0])) values=[1, 2, 3, 4]; lower=[-1]; upper=[]; output=[1.0]; value=1 L5 running_medians: for value in values: values=[1, 2, 3, 4]; lower=[-1]; upper=[]; output=[1.0]; value=2 L9 running_medians: heapq.heappush(upper, value) values=[1, 2, 3, 4]; lower=[-1]; upper=[2]; output=[1.0]; value=2 L15 running_medians: output.append((-lower[0] + upper[0]) / 2) values=[1, 2, 3, 4]; lower=[-1]; upper=[2]; output=[1.0, 1.5]; value=2 L5 running_medians: for value in values: values=[1, 2, 3, 4]; lower=[-1]; upper=[2]; output=[1.0, 1.5]; value=3 ... 2 intermediate state changes omitted ... L17 running_medians: output.append(float(-lower[0])) values=[1, 2, 3, 4]; lower=[-2, -1]; upper=[3]; output=[1.0, 1.5, 2.0]; value=3 L5 running_medians: for value in values: values=[1, 2, 3, 4]; lower=[-2, -1]; upper=[3]; output=[1.0, 1.5, 2.0]; value=4 L9 running_medians: heapq.heappush(upper, value) values=[1, 2, 3, 4]; lower=[-2, -1]; upper=[3, 4]; output=[1.0, 1.5, 2.0]; value=4 L15 running_medians: output.append((-lower[0] + upper[0]) / 2) values=[1, 2, 3, 4]; lower=[-2, -1]; upper=[3, 4]; output=[1.0, 1.5, 2.0, 2.5]; value=4
Result: [1.0, 1.5, 2.0, 2.5]. The exposed middle roots give each prefix median.
running_medians([5,5,5])
L4 running_medians: lower, upper, output = [], [], [] values=[5, 5, 5]; lower=[]; upper=[]; output=[] L5 running_medians: for value in values: values=[5, 5, 5]; lower=[]; upper=[]; output=[]; value=5 L7 running_medians: heapq.heappush(lower, -value) values=[5, 5, 5]; lower=[-5]; upper=[]; output=[]; value=5 L17 running_medians: output.append(float(-lower[0])) values=[5, 5, 5]; lower=[-5]; upper=[]; output=[5.0]; value=5 L7 running_medians: heapq.heappush(lower, -value) values=[5, 5, 5]; lower=[-5, -5]; upper=[]; output=[5.0]; value=5 L11 running_medians: heapq.heappush(upper, -heapq.heappop(lower)) values=[5, 5, 5]; lower=[-5]; upper=[5]; output=[5.0]; value=5 L15 running_medians: output.append((-lower[0] + upper[0]) / 2) values=[5, 5, 5]; lower=[-5]; upper=[5]; output=[5.0, 5.0]; value=5 L7 running_medians: heapq.heappush(lower, -value) values=[5, 5, 5]; lower=[-5, -5]; upper=[5]; output=[5.0, 5.0]; value=5 L17 running_medians: output.append(float(-lower[0])) values=[5, 5, 5]; lower=[-5, -5]; upper=[5]; output=[5.0, 5.0, 5.0]; value=5
Result: [5.0, 5.0, 5.0]. Duplicates may occupy either side without breaking order.
Why the solution is correct
Insertion into the correct side preserves ordering; transferring a boundary root during rebalancing preserves that order too. The size invariant positions the middle element or middle pair exactly at the exposed roots, so each reported median is correct.
Time and space
O(n log n) total time and O(n) storage; each insertion uses O(log n) heap work.
Common mistakes
Balancing sizes without preserving cross-heap order is insufficient. Negated lower values must be converted back when reporting or transferring.
Reasoning card
State: lower stores negated lower-half values. upper stores upper-half values. Every lower value is <= every upper value; len(lower) is len(upper) or one larger.
Invariant: Sizes differ by at most one and every lower value <= every upper value.
Decision to rehearse: Balance lower max-heap and upper min-heap.
Finish: Average the two roots for even size
Transfer exercise
Support a fixed-size sliding median. Identify why expired values require deletion support or lazy removal in both heaps.