PS: 文中图来源于网络,如有侵权请告知删除。
在复习数据结构和算法的时候,想找找有没有优先队列的实现,却发现找了几篇文章的优先队列都是一个队列然后给元素加个权值,出队的时候扫描一遍队列然后splice
出来,感觉好像有那么一点不对。
优先队列的概念
优先队列虽然叫“队列”,但实际上它是一个堆,也就是特殊的完全二叉树。如下图:
如果堆顶的数是最小的话,那么这就是一个最小堆,反之就是一个最大堆。
优先队列所含方法
跟堆的数据结构相仿:1
2
3
4
5
6queue.size() // 返回queue里元素个数
queue.empty() // 返回queue是否为空,空则返回true,否则返回false
queue.push(k) // 在queue的末尾插入k
queue.pop() // 删掉queue的第一个元素
queue.top() // 返回queue的第一个元素
queue.back() // 返回queue的末尾元素
如果我们需要插入一个新的结点的话,新结点会先加在最后一个新结点中,并依次和父结点进行比较,最小堆情况下,如果新结点比父结点小的话,就跟父结点换位置,以此类推,一直到不能再交换位置。
实现结构
类似堆实现的话,基础结构一般就是数组或者链表,而我这里选择的是数组,主要原因有二:
1) 优先队列插入都是先插到最后的新结点位置,数组的话直接插到最后就行,不需要记录最后结点的位置。
2) 因为完全二叉树的结构,我们可以通过当前结点的索引来计算父结点的索引位置,如上上图表明的索引。
实现
下面实现一个最小堆优先队列
1 | function PriorityQueue() { |
测试通过,主要留意第一位留空,是因为我们是通过除以二的操作来计算父结点位置的,所以按索引1为起始索引更好计算。
优先队列相关练习题
leetcode 703:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16设计一个找到数据流中第K大元素的类(class)。注意是排序后的第K大元素,不是第K个不同的元素。
你的 KthLargest 类需要一个同时接收整数 k 和整数数组nums 的构造器,它包含数据流中的初始元素。每次调用 KthLargest.add,返回当前数据流中第K大的元素。
示例:
int k = 3;
int[] arr = [4,5,8,2];
KthLargest kthLargest = new KthLargest(3, arr);
kthLargest.add(3); // returns 4
kthLargest.add(5); // returns 5
kthLargest.add(10); // returns 5
kthLargest.add(9); // returns 8
kthLargest.add(4); // returns 8
说明:
你可以假设 nums 的长度≥ k-1 且k ≥ 1。
解答(使用上面定义的优先队列):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38/**
* @param {number} k
* @param {number[]} nums
*/
var KthLargest = function(k, nums) {
this.k = k
this.queue = new PriorityQueue('min')
for (let i = 0; i < nums.length ; i++) {
if (this.queue.size() < k) {
this.queue.push(nums[i])
} else {
if (this.queue.top() < nums[i]) {
this.queue.pop()
this.queue.push(nums[i])
}
}
}
return this
};
/**
* @param {number} val
* @return {number}
*/
KthLargest.prototype.add = function(val) {
let queue = this.queue
if (queue.size() < this.k) {
queue.push(val)
} else {
if (queue.top() < val) {
queue.pop()
queue.push(val)
}
}
return queue.top()
};
因为只需要返回第K大的元素,所以我们只需要维护一个size为k的最小堆,那么堆顶的元素就是第k大的元素。比k小的数都不插入堆,比k大的数就先pop出堆顶元素,再插入到优先队列中。