船长表情包镇楼
堆:通常是一个可以被看做一棵完全二叉树的数组对象。
- 大顶堆:根节点值最大,任意子节点均小于父节点
- 大顶堆:根节点值最小,任意子节点均大于父节点
- 而完全二叉树可以使用一段连续的存储空间表示(数组)
尾部插入调整
- 大顶堆:插入的值会放在完全二叉树中最后一层最左侧,或者放在数组的末尾,然后跟父节点比较大小,如果比父节点大,则跟父节点交换位置
- 小顶堆:插入的值会放在完全二叉树中最后一层最左侧,或者放在数组的末尾,然后跟父节点比较大小,如果比父节点小,则跟父节点交换位置
头部弹出调整
- 大顶堆:弹出根节点,将尾节点放入根节点位置上,该节点将与较大的子节点交换位置直到符合大顶堆的性质
- 大顶堆:弹出根节点,将尾节点放入根节点位置上,该节点将与较小的子节点交换位置直到符合小顶堆的性质
堆排序
使用大顶堆,堆顶元素弹出与堆尾元素互换,第二次弹出与倒数第二个堆尾元素互换,依次类推,最终可以获得一个从小到大的有序数组
堆与优先队列
堆是优先队列的一种实现方式
核心应用
- 求集合的最值
船长金句:数据结构分为两部分,一部分是结构定义,另外一部分是结构操作。数据结构就是定义一种性质,然后维护这种性质
使用js手撕一个堆
class Heap {
constructor(data, type, compartor) {
this.data = data || []
// 比较规则,不传比较规则会有一个默认规则,可以指定类型,传入less是大顶堆,
// 传入自定义比较规则则type失效
this.compartor = compartor || function(a, b) {
if (type == 'less') return a - b
else return b - a
}
this.heapify()
}
size() {
return this.data.length
}
// 处理元素顺序
heapify() {
if (this.size() < 2) {
return
}
for (let i = 1; i < this.size(); i++) {
this.bubbleUp(i)
}
}
// 获取堆顶元素
top() {
if (!this.size()) return null
return this.data[0]
}
// 添加元素操作
push(val) {
this.data.push(val)
this.bubbleUp(this.size() - 1)
}
// 弹出堆顶元素
pop() {
if (!this.size()) return null
if (this.size() == 1) return this.data.pop()
let res = this.data[0]
this.data[0] = this.data.pop()
if (this.size()) {
this.bubbleDown(0)
}
return res
}
// 交换元素
swap(i, j) {
if (i === j) return
[this.data[i], this.data[j]] = [this.data[j], this.data[i]]
}
// 向上调整,最⾼调整到0号位置
bubbleUp(index) {
while (index) {
//获取到当前节点的⽗节点,
const parenIndex = (index - 1) >> 1;
// const parenIndex = Math.floor((index - 1) / 2);
// const parenIndex = (index - 1) / 2 | 0;
//⽐较⽗节点的值和当前的值哪个⼩。
if (this.compartor(this.data[index], this.data[parenIndex]) > 0) {
// 交换⽗节点和⼦节点
this.swap(index, parenIndex);
// 交换⽗节点和⼦节点下标
index = parenIndex;
} else {
//防⽌死循环。
break;
}
}
}
//获取到最⼤的下标,保证不会交换出界。
bubbleDown(index) {
let lastIndex = this.size() - 1;
while (index < lastIndex) {
//获取左右⼉⼦的下标
let leftIndex = index * 2 + 1;
let rightIndex = index * 2 + 2;
// 待交换节点
let findIndex = index;
if (leftIndex <= lastIndex
&& this.compartor(this.data[leftIndex], this.data[findIndex]) > 0) {
findIndex = leftIndex;
}
if (rightIndex <= lastIndex && this.compartor(this.data[rightIndex], this.data[findIndex]) > 0) {
findIndex = rightIndex;
}
if (index !== findIndex) {
this.swap(index, findIndex);
index = findIndex;
} else {
break;
}
}
}
}
LeetCode肝题(以训练为目的)
- 剑指 Offer 40. 最小的k个数
// 创建一个大顶堆, 如果堆的长度大于k则弹出,剩余的就是最小的k个数
var getLeastNumbers = function(arr, k) {
if (k == 0) return []
let h = new Heap(arr, 'less')
while(h.size() > k) h.pop()
return h.data;
};
-
- 最后一块石头的重量
// 创建一个大顶堆, 每次弹出俩堆顶元素相减,大于0再放入堆中,直到只剩一个元素
var lastStoneWeight = function(stones) {
let h = new Heap(stones, 'less')
while(h.size() > 1) {
let big = h.pop(), small = h.pop()
if (big - small) h.push(big - small)
}
return h.top()
};
-
- 数据流中的第 K 大元素
// 创建一个小顶堆, 堆的长度大于k则弹出,堆顶元素就是第k大的元素
var KthLargest = function(k, nums) {
this.h = new Heap(nums, 'greator')
this.k = k
};
KthLargest.prototype.add = function(val) {
this.h.push(val)
while(this.h.size() > this.k) this.h.pop()
return this.h.top()
};
-
- 查找和最小的K对数字
// 自定义一个大顶堆的比较规则, 比较的是a数组的两项之和与b数组的两项之和
var compartor = function(a, b) {
return a[0] + a[1] - b[0] - b[1]
}
var kSmallestPairs = function(nums1, nums2, k) {
let h = new Heap([], 'less', compartor)
for(let i in nums1) {
for(let j in nums2) {
// 将所有的数字排列放进大顶堆中,比较任意两数之和,和较小的k组会留到大顶堆中
h.push([nums1[i], nums2[j]])
if(h.size() > k) h.pop()
}
}
return h.data
};
-
- 数组中的第K个最大元素
// 创建一个小顶堆, 堆的长度大于k则弹出,堆顶元素就是第k大的元素
var findKthLargest = function(nums, k) {
let h = new Heap(nums, 'greator')
while(h.size() > k) h.pop()
return h.top()
};
-
- 前K个高频单词
// 自定义一个小顶堆的比较规则, 比较的是节点的数值和单词的优先
var topKFrequent = function(words, k) {
let obj = {}
// 先统计所有单词出现的次数
for(let item of words) {
obj[item] = obj[item] + 1 || 1
}
// 自定义比较规则, 当次数不相等时次数小的往上走
// 当单词次数相等时,比较单词的大小即单词的先后顺序,排在后面的单词优先弹出
// 当sort排序时,a>b的情况又是升序排序,符合题意
let compartor = function(a, b) {
if(obj[a] == obj[b]) return a > b ? 1:-1
return obj[b] - obj[a]
}
let h = new Heap(Object.keys(obj), 'greator', compartor)
while(h.size() > k) h.pop()
h.data.sort(compartor)
return h.data
};
-
- 数据流的中位数
// 维护两个堆,数据的前半段用大顶堆表示,数据的后半段用小顶堆表示
// 设定第一个堆最多比第二个堆多一个元素,否则调整数量
// 当堆的元素数量相等时,中位数就是两个堆的堆顶元素/2,不相等时就是第一个堆的堆顶元素
var MedianFinder = function() {
this.lessH = new Heap([], 'less')
this.greatorH = new Heap([], 'greator')
};
/**
* @param {number} num
* @return {void}
*/
MedianFinder.prototype.addNum = function(num) {
if (this.lessH.size() == 0 || num <= this.lessH.top()) {
this.lessH.push(num)
} else {
this.greatorH.push(num)
}
if(this.lessH.size() < this.greatorH.size()) {
this.lessH.push(this.greatorH.pop())
}
if (this.lessH.size() - this.greatorH.size() > 1) {
this.greatorH.push(this.lessH.pop())
}
};
/**
* @return {number}
*/
MedianFinder.prototype.findMedian = function() {
if (this.lessH.size() == this.greatorH.size()) {
return (this.lessH.top() + this.greatorH.top())/2
}
return this.lessH.top()
};
-
- 丑数 II
// 维护一个小顶堆
var nthUglyNumber = function(n) {
let h = new Heap([1], 'greator')
while(--n) {
// 取出堆顶元素top
let top = h.pop()
// 如果是5的整数倍则只push top*5
if (top % 5 === 0) {
h.push(top * 5)
// 如果是3的整数倍则push top*5和top*3
} else if (top % 3 === 0) {
h.push(top * 5)
h.push(top * 3)
} else {// 否则三个都push进堆里,这是为了防止添加重复元素
h.push(top * 5)
h.push(top * 3)
h.push(top * 2)
}
}
return h.top()
};
-
- 超级丑数
// 创建一个数组p记录乘数的位置,创建一个数组data记录所有丑数
var nthSuperUglyNumber = function(n, primes) {
let p = new Array(primes.length).fill(0)
let data = [1], ans = 1
while(data.length < n) {
ans = primes[0] * data[p[0]]
// 得出所有丑数取最小值赋值给ans
for(let i = 1; i< primes.length; i++) {
ans = Math.min(ans, primes[i] * data[p[i]])
}
// 将所有能得出ans的丑数下标+1,更新坐标的同时也能去除重复值
for(let i = 0; i< primes.length; i++) {
if(primes[i] * data[p[i]] === ans) p[i]++
}
data.push(ans)
}
return ans
};
-
- 移除石子的最大得分
// 先排个序, a最小,c最大
var maximumScore = function(a, b, c) {
let ans = 0
if (a > b) [a, b] = [b, a]
if (a > c) [a, c] = [c, a]
if (b > c) [b, c] = [c, b]
if (c - b > a) { // 如果b和c的差大于a,则最终得分为b+c
ans = b + a
} else { // 否则先将bc的差抵消掉a,再将a的一半分别抵消掉b和c,最终再加上b或者c
ans = c - b
a -= ans
let i = a >> 1
b -= i
ans += i * 2
ans += b
}
return ans
};
-
- 设计推特
// 设计一个大顶堆,比较的是第三项时间
// 设计一个用户集合,key为用户id,value为关注列表
// 这题用排序集合更加简答,后期再优化
var cmp = function(a, b) {
return a[2] - b[2]
}
var Twitter = function() {
this.user = {}
this.twitter = new Heap([], 'less', cmp)
this.time = 0
};
// 发送推特,放进堆中
Twitter.prototype.postTweet = function(userId, tweetId) {
this.twitter.push([userId, tweetId, this.time++])
};
// 关注,往用户的关注列表push被关注的用户id
Twitter.prototype.follow = function(followerId, followeeId) {
this.user[followerId] ? this.user[followerId].push(followeeId) : this.user[followerId] = [followeeId]
};
// 取关,往用户的关注列表删除被关注的用户id
Twitter.prototype.unfollow = function(followerId, followeeId) {
if (!this.user[followerId]) return
this.user[followerId].splice(this.user[followerId].findIndex(item => item == followeeId), 1)
};
// 获取推特,如果堆为空返回空数组,设计一个Set集合存入用户和他的关注列表,如果堆顶推特的id在Set里则push该推特进ans里,最后返回ans
Twitter.prototype.getNewsFeed = function(userId) {
if(this.twitter.size() == 0) return []
let list = [...(this.user[userId]||[]), userId], ans = [], n = 10, data = JSON.parse(JSON.stringify(this.twitter.data))
let userSet = new Set(list)
while(n && this.twitter.size()) {
let top = this.twitter.pop()
if (userSet.has(top[0])) {
ans.push(top[1])
n--
}
}
this.twitter = new Heap(data, 'less', cmp)
return ans
};
-
- 积压订单中的订单总数
// 阅读理解题,给一个订单数组,每个元素的第一项是价格,第二项是数量,第三项是类别,0是购买订单,1是销售订单
// 维护两个堆,购买堆是大顶堆,销售堆是小顶堆,堆元素是一个数组,第一项为价格,第二项为数量
// 用最大的购买价格匹配最低的销售价格(奸商就完事了)
// 自定义两个堆的比较规则
var compartor1 = function(a, b) {
return a[0] - b[0]
}
var compartor2 = function(a, b) {
return b[0] - a[0]
}
var getNumberOfBacklogOrders = function(orders) {
let buy = new Heap([], 'less', compartor1), sell = new Heap([], 'greator', compartor2)
for(let x of orders) {
if (x[2] == 0) {
while (x[1] != 0 && sell.size() && sell.data[0][0] <= x[0]) {
let cnt = Math.min(x[1], sell.data[0][1])
x[1] -= cnt
sell.data[0][1] -= cnt
if (sell.data[0][1] == 0) sell.pop()
}
if (x[1] != 0) buy.push(x)
} else {
while (x[1] != 0 && buy.size() && buy.data[0][0] >= x[0]) {
let cnt = Math.min(x[1], buy.data[0][1])
x[1] -= cnt
buy.data[0][1] -= cnt
if (buy.data[0][1] == 0) buy.pop()
}
if (x[1] != 0) sell.push(x)
}
}
let mod = 1000000007, sum = 0
for(let item of buy.data) {
sum = (sum + item[1]) % mod
}
for(let item of sell.data) {
sum = (sum + item[1]) % mod
}
return sum
};