【LeetCode刷题】NO.42---第295题

116 阅读3分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

一.题目

295. 数据流的中位数 中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。例如,[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5 设计一个支持以下两种操作的数据结构:

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

二、思路分析:

根据题目要求,既要保证数据是有序的,又要保证添加数据后时间复杂度不会太高而且能够直接排序,这种动态数据最好的解决方案就是。因为题目要求我们求出中位数,若我们对于这种动态数据问题采取暴力的算法肯定是超时的,所以我们就想到了利用两个堆来解决问题。

一个为最大堆,一个为最小堆,最大堆存放数据流种的较小的元素,这样堆顶元素就是较小元素的最大值,而最小堆则存放数据流中的较大值,这样能够保证堆顶取到的是较大元素的最小值。

那么我们如何能够保存两个堆的平衡呢?这就需要我们保证两个堆的元素数量之差不能够超过1,我们需要维护最小堆里的元素全部大于等于最大堆里面的元素,现在我们假定最大堆的元素数总是跟最小堆的元素数相等或者比最小堆的元素数小1,所以每次添加元素的时候,我们都先往最大堆里面放,然后弹出最大堆的栈顶元素加入最小堆中去,如果发现最大堆的元素数比最小堆的小,则弹出最小堆的栈顶元素加入最大堆。

这样既能够保证最小堆中的所有元素都是大于等于最大堆的元素,又能保证两个堆中的元素数差不会超过1,对于取中位数的时候只需要看两个堆的元素数是否相等,如果相等则两边都取堆顶元素除以2,否则直接取最小堆的堆顶元素即可。

三、代码:

var MedianFinder = function() {
    //默认最大堆
    const defaultsortFun = (a,b) => a>b
    //交换元素
    const swap = (arr,i,j) => ([arr[i],arr[j]] = [arr[j],arr[i]])
    class Heap{
        //sortFun表示最大堆或者最小堆
        constructor(sortFun = defaultsortFun){
            this.container = []
            this.sortFun = sortFun
        }
        size(){
            return this.container.length
        }
        insert(value){
            const {container,sortFun} = this
            container.push(value)
            let index = this.size() - 1
            while(index){
                let parent = (index - 1) >> 1
                if(sortFun(container[parent],container[index])){
                    return 
                }
                swap(container,parent,index)
                index = parent
            }
        }
        pop(){
            const {container,sortFun} = this
            if(!this.size()) return null
            swap(container,0,this.size()-1)
            const res = container.pop()
            const length = this.size()
            let index = 0 , exchange = index * 2 + 1
            while(exchange < length){
                //如果有右节点,右节点值大于左节点
                let right = index * 2 + 2
                //先做左右节点判断
                if(right < length && sortFun(container[right],container[exchange])){
                    exchange = right
                }
                //随后父节点跟子节点判断
                if(sortFun(container[index],container[exchange])){
                    break
                }
                swap(container,exchange,index)
                index = exchange
                exchange = 2 * index + 1
            }
            return res
        }
        peek(){
            //获取堆顶
            if(this.size()) return this.container[0]
            return null
        }
    }
    //最大堆存放元素较小的
    this.maxHeap = new Heap()
    //最小堆存放元素较大的
    this.minHeap = new Heap((a,b)=>a<b)
};

/** 
 * @param {number} num
 * @return {void}
 */
MedianFinder.prototype.addNum = function(num) {
    if(this.maxHeap.size() >= this.minHeap.size()){
        this.maxHeap.insert(num)
        this.minHeap.insert(this.maxHeap.pop())
    }else{
        this.minHeap.insert(num)
        this.maxHeap.insert(this.minHeap.pop())
    }
};

/**
 * @return {number}
 */
MedianFinder.prototype.findMedian = function() {
    let maxLen = this.maxHeap.size()
    let minLen = this.minHeap.size()
    return minLen > maxLen ? this.minHeap.peek() : (this.minHeap.peek() + this.maxHeap.peek())/2
};

四、总结:

这道题目的思路非常巧妙,一般不会想到用堆来维护数组的有序,对于这种动态数据排序的问题,用堆的方法解决能够使时间复杂度是非常小的。