热题100 - 295. 数据流的中位数

17 阅读2分钟

题目描述:

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

  • 例如 arr = [2,3,4] 的中位数是 3 。
  • 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。

实现 MedianFinder 类:

  • MedianFinder() 初始化 MedianFinder 对象。
  • void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
  • double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。

示例 1:

输入
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
[[], [1], [2], [], [3], []]
输出
[null, null, null, 1.5, null, 2.0]

解释
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1);    // arr = [1]
medianFinder.addNum(2);    // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3);    // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0

提示:

  • -105 <= num <= 105
  • 在调用 findMedian 之前,数据结构中至少有一个元素
  • 最多 5 * 104 次调用 addNum 和 findMedian

关键字: 美团 字节 拼多多

思路:

一开始我想的是,这不刚好是最小堆的适用场景吗。然后我手绘了一下操作流程,发现了问题。按照最小堆的入堆方式,一个较小的值是不会入堆的。但是,判断中位数,可以说每个数字都不能乱丢。虽然最小堆的“第k大的数”的思想确实可以在这里套用,但是也不能像以前一样丢弃任何数字。这时候了解到了最大堆一起使用的思想,于是就有了下面的实现:

image.png

class MedianFinder {
    int n;
    int k;
    PriorityQueue<Integer> minHeap, maxHeap;

    public MedianFinder() {
        minHeap = new PriorityQueue<>();
        maxHeap = new PriorityQueue<>((a, b) -> b - a);
        n = 0;
    }
    
    public void addNum(int num) {
        n++;
        k = (n+1)/2;
        int s = minHeap.size();
        if (minHeap.isEmpty()) {
            minHeap.offer(num);
            maxHeap.offer(num);
            return;
        }
        if (s < k) {
            int mi = minHeap.peek();
            int mx = maxHeap.peek();
            if (num > mi && num > mx) {
                maxHeap.offer(mi);
                minHeap.offer(num);
            } else if (num < mi && num < mx) {
                minHeap.offer(mx);
                maxHeap.offer(num);
            } else {
                minHeap.offer(num);
                maxHeap.offer(num);
            }
            return;
        } 
        
        if (minHeap.size() >= k && minHeap.peek() < num) {
            minHeap.poll();
            minHeap.offer(num);
        }
        if (maxHeap.size() >= k && maxHeap.peek() > num) {
            maxHeap.poll();
            maxHeap.offer(num);
        }
    }
    
    public double findMedian() {
        if (n % 2 == 0) {
            int n1 = minHeap.peek();
            int n2 = maxHeap.peek();
            return (n1+n2)/2.0;
        }
        return minHeap.peek();
    }
}

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */

这套实现还是基于之前的“第K大的数”的思想实现的。其中涉及到的比较还挺麻烦的。

DS的优化

后面我请教了一下DS,问问有没有更好的想法。果然对方提供了一个“匀一匀”的思路,实现起来更容易,而且也更简洁。

class MedianFinder {
    PriorityQueue<Integer> min;
    PriorityQueue<Integer> max;

    public MedianFinder() {
        min = new PriorityQueue<>();
        max = new PriorityQueue<>((a, b) -> b - a);  // 维护二者大小差值:max最多之比min多1. total奇数找max,偶数两个都找
    }
    
    public void addNum(int num) {
        if (!max.isEmpty() && max.peek() > num) {
            max.offer(num);
        } else {
            min.offer(num);
        }
        while (Math.abs(max.size() - min.size()) > 1) {
            if (max.size() > min.size()) {
                min.offer(max.poll());
            } else {
                max.offer(min.poll());
            }
        }
    }
    
    public double findMedian() {
        if (max.size() == min.size()) {
            return (max.peek() + min.peek())/2.0;
        }
        if (max.size() > min.size()) {
            return max.peek();
        }
        return min.peek();
    }
}

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */

其实就是先入堆,当然入堆要符合堆的人设,想进最大堆就得比堆顶小。然后while循环平衡堆。

结论: 时间复杂度O(logn),空间复杂度O(n)。