文章目录
如何求一个中位数?
如果输入一个数组,让你求中位数,简单的解决方法就是对数组进行排序
如果数组长度是奇数,最中间的一个元素就是中位数
如果数组长度是偶数,最中间两个元素的平均数作为中位数。
但是如果数据规模非常巨大,排序不太现实,那么也可以使用概率算法,随机抽取一部分数据,排序,求中位数,作为所有数据的中位数。
295. 数据流的中位数
这道题就是让我们设计这样一个类:
class MedianFinder {
// 添加一个数字
public void addNum(int num) {
}
// 计算当前添加的所有数字的中位数
public double findMedian() {
}
}
常规思路:
常规的思路就是
用一个数组记录所有addNum添加进来的数字,通过插入排序的逻辑保证数组中的元素有序,当调用findMedian方法时,可以通过数组索引直接计算中位数
但是用数组作为底层容器的问题也很明显:
addNum搜索插入位置的时候可以用二分搜索算法,但是插入操作需要搬移数据,最坏时间复杂度为O(N)
因为数组的插入时间复杂度问题,考虑使用链表。
使用链表插入数据很快,但是查找插入位置的时候只能线性遍历,最坏时间复杂度为 O(N)
而且findMedian方法也需要遍历寻找中间索引,最坏时间复杂度也是 O(N)
因为链表的查找时间复杂度问题,考虑使用平衡二叉树
平衡二叉树的增删查改的复杂度都为 O(logN)
比如使用Java提供的TreeSet容器,底层是红黑树,addNum直接插入,findMedian可以通过当前元素的个数推算出计算中位数元素的排名
但是,TreeSet是一种Set其中不存在重复元素的元素,但是我们的数据流可能输入重复数据,而且计算中位数也是需要算上重复元素的
不仅如此,TreeSet并没有实现一个通过排名快速计算元素的API。也就是说加入我们想找到TreeSet中的第5大的元素我们需要手动去实现这个需求
平衡二叉树也不行,那**优先级队列(二叉堆)**可以吗?
优先级队列是一种受限的数据结构,只能从堆顶添加/删除元素,我们的addNum方法可以从堆顶插入元素,但是findMedian函数需要从数据中间取,这个功能优先级队列是没办法提供的
解决思路:
解决这个问题我们必然是会用到有序数据结构的,本题所用到的数据结构是两个优先级队列
中位数是有序数组最中间的元素
我们可以将有序数组抽象成一个倒三角形(从大到小),宽度可以视为元素的大小,那么这个倒三角形的中部就是计算中位数的元素
将这个倒三角形从中间切成两半,编程一个小倒三角形和一个梯形
这个小的倒三角形相当于一个从小到大的有序数组,这个梯形相当于一个从大到小的有序数组
他们分别可以是大顶堆和小顶堆,中位数就是他们的堆顶元素
但是梯形虽然是小顶堆,但其中的元素是较大的,我们称其为
large
,倒三角虽然是大顶堆,但是其中元素较小,我们称其为small
。当然,这两个堆需要算法逻辑正确维护,才能保证堆顶元素是可以算出正确的中位数,我们很容易看出来,两个堆中的元素之差不能超过 1。(这其实就是在限制实现addNum方法)
假设元素总数是
n
- 如果
n
是偶数,我们希望两个堆的元素个数是一样的,这样把两个堆的堆顶元素拿出来求个平均数就是中位数;- 如果
n
是奇数,那么我们希望两个堆的元素个数分别是n/2 + 1
和n/2
,这样元素多的那个堆的堆顶元素就是中位数。
因此,我们可以得到代码如下:
class MedianFinder {
private PriorityQueue<Integer> large;
private PriorityQueue<Integer> small;
public