什么是优先级队列?
优先级队列是队列的一个变种,队列是一个先进先出的结构,在头部出队元素在尾部入队元素,
优先级队列顾名思义就是给每个元素具备了优先级,优先级决定了元素在队列中的存储位置,优先级越高的越靠前越先出队
小顶堆又是什么?
小顶堆是堆结构的一个分支,堆分为大顶堆和小顶堆,一般数组实现就是由一个序列组成的二叉树,每个叶子节点都比子节点要大/小,最小值/最大值就是头部元素,所以堆很适合获取最值
堆的常见操作:
上浮(siftUp): 构建堆的一种方式,从堆的尾部开始挨个将节点和父节点比较,如果比父节点大/小则交换位置,重复这个过程直到符合堆的性质(父节点大于/小于子节点)后停止
下沉(siftDown):构建堆的另一种方式,从根节点开始每个节点和左右子节点中较小/大的一个交换位置,重复这个过程直到满足堆的性质时停止
添加节点(push):将节点添加到堆的尾部,然后上浮重新构建堆直到满足堆性质
删除节点(pop):将堆的最后一个节点移除掉覆盖根节点然后下沉重新构建堆直到满足堆性质并返回根节点
小顶堆实现优先级队列:
function defaultCompares(a, b) {
return a - b
}
// 小顶堆构建优先级队列
class PriorityQueue {
constructor(arr = [], compares = defaultCompares) {
if (compares) {
this.compareFn = compares
}
this.queue = arr
this.init()
}
// 构建小顶堆
init() {
// 从倒数第一个非叶子节点(从下往上,从右往左进行构建) 完成每一棵叶子结点子树的递归下沉 直到根节点停止 依次重新排列节点顺序
let lastParentNodeIndex = (this.queue.length >>> 1) - 1
for (let i = lastParentNodeIndex; i >= 0; i--) {
this.buildChildNodes(i) // 递归每个非叶子结点
}
}
// 指定索引处开始下沉
buildChildNodes(index) {
// 在数组中某个节点的左右子节点分别为 2*i+1 和 2*i+2
let arr = this.queue
let left = (index << 1) + 1
let right = (index << 1) + 2
let minIndex = index // 记录较小值
let length = arr.length // 获取队列的长度
// 找出值较小节点下标
if (left < length && this.compares(left, minIndex) < 0) {
minIndex = left
}
if (right < length && this.compares(right, minIndex) < 0) {
minIndex = right
}
// 如果不相等则产生交换
if (minIndex != index) {
// 如果最小值不是父节点的话,需要交换父子节点位置
// 并且对父节点交换后的子树递归地进行重新排列
this.swap(index, minIndex)
this.buildChildNodes(minIndex)
}
}
compares(a, b) {
return this.compareFn(this.queue[a], this.queue[b])
}
peak() {
return this.queue[0] // 获取队列的头部
}
push(val) {
// 添加一个元素然后重新构建堆
this.queue.push(val)
this.init()
}
pop() {
// pop操作,就是将根节点拿出后,用最后一个节点填充根节点然后重新构建
let val = this.queue[0] // 这样重新构建前的操作就是O(1) 如果直接把根节点shift出去再重构建则,重新构建前的操作就是O(n) shift操作会将整个数组前移一位 相当于访问元素的次数是O(n) 下标访问就是O(1)
this.queue[0] = this.queue.pop() // pop出当前最小值
this.init() // 重新构建堆 维护最新的最小值
return val
}
// 排序
sort() {
// 排序的话就是把队列一个个pop出来
var res = []
var length = this.queue.length
for (var i = 0; i < length; i++) {
res.push(this.pop()) // 每次都会得到一个最小值
}
this.queue = res // 排好序的数组更新回队列
return res
}
// 交换两个索引位置的值
swap(index1, index2) {
var arr = this.queue
;[arr[index1], arr[index2]] = [arr[index2], arr[index1]]
}
size() {
return this.queue.length
}
}
对于init方法的图解:
为啥是从最后一个非叶子结点开始下沉,其实是对每个非叶子节点及其子树的一个递归下沉的过程,从下往上从右至左直到根节点,这样所有的叶子节点及其子树都是满足堆性质的二叉树
代入前k个高频数字题测试
function topKFrequent(nums = [], k) {
const map = new Map()
for (const item of nums) {
map.set(item, (map.get(item) || 0) + 1)
}
const minHeap = new PriorityQueue([], (a, b) => a[1] - b[1])
for (const entry of map.entries()) {
minHeap.push(entry)
if (minHeap.size() > k) {
minHeap.pop()
}
}
const result = []
for (let i = minHeap.size() - 1; i >= 0; i--) {
result[i] = minHeap.pop()[0]
}
return result
}
console.log(topKFrequent([5, 3, 1, 1, 1, 3, 5, 73, 1], 3)) // [1,5,3]