什么是优先级队列
概念
我们学习过了队列的知识点知道了队列是一种先进先出的数据结构那么也就是说普通队列是按照时间进行的优先级,越早进去的越早出来,那么有没有一种数据结构可以改变优先级呢?比如说普通队列是进入时间作为优先级,越早的进入优先级越高,那么如果我想让值越小的越早出来该怎么做到呢?因此就有了下面这个数据结构也就是堆。或者叫优先级队列
堆的底层数据结构是什么
堆的底层数据结构其实是二叉树和顺序表,那么有些同学可能会有疑问这个二叉树就是二叉树顺序表就是顺序表为什么是二叉树和顺序表呢?其实这是因为顺序表是我们用来存储元素的而这个元素的一个图解是一个完全二叉树,我这样说有些同学可能还是不理解这是什么意思我给大家解释一下。
请大家想一下二叉树有哪些属性呢?首先是值域其次就是左右节点那么我们可不可以用顺序表去表示一个二叉树呢比如说下图
如图如果我们将这个二叉树当作一个顺序表那么他的下标就是0 1 2 3 4 5 6 那么它的左右子树也就是2i+1和2i+2 那么因此我们就可以利用顺序表按照二叉树的结构去存储,由此我们的优先级队列的实现就有了一个基础和前提了。
如何实现优先级队列
要想实现优先级队列那么首先我们要搞懂我们要实现的是那种,就用最简单的为列我们要实现一个优先级队列要求每一次出队列的元素必须是这个队列里最小的元素(小根堆),或者最大的元素(大根堆)。那么由此我们可以得出,优先级队列的实现有两种,一种是小根堆,一种是大跟堆。
什么是小根堆
小根堆就是根部的元素比他的左右两个孩子都小
什么是大根堆
大根堆就是根部元素比它的两个孩子都大。
由此我们知道优先级队列实现的一个初步样貌,其实就是对顺序表进行特殊的操作。
那么我们接下来就可以动手实现了
模拟实现优先级队列
如何实现堆
那么想要实现堆我们肯定要对已知的数组进行操作,而操作建堆的方式有两种一种是向下建堆一种是向上建堆,那么我们来说一下向上建堆和向下建堆是怎么一回事吧。这里假设我们要实现的是大根堆
向上建堆
首先来说一下向上调整建堆,向上调整建堆其实就是,从最后一个节点开始依次与其父节点进行比较如果大于父节点就让其交换位置并且交换父节点的下标然后逐渐向上调整。
若目标结点的值比其父结点的值大,则交换目标结点与其父结点的位置,并将原目标结点的父结点当作新的目标结点继续进行向上调整。若目标结点的值比其父结点的值小,则停止向上调整,此时该树已经是小堆了。
时间复杂度
我们来计算一下向上建堆的时间复杂度这里我们每一个节点都需要从最下面向上调整因此我们的时间复杂度是O(n)。
向下建堆
向下建堆是什么呢?向下建堆其实就是以某一个节点与其孩子进行比较如果比他的孩子小就跟孩子节点交换位置并且让目标节点的孩子节点当成新的目标节点从而向下调整。
代码实现
首先就是初始化代码
public class heap{
private int[] val;
private int size=0;
public heap(){
val=new int[10];
}
public heap(int[] other){
val= Arrays.copyOf(other,other.length);
size=other.length;
for(int parent=(size-1-1)/2;parent>=0;parent--){
siftDown(parent,size);
}
}
}
初始化我们选择的是用一个数组进行初始化如果没有传入数组的话就使用默认初始化
向下建堆代码
在刚刚的那部分代码中我们使用的是向下建堆来进行创建的,为什么使用向下建堆呢?因为大家可以想一下,我们的二叉树叶子节点也就是最底层的那部分节点是我们整个树中最多的节点并且他还是在最底层那么大家来想一下,他需要向下移动吗?肯定不需要因为它已经在最底层了怎么可能还需要向下移动呢?因此我们的向下建堆可以从倒数第二层进行建堆,从而是我们可以避免去便利一半的节点(因为由等差数列可得最底层的节点是整个二叉树节点的一半)。因此他的时间更快效率更高。
那么代码如下
import java.lang.reflect.Array;
import java.util.Arrays;
public class heap {
private int[] val;
private int upSize=0;
public heap(int[] other){
val= Arrays.copyOf(other,other.length);
upSize=other.length;
for(int parent=(upSize-1-1)/2;parent>=0;parent--){
siftDown(parent);
}
}
public void siftUp(int child){
}
public void siftDown(int parent){
int child=parent*2+1;
while(child<upSize){
if(child+1<upSize&&child<upSize&&val[child]<val[child+1]){
child++;
}
if(val[child]>val[parent]){
int tmp=val[parent];
val[parent]=val[child];
val[child]=tmp;
parent=child;
child=2*child+1;
}
else if(val[child]<=val[parent]){
break;
}
}
}
public int pop(){
return val[0];
}
}
向上建堆代码
向上建堆根据上面的描述它的代码如下
public void siftUp(int child){
int parent=(child-1)/2;
while(parent>=0){
if(val[child]>val[parent]){
int tmp=val[child];
val[child]=val[parent];
val[parent]=tmp;
child=parent;
parent=(parent-1)/2;
}
else{
break;
}
}
}
堆的增删
我们知道堆的底层其实是一个顺序表,那么对于顺序表来说,最难的是在哪里增加删除呢?肯定是第一个位置最简单的就是在末尾增加或删除。因此为了增加和删除方便些,我们增加删除的方式如下
假如删除pos位置:那么就让pos位置的值等于末尾位置的值之后长度减减,然后再从pos位置向上建堆调整一下。
假如增加一个值:那么就在末尾增加一个值然后让长度加加再从末尾向上调整建堆。
由此我们知道其实最开始建堆的时候是向下建堆,而向上建堆这个方法主要是为了进行插入的时候调整堆结构的。