Track the Running Twice-Median of a Live Price Feed

Solve this Problem
Hard30–35 min
Topics
Companies
Practice:LeetCode ↗

A trading desk receives a live feed of prices. You are given a log of events: record adds a price to the feed, and query asks for the median of every price recorded so far. Report each median doubled (for an odd count that is twice the middle price; for an even count it is the sum of the two middle prices) so the answers are always integers, and return them in order.

Re-sorting the entire history on every query gets slower as the feed grows. Keeping the smaller half of the prices in a max-heap and the larger half in a min-heap puts the median right at the two roots, so a query is instant and each new price only costs a heap insertion.

Test Case 1:

Input:ops = ["record","record","query","record","query","record","query"], values = [8,2,0,6,0,10,0]
Output:[10, 12, 14]
Explanation:Prices {2, 8} have median 5 → doubled 10. Adding 6 gives {2, 6, 8}, median 6 → 12. Adding 10 gives {2, 6, 8, 10}, median (6+8)/2 = 7 → 14.

Test Case 2:

Input:ops = ["record","query"], values = [7,0]
Output:[14]
Explanation:A single price is its own median; doubled, 14.

Test Case 3:

Input:ops = ["record","record","query"], values = [-5,5,0]
Output:[0]
Explanation:The two middle prices are -5 and 5; their sum, 0, is the doubled median.

Constraints

  • ◆1 ≤ ops.length ≤ 100, and values.length equals ops.length
  • ◆Each op is "record" (add values[i] to the feed) or "query" (report the current median, doubled)
  • ◆-1000 ≤ values[i] ≤ 1000 for every "record"; the values entry of a "query" is ignored
  • ◆At least one "record" appears before every "query"
  • ◆The median of an even number of prices is the average of the two middle prices; report it doubled so the answer is always an integer
🚀

Try the Dry Run

Don't just read the solution — watch it execute, one step at a time.

Approach & Solutions

Brute Force — Re-Sort Every Recorded Price on Each Query

Brute

Store every recorded price in a plain list. When a query arrives, copy the list, sort the copy, and read the middle: for n prices the doubled median is sorted[(n-1)/2] + sorted[n/2], which works for both odd and even n. It is easy to get right, but every single query sorts everything recorded so far from scratch — long feeds with frequent queries make this very slow.

TimeO(q · n log n)
SpaceO(n)
1class Solution { 2 public List<Integer> runningTwiceMedian(String[] ops, int[] values) { 3 List<Integer> seen = new ArrayList<>(); 4 List<Integer> answers = new ArrayList<>(); 5 for (int i = 0; i < ops.length; i++) { 6 if (ops[i].equals("record")) { 7 seen.add(values[i]); 8 } else { 9 List<Integer> sorted = new ArrayList<>(seen); 10 Collections.sort(sorted); 11 int n = sorted.size(); 12 answers.add(sorted.get((n - 1) / 2) + sorted.get(n / 2)); 13 } 14 } 15 return answers; 16 } 17}

Optimal — Two Heaps Split Around the Median

Optimal

Split the recorded prices into two halves and keep each half in a heap: a max-heap "low" holding the smaller half and a min-heap "high" holding the larger half. The invariant is that every price in low is ≤ every price in high, and low is either the same size as high or exactly one larger. Then the median sits at the roots: if low is bigger it is low's root (doubled), otherwise it is the average of the two roots (their sum, doubled). A new price goes into low if it is ≤ low's root, otherwise into high; if that leaves the sizes unbalanced, the root of the too-large heap is moved across. Every record costs O(log n) and every query costs O(1). (In the C solution the max-heap is a min-heap of negated values.)

TimeO(n log n)
SpaceO(n)
1class Solution { 2 public List<Integer> runningTwiceMedian(String[] ops, int[] values) { 3 PriorityQueue<Integer> low = new PriorityQueue<>(Collections.reverseOrder()); 4 PriorityQueue<Integer> high = new PriorityQueue<>(); 5 List<Integer> answers = new ArrayList<>(); 6 for (int i = 0; i < ops.length; i++) { 7 if (ops[i].equals("record")) { 8 if (low.isEmpty() || values[i] <= low.peek()) { 9 low.offer(values[i]); 10 } else { 11 high.offer(values[i]); 12 } 13 if (low.size() > high.size() + 1) { 14 high.offer(low.poll()); 15 } else if (high.size() > low.size()) { 16 low.offer(high.poll()); 17 } 18 } else if (low.size() > high.size()) { 19 answers.add(2 * low.peek()); 20 } else { 21 answers.add(low.peek() + high.peek()); 22 } 23 } 24 return answers; 25 } 26}

Related Problems