@liweiwei1419
2019-02-10T15:14:27.000000Z
字数 3185
阅读 1633
堆
传送门:295. 数据流的中位数。
同《剑指 Offer》(第 2 版)第 41 题:数据流中的中位数。
中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
- void addNum(int num) - 从数据流中添加一个整数到数据结构中。
- double findMedian() - 返回目前所有元素的中位数。
示例:
addNum(1)addNum(2)findMedian() -> 1.5addNum(3)findMedian() -> 2进阶:
- 如果数据流中所有整数都在 0 到 100 范围内,你将如何优化你的算法?
- 如果数据流中 99% 的整数都在 0 到 100 范围内,你将如何优化你的算法?
分析:

借助两个堆,一个小顶堆,一个大顶堆。任何时刻,两个堆中应该始终保持的性质:
1、小顶堆中最小的元素,都不会小于大顶堆中的元素;
2、小顶堆的元素数目或者与大顶堆的元素数组相等,或者多 。
因此,我们可以针对当前元素的个数进行分类讨论:
1、当前元素个数为偶数的时候,比如一开始的时候,读入一个元素(最终应该是小顶堆中多一个元素),此时元素个数是奇数,我们应该返回小顶堆中的堆顶元素;
如何保持性质:因为最终应该是小顶堆中多一个元素,所以先经过大顶堆,然后再到小顶堆。
2、当前元素个数为奇数的时候,此时再读入一个元素(最终应该是大顶堆中多一个元素),元素个数是偶数,我们应该返回大顶堆中的堆顶元素和小顶堆中的堆顶元素的平均数。
如何保持性质:因为最终应该是大顶堆中多一个元素,所以先经过小顶堆,然后再到大顶堆。
注意:Python 中的 heap 只有小顶堆,在构造大顶堆的时候,要绕一个弯子,把相反数 push 进去,pop 出来的时候也要取相反数。
Python 代码:第 1 版
import heapqclass Solution:def __init__(self):self.min_heap = []self.max_heap = []self.count = 0def insert(self, num):""":type num: int:rtype: void"""self.count += 1if self.count % 2 == 1:heapq.heappush(self.max_heap, -num)temp = -heapq.heappop(self.max_heap)heapq.heappush(self.min_heap, temp)else:heapq.heappush(self.min_heap, num)temp = heapq.heappop(self.min_heap)heapq.heappush(self.max_heap, -temp)# print(self.min_heap)# print(self.max_heap)def getMedian(self):""":rtype: float"""if self.count % 2 == 1:return self.min_heap[0]else:return (self.min_heap[0] + (-self.max_heap[0])) / 2if __name__ == '__main__':solution = Solution()solution.insert(1)result = solution.getMedian()print(result)solution.insert(2)result = solution.getMedian()print(result)solution.insert(3)result = solution.getMedian()print(result)solution.insert(4)result = solution.getMedian()print(result)
说明:这版代码的问题是,有分支。这两个分支其实我们可以合并。
1、奇数偶数的判断可以使用位运算:count & 1 == 1 可以判断 count 是不是奇数;
2、上面的代码写得有一些烦琐,我们注意到,反正小顶堆中的元素一定 >= 大顶堆中的元素,我们可以把顺序统一一下,如果当前是奇数的话,新来的一个数,先进入小顶堆,然后小顶堆中出一个数再进入大顶堆,这样小顶堆和大顶堆元素数目就一样了。那如果当前是偶数,最终效果是小顶堆多一个元素,还按照刚刚那个流程,我们从大顶堆再拿一个元素放回小顶堆就可以了。
这里要介绍一个计算机处理多分支问题的思路:
1、先随机(任意)找一个分支运行下去,同时再判断应该走哪个分支;
2、如果之前随便走的那个分支是对的,那没问题;
3、如果之前走的那个分支是错的,再运行那个正确的分支。
于是,我们可以再写一版代码:
Python 代码:
import heapqclass Solution:def __init__(self):self.min_heap = []self.max_heap = []self.count = 0def insert(self, num):""":type num: int:rtype: void"""# 当前是奇数的时候,直接"最小堆" -> "最大堆",就可以了# 此时"最小堆" 与 "最大堆" 的元素数组是相等的# 当前是偶数的时候,"最小堆" -> "最大堆"以后,最终我们要让"最小堆"多一个元素# 所以应该让 "最大堆" 拿出一个元素给 "最小堆"heapq.heappush(self.min_heap, num)temp = heapq.heappop(self.min_heap)heapq.heappush(self.max_heap, -temp)if self.count & 1 == 0:temp = -heapq.heappop(self.max_heap)heapq.heappush(self.min_heap, temp)self.count += 1# print(self.min_heap)# print(self.max_heap)def getMedian(self):""":rtype: float"""if self.count & 1 == 1:return self.min_heap[0]else:return (self.min_heap[0] + (-self.max_heap[0])) / 2
总结:1、小顶堆的值都大于或者等于大顶堆的值;
2、具体的操作可以是:先进入一个元素个数保持不变的堆,再从这个堆里出一个元素。
如果语言带的库函数里就有直接的大顶堆,代码写出来就比较工整了,例如下面的 Java 语言版本:
Java 代码:
class Solution {PriorityQueue<Integer> maxheap = new PriorityQueue<>((x,y)->y-x);PriorityQueue<Integer> minheap = new PriorityQueue<>();public void insert(Integer num) {# 大顶堆先进一个元素maxheap.offer(num);# 然后从大顶堆里出一个元素到小顶堆minheap.offer(maxheap.poll());if(maxheap.size() < minheap.size()){# 如果大顶堆的元素少于小顶堆# 就要从小顶堆出一个元素到大顶堆maxheap.offer(minheap.poll());}}public Double getMedian() {if (maxheap.size() == minheap.size()){return (double)(maxheap.peek() + minheap.peek())/2;}else{return (double) maxheap.peek();}}}
(本节完)