剑指 offer 41 数据流中的中位数
最大堆 & 最小堆
如图所示,如果数据在容器中已经排序,那么中位数可以由 P 1 P_1 P1和 P 2 P_2 P2指向的数得到。如果容器中数据的数目是奇数,那么 P 1 P_1 P1和 P 2 P_2 P2指向同一个数据。
此时,整个数据容器被分成两部分。位于容器左边部分的数据比右边的数据小。另外, P 1 P_1 P1指向的数据是左边部分最大的数, P 2 P_2 P2指向的数据是右边部分最小的数。如果能保证数据容器左边的数据小于右边的数据,那么即使左、右两边内部的数据没有排序,也可以根据左边最大的数及右边最小的数得到中位数。用最大堆迅速找出最大数,快速从最小堆中找出最小的数。
解决问题的思路:用一个最大堆实现左边的数据容器,用一个最小堆实现右边的数据容器。往堆中插入一个数据的时间效率是 O ( l o g n ) O(logn) O(logn),只需要 O ( 1 ) O(1) O(1)时间就可以得到位于堆顶的数据。
接下来考虑用最大堆和最小堆实现的一些细节。首先要保证数据平均分配到两个堆中,两个堆中数据的数目只差不能超过1.为了实现平均分配,可以在数据的总数目是偶数的时候向最小堆插入数据,否则插入最大堆。
还要保证最大堆中的所有数据都要小于最小堆中的数据。当数据大的总数目是偶数时,按照前面的分配规则会把新的数据插入最小堆,如果此时这个数据比最大堆中的一些数据要小(小于最大堆中的最大值)。可以先把这个新的数据插入最大堆,接着把最大堆中的最大的数字拿出来插入最小堆。由于最终插入最小堆的数字是原最大堆的最大的数字,这样就保证了最小堆中所有数字都大于最大堆中的数字。
当需要把一个数据插入最大堆,但这个数据大于于最小堆里的一些数据时,这个情形和前面类似。
class MedianFinder {
PriorityQueue<Integer> max_que ;
PriorityQueue<Integer> min_que ;
/** initialize your data structure here. */
public MedianFinder() {
max_que=new PriorityQueue<>(new Comparator<Integer> (){
public int compare(Integer num1,Integer num2){
return num2-num1;
}
});
min_que = new PriorityQueue<>(new Comparator<Integer>(){
public int compare(Integer num1,Integer num2){
return num1-num2;
}
});
}
public void addNum(int num) {
if((max_que.size()+min_que.size())%2==0){
//插入最小堆
//num要大于最大堆中最大的
if(max_que.isEmpty()){
min_que.add(num);
}
else if(num>=max_que.peek()){
min_que.add(num);
}else{
min_que.add(max_que.poll());
max_que.add(num);
}
}else{
if(num<min_que.peek()){
max_que.add(num);
}else{
max_que.add(min_que.poll());
min_que.add(num);
}
}
}
public double findMedian() {
if((max_que.size()+min_que.size())%2==0){
return (max_que.peek()+min_que.peek())/2.0;
}else{
return min_que.peek();
}
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/
添加元素的时间复杂度
O
(
l
o
g
n
)
O(logn)
O(logn)。
查找中位数的时间复杂度
O
(
1
)
O(1)
O(1)。
空间复杂度
O
(
n
)
O(n)
O(n),其中
n
n
n是数据流中元素的数量,最小堆和最大堆最多同时保存
n
n
n个元素。