剑指 Offer 41. 数据流中的中位数(困难)
问题描述
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。示例 1:
输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]示例 2:
输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]限制:
最多会对 addNum、findMedian 进行 50000 次调用。
解题思路
前面几道题的解题思路中提到,求最小的k个数和最大的k个数,这种题的最快解法就是维护一个大小为k的大顶堆(求前k小的数)或者小顶堆(求前k大的数)。
并且求一堆数的中位数,使用大顶堆+小顶堆的解法效率也远高于其他解法。具体思路如下:
(1)由于我们要求中位数,即需要把所有数分为左右两部分,左边的数是小于中位数的,右边的数是大于中位数的。
(2)如果这些数的总个数为偶数,那么中位数就是左边部分的最大值,和右边部分的最小值求和再除以2.
(3)如果这些数的总个数是奇数,那么如果我们始终优先维护左边部分,那么直接取左边部分的最大元素即可。
基于以上原理,我们需要构建两部分,并且能方便的得到左边部分所有元素的最大值,和右边部分所有元素的最小值。因此容易想到,只需要构建一颗大顶堆维护左边元素,再构建一颗小顶堆维护右边元素。并优先维护左边元素(保证奇数情况下,左边元素比右边元素多一个)。
因此,在加入元素时,考虑处理如下的情况:
①假如新元素num小于左边元素最大值(左边堆顶结构),说明num元素需要放在左边结构中,但原本元素个数是均匀的,左边部分新加入一个元素,说明需要匀一个元素到右边结构(堆顶元素)。保证左边元素和右边元素在顺序上以及个数上的稳定性。
②假如新元素num大于右边元素最小值(右边堆顶结构),说明num元素需要放在右边结构中,但原本元素个数是均匀的,右边机构中加入一个新元素,说明需要匀一个元素到左边结构(堆顶元素)。保证左边元素和右边元素在顺序上以及个数上的稳定性。
③当左边结构元素个数 与 右边结构元素个数不同时,说明此时元素个数为奇数个,应当优先保证左边结构元素个数 大于 右边结构元素个数。
Java代码
class MedianFinder {
Queue<Integer> maxqueue;
Queue<Integer> minqueue;
/** initialize your data structure here. */
public MedianFinder() {
maxqueue = new PriorityQueue<>((o1,o2)->o2-o1);
minqueue = new PriorityQueue<>();
}
public void addNum(int num) {
maxqueue.offer(num);
minqueue.offer(maxqueue.poll());
if(minqueue.size() > maxqueue.size())
maxqueue.offer(minqueue.poll());
}
public double findMedian() {
if(maxqueue.size()>minqueue.size()) return maxqueue.peek();
return (maxqueue.peek()+minqueue.peek())/2.0;
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/