中位数 是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
例如 arr = [2,3,4]
的中位数是 3
。
例如 arr = [2,3]
的中位数是 (2 + 3) / 2 = 2.5
。
实现 MedianFinder 类:
MedianFinder()
初始化 MedianFinder
对象。
void addNum(int num)
将数据流中的整数 num
添加到数据结构中。
double findMedian()
返回到目前为止所有元素的中位数。与实际答案相差 10-5
以内的答案将被接受。
示例 1:
**输入**
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
[[], [1], [2], [], [3], []]
**输出**
[null, null, null, 1.5, null, 2.0]
**解释**
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1); // arr = [1]
medianFinder.addNum(2); // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3); // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0
提示:
-105 <= num <= 105
在调用 findMedian
之前,数据结构中至少有一个元素
最多 5 * 104
次调用 addNum
和 findMedian
方法一:优先队列 思路和算法
我们用两个优先队列 queMax 和 queMin 分别记录大于中位数的数和小于等于中位数的数。当累计添加的数的数量为奇数时,queMin 中的数的数量比 queMax 多一个,此时中位数为 queMin 的队头。当累计添加的数的数量为偶数时,两个优先队列中的数的数量相同,此时中位数为它们的队头的平均值。
当我们尝试添加一个数 num 到数据结构中,我们需要分情况讨论:
num} \leq \max {\textit{queMin}\
此时 num 小于等于中位数,我们需要将该数添加到 queMin 中。新的中位数将小于等于原来的中位数,因此我们可能需要将 queMin 中最大的数移动到 queMax 中。
num} > \max {\textit{queMin}\
此时 num 大于中位数,我们需要将该数添加到 queMin 中。新的中位数将大于等于原来的中位数,因此我们可能需要将 queMax 中最小的数移动到 queMin 中。
特别地,当累计添加的数的数量为 $0$ 时,我们将 num 添加到 queMin 中。
代码
[sol1-C++] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class MedianFinder {public : priority_queue<int , vector<int >, less<int >> queMin; priority_queue<int , vector<int >, greater<int >> queMax; MedianFinder () {} void addNum (int num) { if (queMin.empty () || num <= queMin.top ()) { queMin.push (num); if (queMax.size () + 1 < queMin.size ()) { queMax.push (queMin.top ()); queMin.pop (); } } else { queMax.push (num); if (queMax.size () > queMin.size ()) { queMin.push (queMax.top ()); queMax.pop (); } } } double findMedian () { if (queMin.size () > queMax.size ()) { return queMin.top (); } return (queMin.top () + queMax.top ()) / 2.0 ; } };
[sol1-Java] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class MedianFinder { PriorityQueue<Integer> queMin; PriorityQueue<Integer> queMax; public MedianFinder () { queMin = new PriorityQueue <Integer>((a, b) -> (b - a)); queMax = new PriorityQueue <Integer>((a, b) -> (a - b)); } public void addNum (int num) { if (queMin.isEmpty() || num <= queMin.peek()) { queMin.offer(num); if (queMax.size() + 1 < queMin.size()) { queMax.offer(queMin.poll()); } } else { queMax.offer(num); if (queMax.size() > queMin.size()) { queMin.offer(queMax.poll()); } } } public double findMedian () { if (queMin.size() > queMax.size()) { return queMin.peek(); } return (queMin.peek() + queMax.peek()) / 2.0 ; } }
[sol1-C#] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class MedianFinder { PriorityQueue<int , int > queMin; PriorityQueue<int , int > queMax; public MedianFinder () { queMin = new PriorityQueue<int , int >(); queMax = new PriorityQueue<int , int >(); } public void AddNum (int num ) { if (queMin.Count == 0 || num <= queMin.Peek()) { queMin.Enqueue(num, -num); if (queMax.Count + 1 < queMin.Count) { int tmp = queMin.Dequeue(); queMax.Enqueue(tmp, tmp); } } else { queMax.Enqueue(num, num); if (queMax.Count > queMin.Count) { int tmp = queMax.Dequeue(); queMin.Enqueue(tmp, -tmp); } } } public double FindMedian () { if (queMin.Count > queMax.Count) { return queMin.Peek(); } return (queMin.Peek() + queMax.Peek()) / 2.0 ; } }
[sol1-Python3] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class MedianFinder : def __init__ (self ): self.queMin = list () self.queMax = list () def addNum (self, num: int ) -> None : queMin_ = self.queMin queMax_ = self.queMax if not queMin_ or num <= -queMin_[0 ]: heapq.heappush(queMin_, -num) if len (queMax_) + 1 < len (queMin_): heapq.heappush(queMax_, -heapq.heappop(queMin_)) else : heapq.heappush(queMax_, num) if len (queMax_) > len (queMin_): heapq.heappush(queMin_, -heapq.heappop(queMax_)) def findMedian (self ) -> float : queMin_ = self.queMin queMax_ = self.queMax if len (queMin_) > len (queMax_): return -queMin_[0 ] return (-queMin_[0 ] + queMax_[0 ]) / 2
[sol1-Golang] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 type MedianFinder struct { queMin, queMax hp } func Constructor () MedianFinder { return MedianFinder{} } func (mf *MedianFinder) AddNum(num int ) { minQ, maxQ := &mf.queMin, &mf.queMax if minQ.Len() == 0 || num <= -minQ.IntSlice[0 ] { heap.Push(minQ, -num) if maxQ.Len()+1 < minQ.Len() { heap.Push(maxQ, -heap.Pop(minQ).(int )) } } else { heap.Push(maxQ, num) if maxQ.Len() > minQ.Len() { heap.Push(minQ, -heap.Pop(maxQ).(int )) } } } func (mf *MedianFinder) FindMedian() float64 { minQ, maxQ := mf.queMin, mf.queMax if minQ.Len() > maxQ.Len() { return float64 (-minQ.IntSlice[0 ]) } return float64 (maxQ.IntSlice[0 ]-minQ.IntSlice[0 ]) / 2 } type hp struct { sort.IntSlice }func (h *hp) Push(v interface {}) { h.IntSlice = append (h.IntSlice, v.(int )) }func (h *hp) Pop() interface {} { a := h.IntSlice; v := a[len (a)-1 ]; h.IntSlice = a[:len (a)-1 ]; return v }
复杂度分析
时间复杂度:
addNum: $O(\log n)$,其中 $n$ 为累计添加的数的数量。
findMedian: $O(1)$。
空间复杂度:$O(n)$,主要为优先队列的开销。
方法二:有序集合 + 双指针 思路和算法
我们也可以使用有序集合维护这些数。我们把有序集合看作自动排序的数组,使用双指针指向有序集合中的中位数元素即可。当累计添加的数的数量为奇数时,双指针指向同一个元素。当累计添加的数的数量为偶数时,双指针分别指向构成中位数的两个数。
当我们尝试添加一个数 num 到数据结构中,我们需要分情况讨论:
初始有序集合为空时,我们直接让左右指针指向 num 所在的位置。
有序集合为中元素为奇数时,left 和 right 同时指向中位数。如果 num 大于等于中位数,那么只要让 left 左移,否则让 right 右移即可。
有序集合为中元素为偶数时,left 和 right 分别指向构成中位数的两个数。
当 num 成为新的唯一的中位数,那么我们让 left 右移,right 左移,这样它们即可指向 num 所在的位置;
当 num 大于等于 right,那么我们让 left 右移即可;
当 num 小于 right 指向的值,那么我们让 right 左移,注意到如果 num 恰等于 left 指向的值,那么 num 将被插入到 left 右侧,使得 left 和 right 间距增大,所以我们还需要额外让 left 指向移动后的 right。
代码
[sol2-C++] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class MedianFinder { multiset<int > nums; multiset<int >::iterator left, right; public : MedianFinder () : left (nums.end ()), right (nums.end ()) {} void addNum (int num) { const size_t n = nums.size (); nums.insert (num); if (!n) { left = right = nums.begin (); } else if (n & 1 ) { if (num < *left) { left--; } else { right++; } } else { if (num > *left && num < *right) { left++; right--; } else if (num >= *right) { left++; } else { right--; left = right; } } } double findMedian () { return (*left + *right) / 2.0 ; } };
[sol2-Java] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 class MedianFinder { TreeMap<Integer, Integer> nums; int n; int [] left; int [] right; public MedianFinder () { nums = new TreeMap <Integer, Integer>(); n = 0 ; left = new int [2 ]; right = new int [2 ]; } public void addNum (int num) { nums.put(num, nums.getOrDefault(num, 0 ) + 1 ); if (n == 0 ) { left[0 ] = right[0 ] = num; left[1 ] = right[1 ] = 1 ; } else if ((n & 1 ) != 0 ) { if (num < left[0 ]) { decrease(left); } else { increase(right); } } else { if (num > left[0 ] && num < right[0 ]) { increase(left); decrease(right); } else if (num >= right[0 ]) { increase(left); } else { decrease(right); System.arraycopy(right, 0 , left, 0 , 2 ); } } n++; } public double findMedian () { return (left[0 ] + right[0 ]) / 2.0 ; } private void increase (int [] iterator) { iterator[1 ]++; if (iterator[1 ] > nums.get(iterator[0 ])) { iterator[0 ] = nums.ceilingKey(iterator[0 ] + 1 ); iterator[1 ] = 1 ; } } private void decrease (int [] iterator) { iterator[1 ]--; if (iterator[1 ] == 0 ) { iterator[0 ] = nums.floorKey(iterator[0 ] - 1 ); iterator[1 ] = nums.get(iterator[0 ]); } } }
[sol2-Python3] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 from sortedcontainers import SortedListclass MedianFinder : def __init__ (self ): self.nums = SortedList() self.left = self.right = None self.left_value = self.right_value = None def addNum (self, num: int ) -> None : nums_ = self.nums n = len (nums_) nums_.add(num) if n == 0 : self.left = self.right = 0 else : if num < self.left_value: self.left += 1 if num < self.right_value: self.right += 1 if n & 1 : if num < self.left_value: self.left -= 1 else : self.right += 1 else : if self.left_value < num < self.right_value: self.left += 1 self.right -= 1 elif num >= self.right_value: self.left += 1 else : self.right -= 1 self.left = self.right self.left_value = nums_[self.left] self.right_value = nums_[self.right] def findMedian (self ) -> float : return (self.left_value + self.right_value) / 2
[sol2-Golang] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 type MedianFinder struct { nums *redblacktree.Tree total int left, right iterator } func Constructor () MedianFinder { return MedianFinder{nums: redblacktree.NewWithIntComparator()} } func (mf *MedianFinder) AddNum(num int ) { if count, has := mf.nums.Get(num); has { mf.nums.Put(num, count.(int )+1 ) } else { mf.nums.Put(num, 1 ) } if mf.total == 0 { it := mf.nums.Iterator() it.Next() mf.left = iterator{it, 1 } mf.right = mf.left } else if mf.total%2 == 1 { if num < mf.left.Key().(int ) { mf.left.prev() } else { mf.right.next() } } else { if mf.left.Key().(int ) < num && num < mf.right.Key().(int ) { mf.left.next() mf.right.prev() } else if num >= mf.right.Key().(int ) { mf.left.next() } else { mf.right.prev() mf.left = mf.right } } mf.total++ } func (mf *MedianFinder) FindMedian() float64 { return float64 (mf.left.Key().(int )+mf.right.Key().(int )) / 2 } type iterator struct { redblacktree.Iterator count int } func (it *iterator) prev() { if it.count > 1 { it.count-- } else { it.Prev() it.count = it.Value().(int ) } } func (it *iterator) next() { if it.count < it.Value().(int ) { it.count++ } else { it.Next() it.count = 1 } }
复杂度分析
时间复杂度:
addNum: $O(\log n)$,其中 $n$ 为累计添加的数的数量。
findMedian: $O(1)$。
空间复杂度:$O(n)$,主要为有序集合的开销。
进阶 $1$ 如果数据流中所有整数都在 $0$ 到 $100$ 范围内,那么我们可以利用计数排序统计每一类数的数量,并使用双指针维护中位数。
进阶 $2$ 如果数据流中 $99%$ 的整数都在 $0$ 到 $100$ 范围内,那么我们依然利用计数排序统计每一类数的数量,并使用双指针维护中位数。对于超出范围的数,我们可以单独进行处理,建立两个数组,分别记录小于 $0$ 的部分的数的数量和大于 $100$ 的部分的数的数量即可。当小部分时间,中位数不落在区间 $[0,100]$ 中时,我们在对应的数组中暴力检查即可。