LeetCode 295. 数据流的中位数 堆题解,了解堆结构实例之一。

题目

295. 数据流的中位数

https://leetcode.cn/problems/find-median-from-data-stream/

中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

例如,

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

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

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

void addNum(int num) - 从数据流中添加一个整数到数据结构中。

double findMedian() - 返回目前所有元素的中位数。

使用堆维护数据流

思路

之前在 堆结构实现及使用场景 中已经提到堆结构可以用来求分位数。

设我们的数据序列维护为递增的:

  • 我们使用最大堆维护前半段小区间,堆顶此时为区间最大值;
  • 使用最小堆维护后半段大区间,堆顶此时为区间最小值;
  • 偶数个数时,两边维护元素数相同,奇数个数时,前半段维护多出来的一个元素,因此中位数为:
    • 奇数个数: maxHeap.peek()
    • 偶数个数: (maxHeap.peek+minHeap.peek) /2

核心问题:如何向两个堆中写入数据?

  • 初始时,两个堆均为空,先向最大堆写入
  • 下次进入时,再向最大堆写入,取堆顶,写入最小堆
  • 再下次,两个堆size一致,且都有数据,先向最小堆写入取出最小值,再向最大堆写入最小堆的堆顶数据

复杂度分析

时间

写:堆化复杂度O(logn)

读:取堆顶复杂度O(1)

空间

使用了额外的堆结构存储数据,空间复杂度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
42
43
44
public class MedianFinder {

    /**
     * 保存更小区间,这个区间存奇数个情况下多出来的数据
     */
    PriorityQueue<Integer> maxHeap;
    /**
     * 保存更大区间
     */
    PriorityQueue<Integer> minHeap;

    public MedianFinder() {
        maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
        minHeap = new PriorityQueue<>(Comparator.naturalOrder());
    }

    public void addNum(int num) {
        if (maxHeap.size() == minHeap.size()) {
            // 等量的情况:
            // 最大堆空时,先入最大堆
            // 最大堆不空,先入最小堆,从最小堆中取出最小元素入最大堆
            if (!maxHeap.isEmpty() && num > maxHeap.peek()) {
                minHeap.offer(num);
                maxHeap.offer(minHeap.poll());
            } else {
                maxHeap.offer(num);
            }
        } else {
            // 最大堆中小元素个数大于最小堆中的大元素个数
            // 这一步本质上是要往minHeap中补充数据,可以先入最大堆,从最大堆中取出最大元素入最小堆
            maxHeap.offer(num);
            minHeap.offer(maxHeap.poll());
        }
    }

    public double findMedian() {
        int maxInSmallerRange = maxHeap.peek() == null ? 0 : maxHeap.peek();
        int minInBiggerRange = minHeap.peek() == null ? 0 : minHeap.peek();
        if (maxHeap.size() == minHeap.size()) {
            return (maxInSmallerRange + minInBiggerRange) / 2.0;
        }
        return maxInSmallerRange;
    }
}

可以看到maxHeap.size() == minHeap.size()时,分之内代码可以简化为只保留23、24行,本质上是只往最大堆中写入元素,为了保证是整个序列的更小值,可以先往最小堆中写入,取最小堆堆顶即为整个序列的更小值,同时也是最大堆中的合法元素(num大于最大堆堆顶时,则插入了新的最大值,否则只是最大堆的中间元素)。