题目描述:
如何得到一个数据流中的中位数
?如果从数据流中读出奇数个数值
,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值
,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
解题思路:
方法1:使用无序数组存储,然后需要对数组进行排序,然后就可以顺利找到中位数。
方法2:在插入数组时,就进行排序,形成有序的数组,排完序后就可以直接找到中位数。
方法3:使用树结构,二叉搜索树或者平衡二叉树,都是在插入的时候完成排序。
方法4:使用两个堆:最大堆和最小堆。
完整代码:
方法1:使用无序数组,直接调用库进行排序
import java.util.*;
public class MedianOfStream_JZ {
/**
* 数据流中的中位数
*
* @param num
*/
// 方法1
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 (array.get(index - 1) + array.get(index)) / 2.0;
}
}
方法2:插入的时候排序,即利用堆排序,每次加入一个元素就调整堆
// 方法2
private List<Integer> list = new LinkedList<>();
public void Insert2(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 - first) / 2;
if (list.get(mid) > num) {
last = mid - 1;
} else {
first = mid + 1;
}
}
list.add(first, num);
return;
}
public Double GetMedian2() {
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.*;
public class Solution {
private 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,则中位数等于小顶堆的最小数字。
插入步骤为:
1. 如果已读取的个数为偶数(包括0)时,两个堆的数目已经相同,再插入一个数时,应该选一个数插入到小顶堆中,从而实现小顶堆的个数多1。但是,不能直接插入到小顶堆,本应该选择一个数加入到小顶堆中,但是必须选一个较大的数放入小顶堆,而插入的这个数不一定符合要求,所以这个数要和大顶堆的最大数比较,哪个较大,就把哪个放进小顶堆。
2. 如果已读取的个数为奇数,小顶堆的个数多1,则要将某个数插入到大顶堆中,所以新进来的数要和小顶堆的堆顶元素比较,更小的那个元素进入大顶堆。
这里使用PriorityQueue实现,默认情况下,PriorityQueue是一个小顶堆,因此需要自己定义比较器实现大顶堆.
import java.util.*;
public class Solution {
private PriorityQueue<Integer> minHeap = new PriorityQueue<>();
private PriorityQueue<Integer> maxHeap = new PriorityQueue<>((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 (minHeap.peek() + maxHeap.peek()) / 2.0;
} else {
// 当前为奇数个,则直接从小顶堆中取元素即可,所以保证小顶堆中元素的个数
return minHeap.peek() * 1.0;
}
}
}