每日一题 -- 堆
实现小顶堆(utils)
- 由于 JS 没有自带的堆结构,需要自己搞一个,所以要充分发挥自己的主观能动性,让使用堆的时候轻松愉悦
- 根据
Lucifer 的堆文章解释,在用数组模拟堆的时候,index===0 是空出来的,一般也可以用作堆长度的值,我这里就把它作为堆长度来记录了;同时他就是堆底的那个值的下标啦。
- 然后考虑到封装完以后,用堆只需要两个 api 即可,一个是出堆
heappop,一个是入堆 heappush.
出堆(小顶堆)
- 出堆就是将堆顶 node 推出去,但是推出之后还得重新整理一下堆,使得它合法,所以一般就是推出之后,用堆底 node 放到堆顶,然后递归 down 下来整理
- 数组删除中间值其实消耗资源挺多的,但是交换两个下标值和删除数组前后的值都是 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) {
this.data.push(val)
this.data[0] += 1
this.up(this.data[0])
}
Heap.prototype.heappop = function () {
this.swap(1, this.data[0])
const res = this.data.pop()
this.data[0] -= 1
this.down()
return res
}
Heap.prototype.swap = function (a, b) {
[this.data[a], this.data[b]] = [this.data[b], this.data[a]]
}
Heap.prototype.down = function (index = 1) {
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)
}
}
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时再转移,这个只是习惯问题,可以按照自己想法做,不要局限了。
- 最后,记得由于是模拟的大顶堆,所以需要入堆出堆的时候,取反,这也是为啥我对小顶堆偏爱的原因,因为这个是以小顶堆为堆模板的啊。以上。
var MedianFinder = function() {
this.minHeap = new Heap()
this.maxHeap = new Heap()
};
MedianFinder.prototype.addNum = function(num) {
this.minHeap.heappush(num)
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]
if(minLen-maxLen>1){
const val = this.minHeap.heappop()
this.maxHeap.heappush(-val)
}
if(minLen<maxLen){
const val = -this.maxHeap.heappop()
this.minHeap.heappush(val)
}
};
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) {
this.data.push(val)
this.data[0] += 1
this.up(this.data[0])
}
Heap.prototype.heappop = function () {
this.swap(1, this.data[0])
const res = this.data.pop()
this.data[0] -= 1
this.down()
return res
}
Heap.prototype.swap = function (a, b) {
[this.data[a], this.data[b]] = [this.data[b], this.data[a]]
}
Heap.prototype.down = function (index = 1) {
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)
}
}
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)
}
}