Java详解剑指offer面试题41--数据流中的中位数

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐李同学(李俊德-大连理工大学)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值