剑指offer 面试题41. 数据流中的中位数(难)& LeetCode 295. 数据流的中位数(难)
由于数据是从一个数据流中读出来的,因而数据的数目随着时间的变化而增加。如果用一个数据容器来保存从流中读出来的数据,则当有新的数据从流中读出来时,这些数据就插入数据容器。这个数据容器用什么数据结构定义最合适呢?这个题的本质就是在探讨,用哪一种数据结构保存数据流时间复杂度最小。
Question
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如, [2,3,4] 的中位数是 3 [2,3] 的中位数是 (2 + 3) / 2 = 2.5 设计一个支持以下两种操作的数据结构:
void addNum(int num)
- 从数据流中添加一个整数到数据结构中。double findMedian()
- 返回目前所有元素的中位数。
示例 1: 1
2
3
4输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]1
2
3
4输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]addNum
、findMedia
进行 50000 次调用。
测试用例
功能测试(从数据流中读出奇数个数字;从数据流中读出偶数个数字)。 边界值测试(从数据流中读出0个、1个、2个数字)
本题考点
考查应聘者对时间复杂度的分析能力。 考查应聘者对数据结构的理解程度。应聘者只有对各个常用数据容器的特点非常了解,知道它们的优缺点及适用场景,才能找出最优的解法。
Intuition
由于数据是从一个数据流中读出来的,因而数据的数目随着时间的变化而增加。如果用一个数据容器来保存从流中读出来的数据,则当有新的数据从流中读出来时,这些数据就插入数据容器。这个数据容器用什么数据结构定义最合适呢?这个题的本质就是在探讨,用哪一种数据结构保存数据流时间复杂度最小。
几种容器比较
作为容器的数据结构 | addNum 的时间复杂度 |
findMeadia 时间复杂度 |
---|---|---|
没排序数组 | \(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)\) |
如何使用堆作为容器

我们注意到整个数据容器被分隔成两部分。位于容器左边部分的数据比右边的数据小。另外,\(P_1\) 指向的数据是左边部分最大的数,\(P_2\)指向的数据是左边部分最小的数。
如果能够保证数据容器左边的数据都小于右边的数据,那么即使左、右两边内部的数据没有排序,也可以根据左边最大的数及右边最小的数得到中位数。如何快速从一个数据容器中找出最大数?用最大堆实现这个数据容器,因为位于堆顶的就是最大的数据。同样,也可以快速从最小堆中找出最小数。
因此,用一个最大堆实现左边的数居容器,用一个最小堆实现右边的数据容器。往堆中插入一个数据的时间效率是 \(O(logn)\)。由于只需要 \(O(1)\) 时间就可以得到位于堆顶的数据,因此得到中位数的时间复杂度是 \(O(1)\)。
接下来考虑用最大堆和最小堆实现的一些细节。首先要保证数据平均分配到两个堆中,因此两个堆中数据的数目之差不能超过1。为了实现平均分配,可以在数据的总数目是偶数时把新数据插入最小堆,否则插入最大堆。
还要保证最大堆中的所有数据都要小于最小堆中的数据。当数据的总数目是偶数时,按照前面的分配规则会把新的数据插入最小堆。如果此时这个新的数据比最大堆中的一些数据要小,那该怎么办呢?
1 |
|
可以先把这个新的数据插入最大堆,接着把最大堆中最大的数字拿出来插入最小堆。由于最终插入最小堆的数字是原最大堆中最大的数字,这样就保证了最小堆中所有数字都大于最大堆中的数字。
Code
- 自行构造了基于
list
的大根堆和小根堆来存储字节流
1 |
|
自建heap会超出LeetCode的时间限制
- 调用系统的
heap
来存储字节流。
1 |
|
调用系统的heap就不会超出时间限制。
Push item on the heap, then pop and return the smallest item from the heap. The combined action runs more efficiently than heappush() followed by a separate call to heappop().
根据以上文档说明,可将 Python 代码优化为,效率最高! 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15from heapq import *
class MedianFinder:
def __init__(self):
self.A = [] # 小顶堆,保存较大的一半
self.B = [] # 大顶堆,保存较小的一半
def addNum(self, num: int) -> None:
if len(self.A) != len(self.B):
heappush(self.B, -heappushpop(self.A, num))
else:
heappush(self.A, -heappushpop(self.B, -num))
def findMedian(self) -> float:
return self.A[0] if len(self.A) != len(self.B) else (self.A[0] - self.B[0]) / 2.0
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!