Java详解剑指offer面试题41–数据流中的中位数
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
这道题的意思是,有一个容器不断接收到流中的数据,因此该容器的大小是动态的,找出一种方法能快速得到动态容器中的中位数。
最容易想到的就是直接对容器排序,中位数就是中间的元素。但是时间复杂度为O(nlgn)太高了;
使用切分算法对一个数组进行部分排序,能以O(n)的时间找出中位数,插入容器中的时间是O(1);
使用有序数组,因为插入时候要将插入位置后的所有元素后移一位,所以时间复杂度为O(n),但是对于始终保持有序的数组来说,要找出中位数只需O(1)的时间;
使用二叉查找树可以实现以平均O(lgn)时间插入和获取,最差情况下:二叉树极度不平衡,树退化成链表,此时时间复杂度变高到了O(n)。
有没有更好的办法呢?
中位数将数组分成两部分,**中位数左边的部分比中位数右边的部分都要小,换言之:左边部分的最大值也不会超过右边部分的最小值。**要获取最大值、最小值,比较容易想到的就是最大堆和最小堆了。又注意到,被中位数分开的两个部分,其大小之差不会超过1。所以在往两个堆里存入元素时,要保证交替存入两个容器,比如:当前元素个数为奇数时,就默认存入最小堆中;当前元素个数为偶数时,就默认存入最大堆中;当前没有元素时,默认存入最大堆中。
为了保证中位数左边部分的最大值也不会超过右边部分的最小值,**应该使用最大堆存放较小元素,最小堆存放较大元素。**有两种特殊情况:
- 当前要存入最大堆中的元素比最小堆的最小值大,这样不能保证最大堆的最大值不会超过最小堆的最小值。此时需要将最小堆中的最小值弹出并存入最大堆中,并将当前元素存入最小堆中,其实就是将当前元素和最小堆的最小值交换了存储位置。
- 当前要存入最小堆的元素比最大堆的最大值小,这样也不能保证最大堆的最大值不会超过最小堆的最小值。此时需要将最大堆中的最大值弹出并存入最小堆中,并将当前元素存入最大堆中。
中位数的获取就很简单了,如果当前数据流中个数为奇数,则中位数一定是最大堆的最大值(因为上面规定了当前数据个数为偶数时存入最大堆中,之后数据个数变成奇数,因此要么最大堆的大小比最小堆一样,要么比最小堆大1);如果当前数据流中个数为偶数,那么要求平均数,这两个中间值一个是最大堆的最大值,一个是最小堆的最小值。
package Chap5;
import java.util.PriorityQueue;
import java.util.Comparator;
public class MedianInStream {
private PriorityQueue<Integer> maxPQ = new PriorityQueue<>(Comparator.reverseOrder());
private PriorityQueue<Integer> minPQ = new PriorityQueue<>();
private int count;
public void Insert(Integer num) {
if (count == 0) {
maxPQ.offer(num);
// 当前数据流为奇数个时,存入最小堆中
} else if ((count & 1) == 1) {
// 如果要存入最小堆的元素比最大堆的最大元素小,将不能保证最小堆的最小元素大于最大堆的最大元素
// 此时需要将最大堆的最大元素给最小堆,然后将这个元素存入最大堆中
if (num < maxPQ.peek()) {
minPQ.offer(maxPQ.poll());
maxPQ.offer(num);
} else {
minPQ.offer(num);
}
// 当前数据流为偶数个时,存入最大堆
} else if ((count & 1) == 0) {
// 如果要存入最大堆的元素比最小堆的最小元素大,将不能保证最小堆的最小元素大于最大堆的最大元素
// 此时需要将最小堆的最小元素给最大堆,然后将这个元素存入最小堆中
if (num > minPQ.peek()) {
maxPQ.offer(minPQ.poll());
minPQ.offer(num);
} else {
maxPQ.offer(num);
}
}
count++;
}
public Double GetMedian() {
// 当数据流读个数为奇数时,最大堆的元素个数比最小堆多1,因此中位数在最大堆中
if ((count & 1) == 1) return Double.valueOf(maxPQ.peek());
// 当数据流个数为偶数时,最大堆和最小堆的元素个数一样,两个堆的元素都要用到
return Double.valueOf((maxPQ.peek() + minPQ.peek())) / 2;
}
}
下面来比较下各个方法的效率。
数据结构 | 插入复杂度 | 得到中位数的复杂度 |
---|---|---|
没有排序的数组 | O(1) | O(n) |
有序数组 | O(n) | O(1) |
二叉查找树 | 平均O(lgn),最差O(n) | 平均O(lgn),最差O(n) |
最大堆、最小堆 | O(lg n) | O(1) |
本文参考文献:
[1]github.com/haiyusun/data-structures