一、优先级队列
1、优先级队列概念
优先级队列,听名字我们就知道他是一种队列,队列在前面我们已经学习过了,它是一种先进先出的数据结构,但是在特殊的情况下,我们我们队列中元素是带有一定优先级的,它需要比我们此时的队头元素,更先的出队列,或者更先的入队列
比如,当我们刚进入游戏的时候,突然有人来敲门,在这种情况下,我们是不是应该先去开门,虽然我们是先进的游戏,但是我们应该先去开门。
或者,当我们要乘坐飞机时,我们是经济舱的乘客,现在我们正在排队检票,下一个就是你,但是这时来了个头等舱的人,他肯定是要比我们先进的。
在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(PriorityQueue)。
2、堆的概念
我们在前面了解了完全二叉树的概念,每一层的节点都是从左往右的,依次排列,中间不能空着元素。这就是完全二叉树的概念,那么完全二叉树跟我们今天要讲的堆又有什么关系呢?
其实,堆是一个(特殊的)完全二叉树,每个父节点都不大于或者不小于自己的孩子节点,因此我们将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
层序遍历这个二叉树,顺序的放入一个数组中,像如果有个关键码的集合K={ k0,k1,k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki=K2i+1且Ki>=K2i+2)i=0, 1,2…,则称为小堆(或大堆)。
这就是堆的存储。因此从逻辑上来说,堆是一棵完全二叉树,从存储底层来说,堆底层是一个数组。
二、优先级队列的模拟实现
1、堆的存储方式
从上述堆的概念可知,堆是一棵完全二叉树,因此可以层序遍历的规则采用顺序的方式来高效存储
但是我们要注意,如果此时我们不是堆,而是一棵非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
2、堆的创建
根据上述的概念知道,堆有两种:大根堆和小根堆,但如果此时我们给出一个大小顺序不同的集合,我们该如何创建一个大根堆或小根堆呢?
这就要引出我们的一种调整方法:向下调整
(1)堆向下调整
我们以这个集合{ 27,15,19,18,28,34,65,49,25,37}为例,我们先利用层序遍历的方式画出他的二叉树图。
我们希望将这棵二叉树调整成一个大根堆,但根据上图我们发现,他的最后的根节点和左右两边,并不满足一个大根堆的形式,因此我们需要将他进行调整,我们将最后根节点左右两边最大的一个结点与他进行交换,这样我们的最后根节点和他的左右两边就形成了一个大根堆,而这样一个往下调整的过程我们就称它为向下调整,但是我们发现进行调整之后,我们有的之后的根结点和他的左右两边,他就不是一个大根堆了因此我们需对他进行调整,因此在这里我们就得出了我们向下调整的过程
1.让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)
2. 如果parent的左孩子存在,即:child< usesize 进行以下操作,直到parent的左孩子不存在如果左孩子存在判断parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标记,然后将parent与较小的孩子child比较,如果:
(1)parent大于较大的孩子child,调整结束
(2)否则交换parent与较大的孩子child,交换完成之后,parent中小的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent=child;child=parent*2+1; 然后继续步骤2。
java"> public void createHeap(){
//我们从下往上走 ,找倒数第⼀个⾮叶⼦节点,从该节点位置开始往前⼀直到根节点,遇到⼀个节点,应⽤向下调整for(int parent = (usesize-1-1)/2;parent >=0 ;parent--){sifDown(parent,usesize);}}public void sifDown(int parent,int usesize){// child先标记parent的左孩⼦,因为parent可能右左没有右int child = 2*parent + 1;while(child < usesize){// 如果右孩⼦存在,找到左右孩⼦中较⼩的孩⼦,⽤child进⾏标记if(child+1 < usesize && elem[child] < elem[child+1]){child++;}// 如果双亲⽐其最⼩的孩⼦还大,说明该结构已经不满⾜堆的特性了,将双亲与较⼩的孩⼦交换if(elem[child] > elem[parent]){swap(child,parent);// parent中⼤的元素往下移动,可能会造成⼦树不满⾜堆的性质,因此需要继续向下调整parent = child;child = 2*parent + 1;}else{break;}}}
(2)堆向上调整:堆的插入
堆的插入总共需要两个步骤:
1. 先将元素放到堆的最后(注意:空间不够时需要扩容)
2. 将最后新插入的节点向上调整,直到满足堆的性质
在这里我们引入了一个新的方法:向上调整,他其实跟我们的向下调整是一样的只是向下调整是传入父亲结点,再去求孩子结点进行判断,而向上调整是传入孩子结点,求父亲结点进行判断
java"> public void offer(int val){if(isFull()){elem = Arrays.copyOf(elem,2*elem.length);}elem[usesize] = val;sifUp(usesize);usesize++;}public void sifUp(int child){int parent = (child-1)/2;while(parent >=0){if(elem[child] > elem[parent]){swap(child,parent);child = parent;parent =(child-1)/2;}else{break;}}}
(3)堆的删除
堆的删除总共需要三个步骤:
2. 将堆中有效数据个数减少一个
3. 对堆顶元素进行向下调整
java"> public int poll(int val){if(empty()){return -1;}int oldVale = elem[0];swap(0,usesize-1);usesize--;sifDown(0,usesize);return oldVale;}
完整代码
java">public class TestHeap {public int[] elem;public int usesize;public TestHeap(){elem = new int[10];}public void len(int[] arr){for (int i = 0; i < arr.length; i++) {elem[i] = arr[i];usesize++;}}public void createHeap(){for(int parent = (usesize-1-1)/2;parent >=0 ;parent--){sifDown(parent,usesize);}}public void sifDown(int parent,int usesize){int child = 2*parent + 1;while(child < usesize){if(child+1 < usesize && elem[child] < elem[child+1]){child++;}if(elem[child] > elem[parent]){swap(child,parent);parent = child;child = 2*parent + 1;}else{break;}}}public void swap(int i,int j){int tmp = elem[i];elem[i] = elem[j];elem[j] = tmp;}public void offer(int val){if(isFull()){elem = Arrays.copyOf(elem,2*elem.length);}elem[usesize] = val;sifUp(usesize);usesize++;}public void sifUp(int child){int parent = (child-1)/2;while(parent >=0){if(elem[child] > elem[parent]){swap(child,parent);child = parent;parent =(child-1)/2;}else{break;}}}public int poll(int val){if(empty()){return -1;}int oldVale = elem[0];swap(0,usesize-1);usesize--;sifDown(0,usesize);return oldVale;}public boolean isFull(){return elem.length == usesize;}public boolean empty(){return usesize == 0;}public int peekHeap(){return elem[0];}}
好了今天的分享就到这里了,还请大家多多关注,我们下一篇见!