概念:
在操作数据的时候,操作的数据具有优先级,需要返回*别的优先级数据或者添加新对象时就需要用到优先级队列。
jdk1.8中的PrioriytQueue底层实现了堆这种数据结构实际上,堆其实就是在完全二叉树进行调整而来。
堆:
对于一个关键码的集合K = { K1,K2,K3……Ki},把所有的元素按完全二叉树的顺序储存方式储存在一个一维数组中
堆的性质:
①孩子结点恒大于父结点(小根堆模式) ②堆总是一棵完全二叉树
注:小根堆模式只要满足孩子结点大于父结点,至于孩子结点之间谁大谁小都无所谓
结构:(以小根堆为例)堆的存储结构,就是按照二叉树的层序遍历去存放
为什么只堆只能是完全二叉树?非完全二叉树不行吗?
这是因为顺序存储的情况下,由于数组里要存放结点,非完全二叉树会浪费存储空间,导致空间利用率不高
如何判断有无子树?
对于结点的顺序存储进行编号,如果i=0,那么i对应的结点就是树的根结点;
如果2*i + 1小于结点数,那么结点i的左孩子是2*1+1,否则没有左孩子;
如果2*i + 2小于结点数, 那么结点i的右孩子是2*i+2, 否则没有右孩子。
堆的创建:
向下调整:
思路:我们已知如何判断一个节点如何判断有无孩子结点,那么我们只需要遍历数组的时候将对应孩子结点的较大者和双亲结点进行比较大小即可。在每次比较之后继续往下遍历孩子结点,直到整棵树都是小根堆(或者大根堆)模式。
【注】:在左右孩子结点进行比较之前得先判断是否有右孩子结点,避免出现下标越界的错误!
以此树为例
大根堆模式下这棵树应该长这样子
public class Heap {
public int[] elem;
public int usedsize;
public Heap(int n)
{
elem = new int[n];
}
//插入元素
public void initElem(int[] array)
{
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
usedsize++;
}
}
//创建大根堆
public void createBigHeap()
{
for (int parent = (usedsize-1-1)/2; parent >=0 ; parent--) {
sifedown(parent, usedsize);
}
}
//向下调整
private void sifedown(int parent, int end)
{
int child = 2*parent+1;
while(child<end)
{
if(elem[child]< elem[child+1] && child+1 < usedsize) //确保右子树一定存在再进行比较,child一定是左右孩子结点的较大者
{
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;
tmp = elem[i];
elem[i] = elem[j];
elem[j] = tmp;
}
public static void main(String[] args) {
int[] array = {77,13,46,20,28,37,55,19,9}; //给定一个数组
Heap heap = new Heap(9);
heap.initElem(array);
heap.createBigHeap(); //创建大根堆模式
System.out.println("=============================");
}
}
通过调试来检验是否成功向下调整
再以小根堆模式为例,来深刻体会向下调整的整个过程~
我们的整体思路是:先调整一棵子树,保证子树一定是小根堆,然后再从子树的父节点向下做调整。
private void Siftdown(int parent, int end)
{
int child = 2*parent+1;
while(child<end)
{
if(child+1 < usedsize && elem[child]>elem[child+1] ) //确保右子树一定存在再进行比较
{
child++;
}
if(elem[child] < elem[parent])
{
Swap(child,parent);
parent = child; //交换完之后再向下调整
child = 2*parent+1;
}
else {
break;
}
}
}
堆的插入和删除:
插入:
先判断是否需要扩容,然后再直接将元素插入到usedsize的位置,每次插入一个元素都需要做一次向上调整。(扩容与顺序表扩容的步骤相同)
//插入
public void offer(int val)
{
//判断是否需要扩容
if(isfull())
{
this.elem = Arrays.copyOf(elem,2*elem.length);
}
//插入元素
elem[usedsize] = val;
usedsize++;
siftup(usedsize-1);
}
删除:
将堆的根节点和最底层最末尾位置的结点交换,将usedsize减一再做一次向下调整
//删除
public int poll()
{
int tmp = elem[0];
Swap(0,usedsize-1);
usedsize--;
siftdown(0,usedsize);
return tmp;
}
在了解了堆的特性以及底层结构是如何实现的之后,接着来了解底层接口PriorityQueue
常用接口PriorityQueue
在java的集合框架里,提供的优先级队列有两种:PriorityqQueue和PriorityBlockingQueue,前者是线程不安全的,后者是线程安全的
注意:
1、用PriorityQueue接口的时候一定要先导包
import java.util.PriorityQueue;
2、PriorityQueue中放置的元素必须能够比较大小,不能插入无法比较大小的对象,不然就会抛出异常ClassCastException
3、插入的对象不能是null,不然会抛出空指针异常
4、没有容量限制,可以随意插入多个元素,内部可以自动扩容
5、插入和删除元素的时间复杂度是 log2 n (以2为底的对数)
6、PriorityQueue底层用了堆数据结构
7、PriorityQueue默认是小堆模式——即每次获取到的元素都是最小的元素
常用接口介绍:
1.优先队列的构造
在PriorityQueue接口中就能观察到实例化一个优先级队列最常用到的三个构造方法
先看看接口中都有哪些变量
然后就是相关的构造方法
关于比较器,我们可以回顾一下java中对象的三种比较方式:
①覆写基类的equals。因为用户实现自定义类型都继承自Object类,而Object类中提供了equals方法,我们想要按照对象中的内容来调整,那就得重写基类的equals方法。基本类型的比较大小可以直接用 == ,>, <进行比较,引用类型最好还是调用equals。
public class people
{
public static void main(String[] args)
{
public int age;
public String name;
public people(int age, String name)
{
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o)
{
//和自己比较
if(this == o)
{
return true;
}
//如果o不是people的子类或者O是null对象
if(o == null ||!(o instanceof people))
{
return false;
}
//基本类型可以直接比较,但是引用类型最好还是调用其equals方法
people p1 = (people)o;
return age == p1.age && name.equals(p1.name);
}
}
}
② 基于Comparable接口类的比较
Comparable是JDK提供的泛类比较接口类,源码如下(绿色的是注释,可以不理):
对用户自定义类型,想要按照大小与方式进行比较时,在自定义类的时候实现Comparable接口即可,然后重写Comparable 中的方法。
例如:
我们自定义一个类people,按照年龄age大小进行比较,那么就是重写Comparable中的方法compareTo
class people implements Comparable<people> {
public int age;
public String name;
public people(int age ,String name) {
this.age = age;
this.name = name;
}
@Override
public int compareTo(people o) {
return this.age-o.age;
}
}
实例化两个对象,调用compareTo进行比较,观察返回的结果
我们可以观察到返回结果是一个大于0的数字,这就证明按照年龄大小的比较方式,p1>p2
那么,现在又有一个问题:如果我想要按照名字来进行比较,这要怎么修改?
这就要用到第三种比较方式,比较器了!
③基于比较器比较
用户自定义比较类,实现Comparator接口 (绿色注释可不理)
返回值>0表示o1指向的对象大于o2指向的对象,==0 就是相当,<0就是o1指向的对象小于o2指向的对象。
此时我们就可以解决刚刚的问题了,自定义一个类作为我们想要的比较器,在类中覆写Comparator中的compare方法
class NameComparator implements Comparator<people>{
@Override
public int compare(people o1, people o2) {
return o1.name.compareTo(o2.name);
}
}
在了解了比较器之后,我们来进一步了解为什么PriorityQueue是默认建的小根堆
public static void main(String[] args) {
people p1 = new people(10,"zhangsan");
people p2 = new people (8,"lisi");
PriorityQueue<people> priorityQueue = new PriorityQueue<>();
priorityQueue.offer(p1);
priorityQueue.offer(p2);
}
我们看一下PriorityQueue接口中,offer的原码
再看siftup我们可以观察到:如果你在实例化PriorityQueue的时候没有传一个比较器作为参数,那么就默认是用接口中的比较器,调用接口中的则默认为小根堆模式