题目
中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。
进阶:
- 如果数据流中所有整数都在 0 到 100 范围内,你将如何优化你的算法?
- 如果数据流中 99% 的整数都在 0 到 100 范围内,你将如何优化你的算法?
题解
解法1:排序 + 二分查找
数据结构
列表
如何添加元素?
如果每次添加新元素都要排序,时间复杂度为O(n * logn)。
而列表本身已经是升序,可以用二分查找寻找插入位置,时间复杂度O(log n),插入后要把插入元素后面的元素全部往后移动一位。
最坏情况下插入到第一个位置,移动所有的数,所以最坏时间复杂度为O(n)。
如何取中位数?
中位数直接取列表的中间的值,时间复杂度为O(1)。
1 | class MedianFinder() { |
解法2:大顶堆 + 小顶堆
数据结构的选择
要保证查询中位数的时间复杂度是O(1),就要在插入元素的时候就保持元素有序,才能直接查询到中位数。
从数据流中不停的插入新元素并保持有序,用离线排序算法复杂度较高,考虑在线排序算法,用堆排序,API层使用优先队列,添加一个新元素调整堆的时间复杂度最多为O(log n)。
用优先队列怎么获取中位数?
中位数的定义?
假设一共有n个数:
- n为奇数时,中位数 = 第n / 2 + 1大的数。
- n为偶数时,中位数 = (第n / 2大的数 + 第n / 2 + 1大的数) / 2
优先队列怎么取元素的?
- 如果是小顶堆,只能取堆顶最小的元素。
- 如果是大顶堆,只能取堆顶最大的元素。
我们需要取第n / 2大的数和第n / 2 + 1大的数,但是却只能从堆顶取,那么:
- 用一个大顶堆存储第1大到第n / 2大的数,堆顶就是第n / 2大的数。
- 用一个小顶堆存储第n / 2 + 1大到第n大的数,堆顶就是第n / 2 + 1大的数。
- n为奇数时,大顶堆比小顶堆多存储一个数,堆顶就是中位数。
- n为偶数时,大顶堆和小顶堆元素个数一样多。
添加新元素时要怎么往优先队列里插入?
初始时,两个堆都为空,允许大顶堆比小顶堆多一个元素,一开始就插入大顶堆。
后续添加新元素,由于是要寻找所有元素中的中位数,所以添加新元素后,中位数可能会变化,两个堆中元素都要调整顺序。
新元素先添加到大顶堆,筛选出最大的数到堆顶,再取出大顶堆的堆顶元素插入小顶堆,这样就保持两个堆顶都是正确的中位数了。
如果后续添加新元素后,元素总数变成奇数,需要在上面的基础上再把小顶堆的顶堆元素取出插入大顶堆中,让大顶堆比小顶堆多一个元素。
复杂度
设元素总数为n。
添加元素时,最坏情况下需要三次插入堆,一次插入堆的时间复杂度是O(log n),三次就是3 * O(log n),所以添加元素最坏时间复杂度为O(log n)。
查询中位数时间复杂度O(1)。
空间复杂度O(n):两个优先队列存储数据流所有的数。
1 | import java.util.* |