🐵本篇文章将对优先级队列(堆)的相关知识进行讲解
一、优先级队列
队列是一种“先入先出”的数据结构,但有时操作的数据带有优先级,需要优先处理,这时普通的队列就不能满足需求。比如:在排队取票时,往往会有老人优先、退伍军人优先等。这就可以理解为优先级队列的一种体现
优先级队列的底层是由一种叫堆的数据结构实现的
二、堆
2.1 什么是堆
将元素按照完全二叉树的顺序存储方式存储到一维数组中,堆分为大根堆和小根堆
如上图,如果满足每一个父亲节点的值都大于其左右孩子结点的值,则称为大根堆
如上图,如果满足每一个父亲节点的值都小于其左右孩子结点的值,则称为小根堆
2.2 堆的创建
堆的向下调整
public class Heap {
public int[] elem;
public int usedSize; //记录数组中元素的个数
public Heap() {
this.elem = new int[10]; //构造方法:数组的初始容量为10
}
public void initElem(int[] array) { //初始化elem数组
for (int i = 0; i < elem.length; i++) {
elem[i] = array[i];
usedSize++;
}
}
}
以大根堆为例,如果一棵完全二叉树是大根堆,则其每一课子树也都是大根堆,定义一个parent表示最后一棵子树的根结点的下标,再定义一个child表示parent结点的左孩子的下标
每调整完一个子树后就让parent--,去调整下一棵子树
那么该如何用代码表示parent和child的下标:首先是parent,usedSize表示数组中元素的个数,usedSize - 1就是最后一个结点的下标,根据二叉树的性质,让其减1再除2就是要得到的最后一个子树的根结点下标,表示为(usedSize -1 - 1)/ 2,child同样也是根据二叉树的性质,child = 2 * parent + 1
得到parent和child后,开始对第一个子树进行调整:首先要让child表示为左右孩子较大值结点的下标
public void creatBigHeap() { //创建大根堆
for (int parent = (usedSize - 1 - 1) / 2; parent >= 0 ; parent--) {
siftDown(parent, usedSize);
}
}
public void siftDown(int parent, int end) { //向下调整
int child = 2 * parent + 1;
if (child + 1 < usedSize && elem[child] < elem[child + 1]) {
child++;
}
...
}
接下来就是比较elem[parent]和elem[child],如果孩子结点大于其父亲结点则进行交换,交换完后,让parent = child,child = 2 * parent + 1,继续向下调整
如上图,此时说明这个子树已经调整完毕,当在调整{19, 34}这个树时当parent = 5、child = 11时,这个树就调整完了,所以可以用代码写一个循环,循环条件为child < usedSize
public 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;
}
}
}
private void swap(int child, int parent) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
}
那么整个大根堆就创建完了,完整代码如下:
public class Heap {
public int[] elem;
public int usedSize; //记录数组中元素的个数
public Heap() {
this.elem = new int[10]; //构造方法:数组的初始容量为10
}
public void initElem(int[] array) { //初始化elem数组
for (int i = 0; i < elem.length; i++) {
elem[i] = array[i];
usedSize++;
}
}
public void creatBigHeap() { //时间复杂度为O(n)
for (int parent = (usedSize - 1 - 1) / 2; parent >= 0 ; parent--) {
siftDown(parent, usedSize);
}
}
public 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;
}
}
}
private void swap(int child, int parent) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
}
}
堆的向上调整和向下调整类似
public void siftUp(int child) {
int parent = (child - 1) / 2;
while(parent > 0) {
if (child + 1 < usedSize && elem[child] < elem[child + 1]) {
child++;
}
if (elem[child] > elem[parent]) {
swap(child, parent);
child = parent;
parent = (child - 1) / 2;
} else {
break;
}
}
}
2.3 堆的插入与删除
堆的插入可以将元素放到数组的末尾,然后可以通过向上调整将其调整为大根堆或小根堆
public void offer(int key) {
if (isFull()) { //判断是否需要扩容
elem = Arrays.copyOf(elem, 2 * usedSize);
}
elem[usedSize] = key;
usedSize++;
siftUp(usedSize - 1);
}
堆的删除,由于堆顶的元素,所以可以将堆顶的元素和最后一个元素交换,再让usedSize--后将其向下调整为大根堆或小根堆即可
public int poll() {
int tmp = elem[0];
swap(0, usedSize - 1);
usedSize--;
siftDown(0, usedSize);
return tmp;
}
2.4 堆排序
堆排序就是按照堆的思想排序,假如要将一组数据按照从小到大排序,则先将这组数据创建为大根堆,不建小根堆是因为其不能保证左右孩子也是有序的。以下图为例:
由于是大根堆,所以堆顶元素是最大的,将堆顶元素和最后一个元素交换,交换后再调整为大根堆,此时再将堆顶元素与倒数第二个元素交换,交换后再调整为大根堆,如此循环
public void heapSort() {
int end = usedSize - 1; //此时为最后一个元素的下标
while(end > 0) {
swap(0, end);
siftDown(0, end - 1);//由于交换了0下标,所以只需要从0下标开始向下调整
//交换完后end位置不再属于大根堆,所以在end -1处结束调整
end--;
}
}
三、PriorityQueue接口介绍
该接口的底层是一个小根堆,先来介绍在该接口中的几个字段
3.1 构造方法
首先是上述三个基础的构造方法,可以观察到这三个构造方法都调用了另外一个构造方法:
调用不带参数的构造方法,此时数组的大小为初始容量:11
调用第一个带参数的构造方法,此时数组的大小为initalCapacity
第三个构造方法,其参数是一个比较器,接下来简单复习一下比较器(Comparator接口)
class Person {
public int age;
public String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
}
class Imp implements Comparator<Person> {
public int compare(Person p1, Person p2) {
return p1.name.compareTo(p2.name); //调用了String类的compareTo方法
//前者>后者,返回一个大于0的数
//前者<后者,返回一个小于0的数
//前者=后者,返回0
}
}
class Main {
public static void main(String[] args) {
Person p1 = new Person("Sans", 20);
Person p2 = new Person("Frisk", 8);
Imp imp = new Imp();
System.out.println(imp.compare(p1, p2));
}
}
调用第二个带参数的构造方法
那么传比较器作为参数究竟有什么用,接下来通过分析offer方法的源码进行讲解
3.2 接口中的常用方法
boolean offer(E e);
插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度为O(log₂N),如果空间不够就会进行扩容
E peek();
获取优先级最高的元素,即堆顶的元素,如果优先级队列为空,则返回null
E poll();
删除优先级最高的元素并返回,如果优先级队列为空,则返回null,时间复杂度为O(log₂N)
int size();
获取优先级队列中有效元素的个数
void clear();
清空优先级队列
boolean isEmpty();
判断优先级队列是否为空,如果为空,则返回true
🙉以上就是本文关于优先级队列的全部内容,接下来会对排序的相关知识进行讲解