在上文中(对堆、堆排序的认识) 我们对堆有了基本的了解,有了堆的知识我们就可以了解另一个很重要的数据结构了,那就是优先级队列。
优先级队列
优先级队列(Priority Queue)是一种特殊的队列,其中每个元素都有一个关联的优先级。在优先级队列中,高优先级的元素会比低优先级的元素先被处理。这种数据结构在许多应用中都非常有用。
优先级队列的实现
我们知道队列是一种先进先出的数据结构,而现在优先级队列的前提增加了优先级这个概念,所以我们要先保证其优先级的情况下再考虑先进先出,因此优先级队列通常使用堆来实现,因为堆能够高效地支持插入和删除操作,同时保持元素的有序性。最大堆可以用来实现最高优先级先出的优先级队列,而最小堆则用于最低优先级先出的优先级队列。
接下来了解一下优先级队列都有哪些基本操作和如何实现吧。
插入(push)
在上文中我们提到了heapify这个堆调整的函数,但是不知道发现没有这个是只能逐个向下调整的,就是往树的叶子节点进行调整。
function heapify(arr,n,i){
let cur = i; // 节点游标
let left = i * 2 + 1; // 左孩子位置
let right = i * 2 + 2; // 右孩子位置
// 如果存在左孩子并且左孩子的值要小于当前节点
if(left < n && arr[left] < arr[cur]){
cur = left; // 游标指向左孩子位置
}
// 右孩子同理
if(right < n && arr[right] < arr[cur]){
cur = right
}
// 如果cur不是指向原来的位置,则代表需要进行调整
if(cur !== i){
[arr[i],arr[cur]] = [arr[cur],arr[i]]; // 将孩子节点中的最小节点与当前节点交换
heapify(arr,n,cur); // 递归进行,直到调整到正确位置
}
}
向下调整可以保证原本在上面的元素调整到下方的合适位置,但却无法让下方的元素调整至上方合适位置,所以我们还需要一个函数来将下方的元素向上调整。
function siftUp(arr,n,i){
let cur = i;
let left = i * 2 + 1;
let right = i * 2 + 2;
if(left < n && arr[left] < arr[cur]){
cur = left;
}
if(right < n && arr[right] < arr[cur]){
cur = right
}
if(cur !== i){
[arr[i],arr[cur]] = [arr[cur],arr[i]];
siftUp(arr,n,Math.floor((cur-1)/2));
}
}
相较于向下调整,我们可以发现只是将下一个要调整的元素换成了它的父元素而已,为了更好理解我们再将向下调整的函数名换为siftDown,
function siftDown(arr,n,i){
let cur = i;
let left = i * 2 + 1;
let right = i * 2 + 2;
if(left < n && arr[left] < arr[cur]){
cur = left;
}
if(right < n && arr[right] < arr[cur]){
cur = right
}
if(cur !== i){
[arr[i],arr[cur]] = [arr[cur],arr[i]];
siftDown(arr,n,cur);
}
}
接下来我们就可以完成我们对优先级队列的插入操作了,因为我们已知了优先级队列是一个堆结构,所以我们要将一个元素插入到队列中时还需要进行堆的调整以维护堆的性质,而这我们只需要将新插入的元素进行一个向上调整即可。
function push(value){
// 假设heap就是我们的一个优先级队列,它是一个最小堆
heap.push(value);
const len = heap.length
shiftUp(heap,len,Math.floor((len-1)/2));
}
删除(pop)
在优先级队列中,从队列中删除元素实际上是取出堆顶的元素,同时再维护堆的性质的一个过程。在上文中,我们已经提到了堆排序了,其实从优先级队列中取元素的过程就是只进行了一步的堆排序而已。
function pop(){
const len = heap.length;
// 交换堆顶和最后一个元素
[heap[0],heap[len-1]] = [heap[len-1],heap[0]];
// 进行一次堆向下调整,同时调整的区间减去一
shiftDown(heap,len-1,0);
// 将最后一个元素(原来堆顶的元素)弹出并返回
return heap.pop();
}
实现基本的优先级队列类
结合上述内容我们就可以实现一个简单的优先级队列类了。
class PriorityQueue {
// 优先级队列中维护的堆
#heap = []
// 通过compare来决定大顶堆和小顶堆
#compare
constructor(cb){
this.#compare = cb;
}
// 初始化,将一个普通数组转化为堆
init(arr){
for(let i = Math.floor((arr.length-1)/2);i>=0;i--){
this.siftDown(arr,arr.length,i);
}
this.#heap = arr;
}
// 得到整个队列结构
get(){
return this.#heap;
}
// 判断是否为空
isEmpty(){
return this.#heap.length === 0;
}
// 队列的大小
size(){
return this.#heap.length;
}
// 查看队头元素
peek(){
return this.#heap[0] ? this.#heap[0] : null;
}
// 向队列中插入一个元素
push(value){
this.#heap.push(value);
let cur = Math.floor((this.#heap.length-1)/2);
this.#siftUp(this.#heap,this.#heap.length,cur);
}
// 取出堆顶元素
pop(){
const len = this.#heap.length
if(len<1){
return null;
}
[this.#heap[0],this.#heap[len-1]] = [this.#heap[len-1],this.#heap[0]];
this.#siftDown(this.#heap,len-1,0);
return this.#heap.pop();
}
// 清空整个队列
clear(){
this.#heap = [];
}
// 堆的向上调整
#siftUp(arr,n,i){
let cur = i;
let left = i * 2 + 1;
let right = i * 2 + 2;
if(left < n && this.#compare(arr[left],arr[cur])){
cur = left;
}
if(right < n && this.#compare(arr[right],arr[cur])){
cur = right
}
if(cur !== i){
[arr[i],arr[cur]] = [arr[cur],arr[i]];
this.#siftUp(arr,n,Math.floor((cur-1)/2));
}
}
// 堆的向下调整
#siftDown(arr,n,i){
let cur = i;
let left = i * 2 + 1;
let right = i * 2 + 2;
if(left < n && this.#compare(arr[left],arr[cur])){
cur = left;
}
if(right < n && this.#compare(arr[right],arr[cur])){
cur = right;
}
if(cur !== i){
[arr[i],arr[cur]] = [arr[cur],arr[i]];
this.#siftDown(arr,n,cur);
}
}
}
优先级队列的应用
-
操作系统中的进程调度:操作系统可以使用优先级队列来管理进程,确保高优先级的任务能够更快地得到执行。
-
网络通信:网络设备可以使用优先级队列来决定哪些数据包应该首先被发送,以保证关键服务的质量。 或在网络拥塞的情况下,优先级队列可以用来优先传输重要的流量,如语音或视频流。
-
资源管理:在某些内存管理系统中,优先级队列可以用来决定哪些页面应该被置换出内存。
-
任务队列:例如,在Web服务器中,后台任务(如图像处理、邮件发送等)可以根据其重要性和资源需求来排序和处理。
当然优先级队列的应用肯定不止如此,更多的用途还是要我们自己去发现的。