剑指offer-面试题41:数据流中的中位数

题目描述:如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

题目分析:首先题目要求是从数据流中读取一个数据,这也就意味着,数据容器中的数据是在不断变化的,因此这里首先要考虑的一个问题就是:在将新读取到的数据插入到数据容器中时,要保证其时间效率下表是不同的数据结构下,所需要的时间复杂度

数据结构 插入的时间复杂度 得到中位数的时间复杂度
没有排序的数组 O(1) O(n)
排序的数组 O(n) O(1)
排序的链表 O(n) O(1)
二叉搜索树 平均O(logn),最差O(n) 平均O(logn),最差O(n)
AVL(平衡的二叉搜索树) 平均O(logn) 平均O(1)
最大堆和最小堆 平均O(logn) 平均O(1)

在这里不使用AVL树,而使用最大堆和最小堆的原因在于,在面试时短时间内,不太可能构造出一颗适合本例题的AVL树,因此此处使用最大、最小堆。

如上图所示,如果数据已经在容器中有序,并且如果容器中数据的个数为偶数,那么中位数可以由P1和P2指向的数求平均得到,如果为奇数,则中位数为P1指向的数。

我们可以发先容器被封分割成了两个部分。位于容器左边的数据比右边的数据小。P1指向的是左边的最大数,P2指向的是右边的最小数。基于以上思路:用一个最大堆实现左边的数据容器,用一个最小堆实现右边的数据容器。

具体代码如下:
import java.util.PriorityQueue;
import java.util.Comparator;
public class Solution {
   PriorityQueue<Integer> minHeap = new PriorityQueue<Integer>();//优先队列默认为小顶堆
   //通过比较器,实现大顶堆
   PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(11,new Comparator<Integer>(){
   public int compare(Integer i, Integer j)
   {
       return j-i;
   }
   });
    public void Insert(Integer num) {
        //如果已经读取到的数为偶数个,则下一个读取到的数将放入小顶堆中
        if(((minHeap.size()+ maxHeap.size())&1)==0)//已经读取到的数为偶数个,下一个读进来变为奇数个
        {
        //判断如果大顶堆不为空,并且插入的数字比大顶堆最大的数字小
             if(!maxHeap.isEmpty() && maxHeap.peek() > num)
             {
                  //首先将数据插入到大顶堆中
                  maxHeap.offer(num);
                  num = maxHeap.poll();
             }
                   //如果maxHeap.peek()<num,将新读取到的数字插入小顶堆中
                  minHeap.offer(num);
        }
        else
        {
        //如果已经读取到的数为奇数个,即小顶堆数量比大顶堆多一,将新读取到的数插入到大顶堆中
            if(!minHeap.isEmpty()&& minHeap.peek()<num)
            {
                minHeap.offer(num);
                num = minHeap.poll();
            }
            //如果MinHeap.peek() > num,则直接插入大顶堆中
            maxHeap.offer(num);
        }
    }

    public Double GetMedian() {
        if((minHeap.size()+maxHeap.size()) == 0)
        {
            throw new RuntimeException();
        }
        double median;
        if(((minHeap.size() + maxHeap.size()) & 1 )== 0 )
        {
            median = (minHeap.peek() + maxHeap.peek()) / 2.0;
        }
        else
        {
           median = minHeap.peek();
        }
        return median;
    }
}

PriorityQueue是基于堆实现的数据结构,其逻辑结构是一颗完全二叉树,存储结构其实是一个数组。PriorityQueue,也叫优先级队列,它是不同于先进先出队列的另一种队列。每次从队列中取出的是具有最高优先权的元素。
如果不提供Comparator的话,优先队列中元素默认按自然顺序排列,也就是数字默认是小的在队列头,字符串则按字典序排列。也就是说我们通过设置comparator比较器来定义优先级别。

有以下代码:

//小顶堆

public static void MinPriorityQueue()
{
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(10);
//入队
for(int i = 10; i >=5;i--)
{
queue.offer(i);

}
//遍历元素
for(Integer i : queue)
{
System.out.print(i+" ");
}
System.out.println();
}
这段代码的打印结果为:打印小顶堆中的元素:5 7 6 10 8 9 ,并不是有序的,不是按照5,6,7,8,9,10输出,原因在于:小顶堆只是保证了根节点不大于左右两个节点,但是左右两个节点谁比谁大并不能保证,也就是说队列的元素,在物理结构上是数组,是无序的。

//大顶堆
public static void MaxPriorityQueue()
{
//通过比较器实现大顶堆
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(10,new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
for(int i = 10; i >=5;i--)
{
queue.offer(i);
}
//打印
for(Integer i : queue)
{
System.out.print(i);
}
}
输出结果:10,9,8,7,6,5
队列在物理上是数组,是无序的,但是其逻辑结构是小顶堆,是有序的。那么如何让小顶堆也按照5,6,7,8,9,10输出呢?可以通过以下代码:
public static void minPriorityQueueOrder()
{
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(10);
//入队
for(int i = 10; i >=5;i--)
{
queue.offer(i);
}
//出队,采用priorityQueue中的内置方法,先使用peek()方法判断堆顶元素是否存在
//然后使用poll()方法取出堆顶元素,并将其删除
while(queue.peek() != null)
{
System.out.print(queue.poll()+ " ");
}
}

总结:这里面用到了优先队列,首先优先队列本身就是一个小顶堆,可以通过比较器的方式实现大顶堆,另外还用到了priorityQueue中的一些方法:peek()、poll()、offer();小顶堆只是保证了根节点不大于左右两个节点,但是左右两个节点谁比谁大并不能保证,也就是说队列的元素,在物理结构上是数组,是无序的;而大顶堆可以通过比较器的方式实现,并且遍历出来的顺序也同样有序。

附上完整的测试代码:

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Scanner;

public class MiddleNumber {
//定义小顶堆
PriorityQueue<Integer> minQueue = new PriorityQueue<Integer>();
//通过比较器创建大顶堆
PriorityQueue<Integer> maxQueue = new PriorityQueue<Integer>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
//将数据流中获取到的数字放入堆中
public void Insert(Integer num)
{
//如果当前读取到的数字为偶数个,则将当前数字放入小顶堆中
if(((minQueue.size() + maxQueue.size()) & 1) == 0)
{
if(!maxQueue.isEmpty() && maxQueue.peek() > num)
{
maxQueue.offer(num);
num = maxQueue.poll();
}
minQueue.offer(num);
}
//如果已经读取到的个数为奇数个,则将当前读取到的数字放入大顶堆中
else
{
if(!minQueue.isEmpty() && minQueue.peek() < num)
{
minQueue.offer(num);
num = minQueue.poll();
}
maxQueue.offer(num);
}
}
//获取中位数
public double getMediam()
{
if(minQueue.size() + maxQueue.size() == 0)
{
throw new RuntimeException();
}
if(((maxQueue.size()+minQueue.size())& 1) == 0)
{
return (minQueue.peek()+maxQueue.peek())/2.0;
}
else
{
return minQueue.peek();
}
}
public static void main(String[] args)
{
MiddleNumber middleNumber = new MiddleNumber();
Scanner sc = new Scanner(System.in);
int c = 0;
while(true)
{
System.out.println("请输入一个数字");
c = sc.nextInt();
middleNumber.Insert(c);
System.out.println("当前的中位数为:" + middleNumber.getMediam());
}
}
}
posted @ 2019-04-23 10:15  keep-the-faith  阅读(213)  评论(0编辑  收藏  举报