编程题求数据流中位数java实现(优先队列-大顶堆-小顶堆)
题目描述
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
问题分析
首先我们要明白这个题目想要我们做什么,数据流的中位数并不是指给你一个长度一定的数组,你将数组排序,数据流指的是实时地输入数据,动态地排序和取数据中间值。
代码及讲解
老实说,我这题确实没写出来,下面是我看到牛客网的一个非常好的解答,给大家链接和引用,后面我会加入一些自己的理解在代码的注释中。
链接:https://www.nowcoder.com/questionTerminal/9be0172896bd43948f8a32fb954e1be1?f=discussion
来源:牛客网
先用java集合PriorityQueue来设置一个小顶堆和大顶堆
主要的思想是:因为要求的是中位数,那么这两个堆,大顶堆用来存较小的数,从大到小排列;
小顶堆存较大的数,从小到大的顺序排序*,显然中位数就是大顶堆的根节点与小顶堆的根节点和的平均数。
⭐保证:小顶堆中的元素都大于等于大顶堆中的元素,所以每次塞值,并不是直接塞进去,而是从另一个堆中poll出一个最大(最小)的塞值
⭐当数目为偶数的时候,将这个值插入大顶堆中,再将大顶堆中根节点(即最大值)插入到小顶堆中;
⭐当数目为奇数的时候,将这个值插入小顶堆中,再讲小顶堆中根节点(即最小值)插入到大顶堆中;
⭐取中位数的时候,如果当前个数为偶数,显然是取小顶堆和大顶堆根结点的平均值;如果当前个数为奇数,显然是取小顶堆的根节点
import java.util.PriorityQueue;
import java.util.Comparator;
public class Solution {
//小顶堆
//小顶堆指的是父节点最小,右子结点最大的二叉树,堆顶(根节点)是最小的
private PriorityQueue<Integer> minHeap=new PriorityQueue<Integer>();
//大顶堆(下面有api描述)
//大顶堆指的是父节点最大,右子结点最小的二叉树,堆顶(根节点)是最大的
private PriorityQueue<Integer> maxHeap=new PriorityQueue<>(15,new Comparator<Integer>(){
//重写compare方法(详解在下面,请一定要看)
//原compare方法当o1>o2返回1,o1=o2返回0,o1<o2返回-1;
//重写后,当o1>o2返回-1,o1=o2返回0,o1<o2返回1;刚好相反,则原compare方法会选出两数中较小的,现在会根据重写的方法选出两数中较大的。
@Override
public int compare(Integer o1,Integer o2){
//这里在大家看来返回的不是-1,1,而是正负数值,回答在下面的compare源码中,请看解析。
return o2-o1;
}
});
//计数,当前堆中的元素个数
int count=0;
public void Insert(Integer num) {
if(count%2==0){
//当数目为偶数的时候,将这个值插入大顶堆中,再将大顶堆中根节点(即最大值)插入到小顶堆中;
maxHeap.offer(num);
minHeap.offer(maxHeap.poll());
}else{
//当数目为奇数的时候,将这个值插入小顶堆中,再讲小顶堆中根节点(即最小值)插入到大顶堆中;
minHeap.offer(num);
maxHeap.offer(minHeap.poll());
}
count++;
}
public Double GetMedian() {
//取中位数的时候,如果当前个数为偶数,显然是取小顶堆和大顶堆根结点的平均值;如果当前个数为奇数,显然是取小顶堆的根节点
if(count%2==0){
return new Double(minHeap.peek()+maxHeap.peek())/2;
}else{
return new Double(minHeap.peek());
}
}
}
按照上述的方法可以将这个毫无头绪的问题简单化,大佬的方法真的很值得我学习,会的还是有点少了啊。
compare方法的解释
在网上看了几个瞎j2写的博客,有人复制别人的代码都没放放到代码块里就在那解释了,实在是看不明白也看不过去,自己解释一下,如果有问题还请各位指出来,谢谢。
下面这段话截取自Comparator接口中compare方法源码:解释了compare方法的返回值,并介绍了sgn函数,翻译过来大概意思就是说:compare比较o1,o2并在o1分别<,=,>o2的情况下,返回负数,0,正数。而sgn的函数的作用是将小于0的数置为-1,大于0的数置为1,0还是0,所以就会得到:在o1分别<,=,>o2的情况下,返回-1,0,1。
/**
* Compares its two arguments for order. Returns a negative integer,
* zero, or a positive integer as the first argument is less than, equal
* to, or greater than the second.<p>
*
* In the foregoing description, the notation
* <tt>sgn(</tt><i>expression</i><tt>)</tt> designates the mathematical
* <i>signum</i> function, which is defined to return one of <tt>-1</tt>,
* <tt>0</tt>, or <tt>1</tt> according to whether the value of
* <i>expression</i> is negative, zero or positive.<p>
*/
int compare(T o1, T o2);
PriorityQueue中comparator接口解释
下面这部分代码取自PriorityQueue的源码,意思是:创建优先队列时会按照给定的容量和给定的比较器初始化。但是,你可能就会问了,那么这个重写的方法到底有啥用啊?请不要着急,往下看。
/**
* Creates a {@code PriorityQueue} with the specified initial capacity
* that orders its elements according to the specified comparator.
*
* @param initialCapacity the initial capacity for this priority queue
* @param comparator the comparator that will be used to order this
* priority queue. If {@code null}, the {@linkplain Comparable
* natural ordering} of the elements will be used.
* @throws IllegalArgumentException if {@code initialCapacity} is
* less than 1
*/
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
重写的compare方法在优先队列中的使用
下面这部分代码同样取自PriorityQueue的源码,意思是:在k位置插入一个元素x时,为了保证堆还是一个大/小顶堆,会用这个方法进行调整,(不是只用这个方法,我截取了部分源码用来说明问题。)
/**
* Inserts item x at position k, maintaining heap invariant by
* promoting x up the tree until it is greater than or equal to
* its parent, or is the root.
*
* To simplify and speed up coercions and comparisons. the
* Comparable and Comparator versions are separated into different
* methods that are otherwise identical. (Similarly for siftDown.)
*
* @param k the position to fill
* @param x the item to insert
*/
private void siftUp(int k, E x) {
//比较器不为空即我们自己定义了比较器时
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
//比较器不为空即我们自己定义了比较器时用下面的方法比较。
@SuppressWarnings("unchecked")
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
//二叉树父结点位置的表示为(当前元素位置-1)/2
int parent = (k - 1) >>> 1;
Object e = queue[parent];
//如果x和其父节点e的比较结果返回正数或0,则不变,否则将它和父节点互换。
//大顶堆时重写了方法x>e时返回-1<0,则将当前元素x上移致父节点,这样就符合了大顶堆的规范,到这里应该就非常的清楚了吧!
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
总结
通过上面的分析,相信大家对这道题和优先队列都有了一定的认识,也明白了为什么大顶堆要这样来构建,希望和你们一起继续加油!(今天听说vivo面开发的人才库里进了一大堆本硕都是985的有项目的有志青年,心里稍微平衡了一点,本彩笔简历进人才库也没啥大不了的嘛,继续努力,不过vivo的人才库怕是真的牛皮,有好多真正的人才,hhhh)。