一、初识【堆】
1、什么是【优先级队列】?
前面的文章我们介绍过队列,队列是一种先进先出的数据结构,但是,在某些情况下,操作的数据可能需要有一个优先级来获取数据,例如优先获取队列中最大的元素,或者优先获取队列中最小的元素,单靠先进先出并无法实现这种功能,在该种情况下,使用队列明显就不合适了。
举例来说,当一个队列分别存储11,13,23,53时,如果我们要获取队列中的最大元素,那么根据队列的规则,只能先分别在队列中弹出11,13,23后才可以获取到53。
因此,我们由此需要一个优先级队列来实现这个功能!
2、堆的底层实现
优先级队列有个别称,也就是【堆】,堆是由一个类实现的:PriorityQueue类(这个类实现了Queue接口)
在学习堆之前,我们需要学习二叉树,因为它的底层就是一个二叉树,并且这棵二叉树是一颗完全二叉树!另外,这个二叉树又是使用数组模拟实现的!关于二叉树的学习我也有写过相关文章,如有兴趣可去了解!
3、堆的概念
堆分为【大根堆】和【小根堆】!
小根堆:
所谓的小根堆,其实就是根节点的大小 小于 孩子节点的大小
整棵树都是【小根堆】的前提是:每棵子树都是【小根堆】
大根堆:
所谓的大根堆,其实就是根节点的大小 大于 孩子节点的大小
整棵树都是【大根堆】的前提是:每棵子树都是【大根堆】
4、节点下标的规律
如图:每个节点上面的数字代表节点的下标
假设一个节点的下标为i
1、如果 i =0;则 i 表示的节点为根节点,如果该节点不是根节点,则节点 i 的父亲节点下标为(i-1)/2
2、如果2*i +1 小于 节点的个数,则节点 i 的左孩子下标为 2* i+1;否则没有左孩子
3、如果2*i +2 小于 节点的个数,则节点 i 的右孩子下标为2* i +2,否则没有右孩子
二、模拟实现【堆】
首先我们先创建两个类,TestHeap类(模拟实现堆这个类)和 Test类(测试类)
1、TestHeap类的一些【基础对象】和【方法】
首先,我们要实现的堆是通过数组实现的,因此这个TestHeap的类要【创建一个数组对象】,另外,我们还需要实现一下【构造方法】 和 【初始化数组的方法】!
2、实现大根堆方法
前面我们介绍过【大根堆】的性质,它需要满足两个条件:
1、根节点元素【大于】孩子节点元素,这种树称为【大根堆】
2、整棵树是大根堆的前提是:每一颗子树都是大根堆
【问题】
那么,如果给你一个乱序的数组,如何排列数组元素,使这个数组的【逻辑结构】是【二叉树的大根堆】呢?(如果不了解什么是逻辑结构,那么一定是你看漏了,它在这篇文章【堆的概念】图中)
其实不难,首先我们先找到这颗满二叉树的最后一颗子树的父亲节点下标parent(以下简称parent为P),在根据【节点下标规律】求出该父亲节点左右孩子的节点下标,找到孩子节点元素的最大值,同父亲节点元素比较大小,接着会出现以下两种情况:
1:如果父亲节点元素【大于或等于】孩子节点的最大值,那么不做任何操作
2:如果父亲节点元素【小于】孩子节点的最大值,那么【交换】父亲节点和该孩子节点的元素,接下来操作才是难点:
在交换完节点内容后,则有可能会出现,交换前【孩子节点树】本来已经调整为【大根堆】,交换后又不满足【大根堆的性质】,那么就要【继续调整】该孩子节点树,使其满足大根堆的性质!(看到这里如果不理解没有关系,后举一个例子,你也许会恍然大悟)
【例子】
如图,此时这个二叉树其实是一个数组的逻辑结构图!因为该数组尚未排序,因此其不满足大根堆的性质,圆圈里面的数代表【元素的值】,圆圈下面的数子代表【元素的下标】
1、 首先,我们之前在TestHeap类中定义了【usedSize】成员变量,用来记录数组存储的元素个数。关于如何调整该堆?先从最后一颗子树,【从右到左,从下到上】的轨迹依次调整每一颗子树,使其都成为【大根堆】。
所以,我们需要求出最后一颗子树P的节点下标,即元素为4,元素下标为3的元素! 它就是我们要调整的第一棵树!
那么该如果求得该元素下标的值?其实我们早就知道了:P=(最后一个元素的下标-1)/2,也就是P=[ (usedSIze-1) - 1 ] / 2;
求得该P节点的孩子节点最大值为9,因此交换元素
2、 第一棵树调整完成,开始调整第二棵树,即元素为3,下标为2的元素 ,这个时候使P=P-1即可;
求得该P节点的孩子节点的最大值为7,交换元素
3、第二棵树调整完成,开始调整第三棵树,即元素为2,下标为1的元素,P=P-1;
此时,发现P节点的孩子节点最大值为9,交换元素;交换完元素我们就会发现,第一棵树交换前是大根堆,交换后就不是大根堆了,因此,我们需要再次调整这棵树;
令P=该孩子节点的下标,重新调整!
后面的情况就不一一在推导了,相信大家也可以自己推导出来!上面的讲解只是未来让你能更好地结合讲解理解代码,大根堆的代码实现如下!
它分为三个方法:
第一个方法:createHeap方法利用while循环,依次调整每一颗子树(具体调整调用方法三),是实现数组调整的主体逻辑
第二个方法:swap方法即交换父亲节点和孩子节点元素
第三个方法:siftDown方法:则通过接收根节点的下标,调整以该节点为下标的整棵树,使其成为大根堆!creatHeap方法配合使用该方法,完成每一颗子树的调整,使整棵树成为大根堆!
//实现大根堆方法:
public void createHeap(){
int parent=(usedSize-1-1)/2;
for(int i=parent;i>=0;i--){
siftDown(i,usedSize);//调用向下调整方法
}
}
//交换数组元素方法:
private void swap(int i,int j){
int tmp=elem[i];
elem[i]=elem[j];
elem[j]=tmp;
}
//向下调整方法:
private void siftDown(int parent,int end){
//通过父亲节点的下标计算左孩子节点下标
int child=2*parent+1;
//当孩子节点的下标大于数组下标的最大值,跳出循环
while(child<end){
//确保child为最大孩子节点的下标
if(child+1<end&&elem[child]<elem[child+1]){
child++;
}
//调整为大根堆
if(elem[parent]<elem[child]){
swap(child,parent);//调用交换数组元素方法
parent=child;
child=2*parent+1;
}else{
break;
}
}
}
3、添加元素方法:
在给这个数组添加元素的时候,由于数组的【逻辑结构】要满足大根堆的性质,因此,在添加完元素过后仍然需要检查一下该数组的元素顺序。
如图为例:
以下是一个排序为大根堆的数组的【逻辑结构】(黄色图标元素80为我们要添加的元素),现在我们要给该数组末尾太添加一个元素80,添加80后,我们发现这棵二叉树不满足大根堆的结构了!因此,我们需要对此做出调整!
调整原理!
首先,我们调整的主体逻辑是【向上调整】,即从最下面的子树开始,依次向上调整。具体实现如下:
1、先创建一个siftUp(int child)方法,给该方法传入数组最后一个元素的下标(以下简称孩子节点下标为C),此时这个下标正指向二叉树的最后一棵树的孩子节点,接着计算出父亲节点的下标(以下简称父亲节点下标为P);
比较父亲节点和孩子节点元素的大小,发现孩子节点的元素【大于】父亲节点的元素,交换元素;
将P的值【赋值】给C,即C=P,使P指向父亲节点的父亲节点的下标,即P=(C-1) / 2;
2、比较父亲节点和孩子节点元素的大小,发现父亲节点的元素【小于】孩子节点的元素 ,交换元素;
接着C=P,P=(C-1) / 2;
3、比较父亲节点和孩子节点元素的大小,发现父亲节点的元素【小于】孩子节点的元素 ,交换元素;
接着C=P,P=(C-1) / 2;
发现此时P<0,因此调整结束!
代码实现:
//插入新数据方法:
public void offer(int val){
//1、如果数组满了,扩容
if(isFull()){//调用判断数组空间已满的方法
elem=Arrays.copyOf(elem,2*elem.length);
}
//2、添加元素
elem[usedSize]=val;
usedSize++;
//3、调整数组顺序使其插入新数据后仍然满足大根堆
siftUp(usedSize-1);
}
//判断数组空间是否已满的方法
private boolean isFull(){
return usedSize== elem.length;
}
//向上调整方法
private void siftUp(int child){
int parent=(child-1)/2;
while(parent>=0){
if(elem[child]>elem[parent]){
swap(child,parent);
//使child指向该孩子节点的父亲节点
child=parent;
//计算出该父亲节点的父亲节点下标
parent=(child-1)/2;
}else{
break;//注意注意注意!
}
}
}
在这里有一个点需要注意,那就是siftUp方法循环里面的else语句的作用,让我来举一个例子;
假设这里我们要添加的元素不是80,而是8;
此时if……else……语句走else语句,跳出while循环,因为此时这个二叉树添加8这个元素后仍然是大根堆,不需要调整!
4、删除元素方法:
【优先级队列】删除元素时,是删除二叉树的根节点元素,因为该元素是整个数组中的【最大值】或者【最小值】,那么该如何执行该操作?
只需要将【数组第一个元素】和【数组最后一个元素】交换,usedSize--,最后对【下标为0】的树进行一次向下调整即可!
1、
2、
代码实现:
//删除元素方法:
public int poll(){
//如果数组为空,返回-1
if(isEmpty()){
return -1;
}
//数组不为空,执行删除操作
int old=elem[0];
swap(0,usedSize-1);//交换数组第一个元素和最后一个元素的值
usedSize--;
siftDown(0,usedSize);//向下调整第一棵树
return old;
}
//判断数组是否为空方法
public boolean isEmpty(){
return usedSize==0;
}
}
三、PriorityQueue的常见接口介绍
1、优先级队列的构造
priorityQueue的构造方法有三种:
构造方法: 功能介绍:
PriorityQueue() 创建一个空的优先级队列,默认容量是11
PriorityQueue(int initialCapacity) 创建一个初始容量为initialCapacity的优先级队列
PriorityQueue(Collection<? extends E> c) 用一个集合来创建优先级队列
实例:
//创建一个空的优先级队列,底层默认容量为11
PriorityQueue<Integer> q1=new PriorityQueue<>();
//创建一个空的优先级队列,底层的容量为initialCapacity
PriorityQueue<Integer> q2=new PriorityQueue<>(100);
//以一个集合为参数
ArrayList<Integer> list=new ArrayList<>();
PriorityQueue<Integer> q3=new PriorityQueue<>(list);
2、比较器
其实,PriorityQueue默认情况下的优先级队列是【小根堆】, 让我们来看看!
首先,创建一个优先级队列q1,给该队列添加两个元素,分别是:2 、 3
如果该队列是小根堆,那么会打印出来2;反之,如果该队列是大根堆,那么会打印出来3;
运行代码,发现打印出来的是2,说明该队列默认是小根堆!
那么,就有一个问题,如果我们要使该队列是一个大根堆,该怎么做呢?
答:在构造方法中传入一个【比较器】!
//自定义实现一个【比较器】
class IntCmp implements Comparator<Integer>{
public int compare(Integer o1,Integer o2){
return o2.compareTo(o1);
}
}
public class Test {
public static void main(String[] args) {
//给构造方法中传入一个【比较器】
PriorityQueue<Integer> q1=new PriorityQueue<>(new IntCmp());
q1.offer(2);
q1.offer(3);
System.out.println(q1.poll());
}
}