数据流中的中位数
数据流中的中位数
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
本题很重要,它第一次用到了Java中的优先队列,它要是不提我几乎要忘记它的存在了
由图可知,优先队列实现了Queue接口,它自己就是一个实现类,所以new的时候直接new优先队列自己就可以,不需要像队列那样new一个实现类LinkedList
同样的,因为它实现了队列的接口,所以读写方法也是和队列保持一致的,也是offer/poll/peek:
public interface Queue<E> extends Collection<E> {
boolean add(E var1);
boolean offer(E var1);
E remove();
E poll();
E element();
E peek();
}
jdk中默认的优先队列是小顶堆,如果要使用大顶堆的话需要传入自定义的Comparator对象:
// 默认维护小顶堆
private PriorityQueue<Integer> low = new PriorityQueue<>();
//大顶堆
private PriorityQueue<Integer> high = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
/*
我觉得这个写法是很聪明的,它防止了直接返回-1和1时弄混的可能,我就经常记不住。默认是o1和o2比,那返回o2.compareTo(o1)自然就是反着来了。当然从代码角度也是能解释通的,o2.compareTo(o1)<0表示o2<o1,而方法返回-1默认表示o1<o2,这不就反过来了吗
*/
return o2.compareTo(o1);
}
});
另外一个很妙的思路是怎么在一次遍历中就将前半段放在大顶堆,后半段放在小顶堆,那就是先规定奇数个放入大顶堆,而偶数个放入小顶堆,但是在放入之前先和对方比较一下,要是比小顶堆的顶还大\大顶堆的顶还小,就和对方的顶交换再把交换的放入目标堆中,这样最后就能达到效果了。听起来有点梦幻,但是试一试确实是这个道理。
首先按照我们的尝试,中位数奇数正好前后对半,取出来即可。偶数呢,前后难以对半,只能折中,取靠近中间的两个数之和求均值。
没错,这一题也是如此。但是如何动态的求均值呢。如何在任意时刻都能够直接拿到我们想要的均值而不去计算下标取值呢?百思不得其解。
参考他人的想法,使用优先队列PriorityQueue
,然后问题就变得很简单了。
这一题主要的思想是利用优先队列,优先队列分为大顶堆和小顶堆,默认维护的是小顶堆的优先队列。
思路:
需要求的是中位数,如果我将 1 2 3 4 5 6 7 8 定为最终的数据流
此时的中位数是 4+5 求均值。为什么是 4,为什么是 5
利用队列我们就可以看得很清楚,4 是前半部分最大的值,肯定是维系在大顶堆
而 5 是后半部分的最小值,肯定是维系在小顶堆。
问题就好理解了:
使用小顶堆存大数据,使用大顶堆存小数据。这样堆顶一取出就是中位数了。
代码如下:代码中奇数时刻大顶堆存值,所以遇到奇数时刻,大顶堆直接弹出就是中位数
private int cnt = 0;
private PriorityQueue<Integer> low = new PriorityQueue<>();
// 默认维护小顶堆
private PriorityQueue<Integer> high = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//注意这个方法叫compareTo,不是compare
return o2.compareTo(o1);
}
});
public void Insert(Integer num) {
// 数量++
cnt++;
// 如果为奇数的话
if ((cnt & 1) == 1) {
// 由于奇数,需要存放在大顶堆上
// 但是呢,现在你不知道num与小顶堆的情况
// 小顶堆存放的是后半段大的数
// 如果当前值比小顶堆上的那个数更大
if (!low.isEmpty() && num > low.peek()) {
// 存进去
low.offer(num);
// 然后在将那个最小的吐出来
num = low.poll();
} // 最小的就放到大顶堆,因为它存放前半段
high.offer(num);
} else {
// 偶数的话,此时需要存放的是小的数
// 注意无论是大顶堆还是小顶堆,吐出数的前提是得有数
if (!high.isEmpty() && num < high.peek()) {
high.offer(num);
num = high.poll();
} // 大数被吐出,小顶堆插入
low.offer(num);
}
}
public Double GetMedian() {// 表明是偶数
double res = 0;
// 奇数
if ((cnt & 1) == 1) {
res = high.peek();
} else {
res = (high.peek() + low.peek()) / 2.0;
}
return res;
}
GetMedian方法中还涉及到一个包装类转换的问题,因为我们堆中保存的是Integer对象,而要返回的类型却是Double。上诉算法选择的方法是先由Integer转换为double,再自动装箱为Double,当然res = (high.peek() + low.peek()) / 2.0;
这个语句因为和2.0除了,出来就是double类型。有关自动装箱和拆箱可以看我的这篇博文,可以看到Integer是不能直接转为Double的,强制转换也不行。所以除了上面的办法,还有以下的转换办法:
-
强转为double,然后自动装箱:
public Double GetMedian() { if(count%2==1){ return (double)(big.peek());
-
在Integer后加上/1.0,出来就是double,然后自动装箱