数据流的中位数
中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
- void addNum(int num) - 从数据流中添加一个整数到数据结构中。
- double findMedian() - 返回目前所有元素的中位数。
示例:
addNum(1)
addNum(2)
findMedian() -> 1.5
addNum(3)
findMedian() -> 2
方法一:利用插入排序
插入排序能保证将元素添加到列表中还有序
可以用二分法定位到应该插入的位置,将其插入
中位数即为中间的一个或两个数
时间复杂度:O(n)
- 二分查找:O(logn)
- 插入后元素移动:O(n)
class MedianFinder {
private List<Integer> list;
public MedianFinder() {
list=new ArrayList<>();
}
public void addNum(int num) {
int pos=binarySearch(num);
list.add(pos,num);
}
public int binarySearch(int target){
int low=0;
int high=list.size()-1;
while (low<=high){
int mid=(low+high)/2;
if(list.get(mid)<=target){
low=mid+1;
} else {
high=mid-1;
}
}
return low;
}
public double findMedian() {
int size=list.size();
if(size==0) return 0;
if(size%2==0){
return (list.get(size/2-1)+list.get(size/2))/2.0;
} else {
return list.get(size/2);
}
}
}
方法二:两个堆
一个大根堆:存较小的数字,根顶为这部分最大的数字
一个小根堆:存较大的数字,根顶为这部分最小的数字
只要保证两个堆大小相同或相差1,可以直接从根顶得到中位数
如允许大根堆元素多1,获取中位数时:
- 两个堆大小相同,取两个堆顶元素的平均值
- 大根堆要大1,取大根堆的堆顶元素
两个堆平衡的保证:
- 两个堆大小相同,将元素添加到大根堆中,为了保证大根堆存较小数字,小根堆存较大数字,进行调整
- 将大根堆中最大的元素移到小根堆中,再将小跟堆中最小的元素移到大根堆中
- 大根堆更大, 将元素添加到小根堆中,调整方式:
- 将小根堆中最大的元素移到大根堆中,再将大根堆中最大的元素移到小根堆中
时间复杂度:O(logn)
- 进行堆调整时,3次添加,2次删除,O(5logn)
- 获取中位数:O(1)
class MedianFinder {
private PriorityQueue<Integer> bigQueue = new PriorityQueue<>((o1, o2) -> o2 - o1);//大根堆,存较小的元素
private PriorityQueue<Integer> smallQueue = new PriorityQueue<>();//小根堆,存较大的元素
public void addNum(int num) {
if (bigQueue.size() == smallQueue.size()) {
bigQueue.offer(num);//先加到大根堆中
smallQueue.offer(bigQueue.poll());//再将大根堆中最大的元素移到小根堆中
bigQueue.offer(smallQueue.poll());//再将小根堆最小的元素移到大根堆中
} else {
smallQueue.offer(num);//先加到小根堆中
bigQueue.offer(smallQueue.poll());//再将小根堆中最小的元素移到大根堆中
smallQueue.offer(bigQueue.poll());//再将大根堆最大的元素移到小根堆中
}
}
public double findMedian() {
if(smallQueue.size()==bigQueue.size()){//两个堆大小相同,取堆顶元素的平均值
return (smallQueue.peek()+bigQueue.peek())/2.0;
} else {//大根堆比小根堆元素个数多1,中位数即大根堆的根元素
return bigQueue.peek();
}
}
}