前端就该用 JS 刷算法30

158 阅读4分钟

每日一题 -- 堆

实现小顶堆(utils)

  • 由于 JS 没有自带的堆结构,需要自己搞一个,所以要充分发挥自己的主观能动性,让使用堆的时候轻松愉悦
  • 根据 Lucifer 的堆文章解释,在用数组模拟堆的时候,index===0 是空出来的,一般也可以用作堆长度的值,我这里就把它作为堆长度来记录了;同时他就是堆底的那个值的下标啦。
  • 然后考虑到封装完以后,用堆只需要两个 api 即可,一个是出堆 heappop,一个是入堆 heappush.

出堆(小顶堆)

  • 出堆就是将堆顶 node 推出去,但是推出之后还得重新整理一下堆,使得它合法,所以一般就是推出之后,用堆底 node 放到堆顶,然后递归 down 下来整理
  • 数组删除中间值其实消耗资源挺多的,但是交换两个下标值和删除数组前后的值都是 O(1)O(1) , 所以这里是先交换堆顶和堆底的值,然后删除堆底,再从堆顶开始 down 下来
  • 需要注意的是,出堆会减少堆长度,记得为数组第一个值自减哦
  • down 的操作其实就是自顶向下,和左右子节点进行值的对比,如果存在子节点比父节点的值小,则交换父子节点的值,然后再以子节点的下标递归 down 下去

入堆

  • 入堆其实就是往堆里面塞值,所以先直接往数组加入一个值,为 heap.data[0] 自加 1
  • 然后自底向上 up 整理堆
  • 由于堆的特性只是父节点不大于子节点,而兄弟节点之间是么任何联系的,所以 up 上去的比 down 下来需要的操作更少一点

模拟大顶堆

  • 用小顶堆模拟大顶堆,只需要入堆的时候将值取反,出堆的时候返回值取反即可

一些自己的感想

  • 之所以以小顶堆作为模板,是因为深恶痛绝的第 K 大的值,维护一个 K 长度的小顶堆,在堆顶就可以直接获取到值,所以在大小顶堆之间选择小顶堆做模板
  • 感觉模拟实现堆是做堆题做难的第一步(废话,写不出来,直接后面都没法搞了),多写几十次,就能直接背下来了。。
/**
 * 实现小顶堆
 */

const Heap = function () {
    this.data = []
    // 第一个值是堆的实际长度,也就是堆末的下标
    this.data.push(0)
}


//  追加一个值
Heap.prototype.heappush = function (val) {
    // 往尾部加一个值,然后 up 上去
    this.data.push(val)
    this.data[0] += 1
    this.up(this.data[0])
}

//  弹出堆顶
Heap.prototype.heappop = function () {
    // 先将堆顶和堆最后一个值交换,删除,然后down 下来
    this.swap(1, this.data[0])
    const res = this.data.pop()
    this.data[0] -= 1
    this.down() // 默认就是从 1 开始
    return res // 将 pop 出来的值保存一下
}

Heap.prototype.swap = function (a, b) {
    [this.data[a], this.data[b]] = [this.data[b], this.data[a]]
}

// 这边第一个值就当是当前堆的长度好了 -- 默认是从根节点开始
Heap.prototype.down = function (index = 1) {
    // 已经到最后一个可以找到左右节点的第二层节点了,这样 left 和 right 就不需要再判断了
    if (index*2 > this.data[0]) return
    const left = 2 * index
    const right = 2 * index + 1
    let target = index
    if (left<=this.data[0] &&this.data[left] < this.data[target]) {
        target = left
    }
    if (right<=this.data[0] &&this.data[right] < this.data[target]) {
        target = right
    }
    if (target !== index) {
        this.swap(target, index)
        // 换的是值,还得继续往下面走
        this.down(target)
    }
    // 如果没变,证明走到这里已经整理完了,下面的子树已经是ok的了
}

Heap.prototype.up = function (index) {
    if (index < 2) return
    const fatherIndex = Math.floor(index / 2)
    // 只需要和父节点比较,兄弟节点是没有比较的价值的
    if (this.data[index] < this.data[fatherIndex]) {
        this.swap(index, fatherIndex)
        this.up(fatherIndex)
    }
}

295. 数据流的中位数

295. 数据流的中位数

分析

  • 这里求的是动态去中位数,其实就是动态求极值的变形,所以用堆是比较好的解决办法。
  • 这里有两种情况,一个是奇数输入的时候,中位数就是排好序的 (n+1)/2 , 如果是偶数,则需要取两个值,然后取平均
  • 所以如果用大小两个堆来存储所有的输入,那么就可以忽略数组长度,只需要比较两个堆的长度,然后取响应的堆顶值进行比较
  • 我们最后的目的是希望拿到两个长度相差不超过 1 的堆,目的就是,堆顶能代表数组中位数或附近的值
  • 大顶堆保存的是小的一半值(排序后,中位数左边那部分),小顶堆保存的是大的一半值(排序后,中位数右边那部分),这样中位数就比较好求了
    • 我这边是先走小顶堆,且如果大顶堆的长度超过了小顶堆,就需要转移大顶堆的最大值到小顶堆上
    • 所以如果两个堆的长度不相等,即小顶堆多出来一个,那么小顶堆的堆顶就是所求
    • 如果两个堆的长度相等,则各自取出堆顶值取平均
  • 需要注意的是,
    • 每次 addNum 都是先走小顶堆是我自己对 K 大的执著,你也可以先走大顶堆
    • 加入小顶堆后,堆顶值必须保证大于等于大顶堆的堆顶,不然就要转移了
    • 大顶堆的长度超过了小顶堆的时候,自动转移回小顶堆,是为了只做一次相差超过 1 的判断,即只有当 addNum 触发,先到小顶堆,长度差超 1 了,就转移;
    • 如果不自动转移,也可以判断当大顶堆长度大于小顶堆长度超1时再转移,这个只是习惯问题,可以按照自己想法做,不要局限了。
  • 最后,记得由于是模拟的大顶堆,所以需要入堆出堆的时候,取反,这也是为啥我对小顶堆偏爱的原因,因为这个是以小顶堆为堆模板的啊。以上。
// 295. 数据流的中位数
// https://leetcode-cn.com/problems/find-median-from-data-stream/


var MedianFinder = function() {
    this.minHeap = new Heap() // 存的是大的数,所以堆顶就是第 n 大
    this.maxHeap = new Heap() // 存的是小的数,所以堆顶就是第 n 小
};

/** 
 * @分析
 * 1. 大小顶堆的长度最多相差一个
 * 2. 小顶堆堆顶 >= 大顶堆的堆顶 
 */
MedianFinder.prototype.addNum = function(num) {
    // 都是先走小顶堆,等到小顶堆的长度大于大顶堆长度的 2 个的时候,就会取出小顶堆的堆顶,放到大顶堆的堆顶中
    this.minHeap.heappush(num)
    // 是否需要移动主要有两个情况
    // 1. 小顶堆的堆顶是否能够大于等于大顶堆的堆顶
    if(this.minHeap.data[1]< -this.maxHeap.data[1]){
        // 证明需要将这个值移动的大顶堆处理了
        const val = this.minHeap.heappop()
        this.maxHeap.heappush(-val)
    }

   
    const maxLen = this.maxHeap.data[0]
    const minLen = this.minHeap.data[0]
    // 2. 当小顶堆的长度比大顶堆大2个以上,就需要移动,保证两边长度差在 1
    if(minLen-maxLen>1){
        // 从小顶堆拿到的堆顶,其实已经是大顶堆的堆顶最大值了
        const val = this.minHeap.heappop()
        this.maxHeap.heappush(-val)
    }
     // 因为要先走小顶堆,所以必须保证下一次执行 add 的时候,小顶堆的长度较高
    // 不然小顶堆新增一个值,那么根据长度来判定是否需要移动的判定条件就会失效
    // 如果不做这个处理,那么第一种判断就会使得 maxLen 的长度不断加大,这个时候也是需要根据长度来将小顶堆向大顶堆转移
    if(minLen<maxLen){
        // 从大顶堆中取回值
        const val = -this.maxHeap.heappop()
        this.minHeap.heappush(val)
    }
};

/**
 * @return {number}
 */
MedianFinder.prototype.findMedian = function() {
    const maxLen = this.maxHeap.data[0]
    const minLen = this.minHeap.data[0]
    if(maxLen === minLen){
        return (-this.maxHeap.data[1]+this.minHeap.data[1])/2
    }else{
        // 先走的小顶堆,所以如果是技术的话,就是小顶堆的堆顶
        return this.minHeap.data[1]

    }

};



/**
 * 实现小顶堆
 */

const Heap = function () {
    this.data = []
    // 第一个值是堆的实际长度,也就是堆末的下标
    this.data.push(0)
}


//  追加一个值
Heap.prototype.heappush = function (val) {
    // 往尾部加一个值,然后 up 上去
    this.data.push(val)
    this.data[0] += 1
    this.up(this.data[0])
}

//  弹出堆顶
Heap.prototype.heappop = function () {
    // 先将堆顶和堆最后一个值交换,删除,然后down 下来
    this.swap(1, this.data[0])
    const res = this.data.pop()
    this.data[0] -= 1
    this.down() // 默认就是从 1 开始
    return res // 将 pop 出来的值保存一下
}

Heap.prototype.swap = function (a, b) {
    [this.data[a], this.data[b]] = [this.data[b], this.data[a]]
}

// 这边第一个值就当是当前堆的长度好了 -- 默认是从根节点开始
Heap.prototype.down = function (index = 1) {
    // 已经到最后一个可以找到左右节点的第二层节点了,这样 left 和 right 就不需要再判断了
    if (index*2 > this.data[0]) return
    const left = 2 * index
    const right = 2 * index + 1
    let target = index
    if (left<=this.data[0] &&this.data[left] < this.data[target]) {
        target = left
    }
    if (right<=this.data[0] &&this.data[right] < this.data[target]) {
        target = right
    }
    if (target !== index) {
        this.swap(target, index)
        // 换的是值,还得继续往下面走
        this.down(target)
    }
    // 如果没变,证明走到这里已经整理完了,下面的子树已经是ok的了
}

Heap.prototype.up = function (index) {
    if (index < 2) return
    const fatherIndex = Math.floor(index / 2)
    // 只需要和父节点比较,兄弟节点是没有比较的价值的
    if (this.data[index] < this.data[fatherIndex]) {
        this.swap(index, fatherIndex)
        this.up(fatherIndex)
    }
}