用堆高效返回中位数

题目链接-来源:力扣(LeetCode)

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。
示例 1:

输入:
[“MedianFinder”,”addNum”,”addNum”,”findMedian”,”addNum”,”findMedian”]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]

思路:

如果要快速返回数据的最大值或者最小值,显然应该用 最大堆 或者 最小堆。
本题需要中位数,我们思考能否在堆的中间「剖开」一个接口,让我们能够像接触最大值或者最小值一样,快速地接触到中位数。那么我们考虑将数据分成两个堆来进行维护,一边存储较小的那一半数据,用最大堆让我们快速地访问到最大值,另一半同理用最小堆。这样一来,中位数只有可能出现在「最小堆」的最大值和「最大堆」的最小值中间。
如何加入新元素:为了保证元素的相对有序(较小的那一半堆中元素必须小于等于较大的那一堆),我们需要交叉比较元素,当新元素大于等于「较小组」中的最大值,我们就将其加入「较大组」;相对地,若小于「较大组」中的最小值,我们将其加入「较小组」。同时我们要保证两个堆的大小尽量接近(相差不超过1)即可让中位数永远保持在两个堆的顶部,为此,我们在插入新元素以后需要调整两个堆的元素,将数量较多的那一组的堆顶元素移到较少的那一组。
时间复杂度:O(nlogn)
空间复杂度:O(n)

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class MedianFinder {
private PriorityQueue<Integer> low;
private PriorityQueue<Integer> high;
private int size;
/** initialize your data structure here. */
public MedianFinder() {
Comparator<Integer> larger = (i, j) -> (j - i);
low = new PriorityQueue<>(larger);
high = new PriorityQueue<>();
size = 0;
}

public void addNum(int num) {
size++;
if (high.isEmpty() || num > high.peek()) {
high.offer(num);
} else {
low.offer(num);
}
int sizeDiff = low.size() - high.size();
if (sizeDiff > 1) {
high.offer(low.poll());
} else if (sizeDiff < -1) {
low.offer(high.poll());
}
}

public double findMedian() {
if (size == 0) {
return 0;
}
int sizeDiff = low.size() - high.size();
if (sizeDiff > 0) {
return low.peek();
} else if (sizeDiff < 0) {
return high.peek();
} else {
return ((double)low.peek() + (double)high.peek()) / 2;
}
}
}