题目描述
如何求数据流的中位数情况,这个数据流可以随时添加数据,此题要找出数据流的中位数,数据流由无序整数组成,并且数据流是在变化的。
数据流顺序是无序的,添加的顺序也是无序,但是求解是中位数的话就要保持有序的状态,所以基本
Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value.
For example,
[2,3,4], the median is 3
[2,3], the median is (2 + 3) / 2 = 2.5
Design a data structure that supports the following two operations:
void addNum(int num) - Add a integer number from the data stream to the data structure.
double findMedian() - Return the median of all elements so far.
addNum(1)
addNum(2)
findMedian() -> 1.5
addNum(3)
findMedian() -> 2
思路
数据流的添加是无序的,如何保证添加进来后维持有序性问题,保持有序即可,这个是很重要的,因为如果添加进来不保持一致性的话,是很难求解的?
问题的关键点
使用什么样的数据结构解决这个问题是关键?
数组,二叉树, 优先级队列情况
解法一 传统数组
插入排序, 每次插入一个数,都保持有序,然后分为偶数和奇数情况, 但这样的情况,时间复杂度很大, 时间复杂度为 O(n ^2) , 插
树(二分查找树,AVL树基本情况)
一般情况下时间复杂度为Olog(N), 最坏的情况下是O(N),二分查找树退化为一个链表的基本情况,这个是比较最坏的情况
堆维持有序性
堆Heap,将进来的数据流分成大小两个堆,分别保存堆的前半部分和后半部分,大堆是最小数字在堆顶,小堆是最大数在堆顶。取中间值时,根据两个堆的数字个数,如果个数相同,就各取堆顶数字取平均数就是中间数,如果有一个堆数字多1,中间数就是这个堆顶数字。
------- 堆一堆顶(最大数 m1) 堆二堆顶(最小数m2) ----------维持了中间的结构,
如何从堆中求得中位数情况
根据两个堆中元素的数目情况来定做吧,如果是总数是偶数, 如果是奇数
- left.size()== right.size()
return (left.peek()+ right.peek())/2 - left.size() = right.size()+1
return right.peek() - left.size()+1 = right.size
return left.peek();
如果选择哪个堆进行加入是关键
也可以优化成为这种方式
if(maxHeap.isEmpty() || num <= maxHeap.peek()) {
maxHeap.add(num);
} else {
minHeap.add(num);
}
加入的方式
- 如果是第一次加入,也就是left 为空的, left.offer(num)
- 如果不是第一加入, 分为两种可能性
- 如果num>left.peek() ,那肯定是right.offer(num)
- 否则left.offer(num)
加入后如何维持两个堆的平衡
这个步骤确保从堆顶取出来的元素可以直接计算出数据流中的中位数情况
所以这两个堆中的数组保持如下的关系
- 数组相同
- left 比right多一个
- right 比left 多一个元素
为了维护两个树的关系,就像AVL树,我们需要进行调整
如果进行调整
在num加入两个堆中的一个后,判断他们之间的数目关系
-
如果左边的多于右边的
- 将左边的头元素加入右边
-
返过来是一样的效果情况
// 两边堆的数组大小相差个数,就是元素个数情况,最多差一个
if(left.size()>right.size()+1){
right.offer(left.poll());
}
if(right.size()>left.size()+1){
left.offer(right.poll());
}
如何维持两个堆的数目关系
为了保持中位数很容易算出来,跟堆顶的元素相关,无论是数据流中总的数组是多少,奇数还是偶数情况,都应该
- left.size()== right.size()
return (left.peek()+ right.peek())/2 - left.size() = right.size()+1
return right.peek() - left.size()+1 = right.size
return left.peek();
Java:
所以第一个该考虑的问题是选择什么样的数据结构, 当加入一个大小不知道的数据时,和前面的数据一样保持有序的状态即可
如何设计这个堆是个关键问题,设计成为两个堆,两个堆中的元素连接起来构成一个数组的形式,
第一个堆设计成为最大堆,堆顶元素为最大值,
第二个堆设计成为最小堆的形式,堆定元素最小值的形式,加入一个数组
成型代码
class MedianFinder {
private PriorityQueue<Integer> left, right;
/** initialize your data structure here. */
public MedianFinder() {
left = new PriorityQueue<>(new Comparator<Integer>()
{
@Override
public int compare(Integer o1, Integer o2){
return o2-o1;}
});
right = new PriorityQueue<>();
}
public void addNum(int num) {
//代码逻辑如何进行设计
//第一次插入该种情况,肯定是在左边的情况进行
if(left.size()==0)
{
left.offer(num);
}
// 第二次插入的情况, 判断是加在什么位置信息处
else {
if(num>left.peek())
{
right.offer(num);
}
else
left.offer(num);
}
//对添加后的长度进行修改即可,就类似AVL树,进行各种旋转, 保持整个数组平衡,,
// 两边堆的数组大小相差个数,就是元素个数情况,最多差一个
if(left.size()>right.size()+1){
right.offer(left.poll());
}
if(right.size()>left.size()+1){
left.offer(right.poll());
}
}
public double findMedian() {
//如果获取堆顶的元素进行计算该种情况
if(left.size()==right.size())
{
return (left.peek()+right.peek())/2.0;
}
else
return left.size()>right.size() ? left.peek(): right.peek();
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/
时间复杂度分析
O(log(N)) 调整堆中元素,O(1) 是进行计算