题目描述
如何得到一个数据流中的中位数?
如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
其实方法很多啊。简单介绍几个思路
方法1:使用无序数组存储,然后需要对数组进行排序,然后就可以顺利找到中位数。
方法2:在插入数组时,就进行排序,形成有序的数组,排完序后就可以直接找到中位数
方法3:使用树结构,二叉搜索树或者平衡二叉树,都是在插入的时候完成排序。
方法4:大家答案中出现最多的方法,利用两个堆。
解法1:
对应方法1,使用无序数组,主要为了体现思路,所以直接调用库的排序方法。就非常直观
import java.util.ArrayList; import java.util.Collections; public class Solution { ArrayList<Integer> array = new ArrayList<>(); public void Insert(Integer num) { array.add(num); } public Double GetMedian() { Collections.sort(array); int index = array.size()/2; if(array.size()%2 != 0){ //奇数直接取 return (double)array.get(index); } return ((double)(array.get(index-1))+(double)array.get(index))/2;//偶数平均值 } }
解法2:
对应方法2,插入的时候排序,所以很容易想到利用堆排序,每次加入一个元素就调整堆。因为第4种使用到了堆,所以这里选择(在网上扒了)另一种插入排序的方法,二分法。
import java.util.LinkedList; import java.util.List; public class Solution { private List<Integer> list = new LinkedList(); public void Insert(Integer num) { if(list.size()==0){ list.add(num); return; } int first = 0; int last = list.size()-1; int mid = 0; while(first <= last){ mid = (first+last)/2; if(list.get(mid)>num) last = mid-1; else first = mid+1; } list.add(first,num); return; } public Double GetMedian() { int index = list.size(); if(index%2==1){ return (double)list.get(index/2); } return ((double)(list.get(index/2-1))+(double)list.get(index/2))/2; } }
解法3:
使用树。让树给我们排序,然后我们再取中位数,但是值得注意的是,在Set集合中,没有get方法,所以无法直接获取某个角标所对应的元素,通过查资料,发现需要将Set转换成List即可,因此需要再处理一次。
import java.util.ArrayList; import java.util.TreeSet; public class Solution { public TreeSet<Integer> tree = new TreeSet<>(); public void Insert(Integer num) { tree.add(num); } public Double GetMedian() { ArrayList<Integer> list = new ArrayList<>(); list.addAll(tree); int index = list.size(); if(index%2==1){ return (double)list.get(index/2); } return ((double)(list.get(index/2-1))+(double)list.get(index/2))/2; } }
解法4:
需要注意的是,使用两个堆,利用两个堆的堆顶解题。
思路如下:
将读入的数据分为几乎数量相同的两部分,一部分数字小,另一部分大。小的一部分采用大顶堆存放,大的一部分采用小顶堆存放。这样两个堆的堆顶就是整个数据流中,最中间的两个数。当总个数为偶数时,使两个堆的数目相同,则中位数=大顶堆的最大数字与小顶堆的最小数字的平均值;而总个数为奇数时,使小顶堆的个数比大顶堆多一,则中位数=小顶堆的最小数字。
插入的步骤如下:
1.若已读取的个数为偶数(包括0)时,两个堆的数目已经相同,再插入一个数时,应该选一个数插入到小顶堆中,从而实现小顶堆的个数多一。但是,不能直接插到小顶堆,本应该选择一个数加入到小顶堆中,但是必须选一个较大的数放入小顶堆,而插入的这个数不一定符合要求(大顶堆的数不服它),所以这个数要和大顶堆的最大数(先进大顶堆)打一群架,谁赢了(谁大)谁进小顶堆。
2。若已读取的个数为奇数时,小顶堆的个数多一,所以要将某个数字插入到大顶堆中,此时方法与上面类似。新进来的数要和小顶堆的堆顶(最小值)打一架,打输的(更小的那个数)进入大顶堆。
本方法的空间复杂度是O(1),空间复杂度是O(logn),相比于以上几个方法,可以说是最优选择。因此也是大家使用最多的解法。堆有多种方式实现,数组或者基于队列实现。这里使用PriorityQueue实现,PriorityQueue默认是一个小顶堆,因此我们需要自己实现大顶堆,这里传入自定义的Comparator函数可以实现大顶堆
import java.util.PriorityQueue; import java.util.Comparator; public class Solution { PriorityQueue<Integer> minHeap = new PriorityQueue<>(); //小顶堆 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>(){ //大顶堆 @Override public int compare(Integer i1,Integer i2){ return i2-i1;//降序排列,小顶堆中是i1-i2 } }); //Lambda表达式写法: //PriorityQueue<Integer> Heap=new PriorityQueue<>((Comparator<Integer>)(o1,o2)->o2-o1); int count = 0;//记录当前个数是奇数还是偶数 public void Insert(Integer num) { //个数为偶数的话,则先插入到大顶堆,并调整,然后将大顶堆中最大的数插入小顶堆中 if(count % 2 == 0){ maxHeap.offer(num); int max = maxHeap.poll(); minHeap.offer(max); }else{ //个数为奇数的话,则先插入到小顶堆,然后将小顶堆中最小的数插入大顶堆中 minHeap.offer(num); int min = minHeap.poll(); maxHeap.offer(min); } count++; } public Double GetMedian() { //当前为偶数个,则取小顶堆和大顶堆的堆顶元素求平均 if(count % 2 == 0){ return new Double(minHeap.peek() + maxHeap.peek())/2; }else{ //当前为奇数个,则直接从小顶堆中取元素即可,所以我们要保证小顶堆中的元素的个数。 return ((double)minHeap.peek()); } } }