文章目录
前言
本人是一个刚刚上路的IT新兵,菜鸟!分享一点自己的见解,如果有错误的地方欢迎各位大佬莅临指导,如果这篇文章可以帮助到你,劳请大家点赞转发支持一下!:
优先级队列,可以帮助我们快速得到优先级最高的数据,十分便捷。话不多说,一起来畅游知识的海洋吧!
一、优先级队列
1.概念
先前已经介绍过队列
这一种数据结构了,但有些情况,操作的数据可能带有优先级,需要优先出队优先级高的数据。
在生活中,我们有时需要处理的事情很多,而人们往往就会处理优先级较高的事,就比如,有人敲门,和吃饭。人们往往都会选择先去开门再吃饭。
所以我们需要一种新的数据结构提供返回最高优先级数据以及添加新数据这两个方法
2.优先级队列的模拟实现
JDK1.8中的PriorityQueue底层使用了堆这种数据结构。
1.堆的概念
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
大小根堆的逻辑结构与存储结构如下
2.堆的存储方式
从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储。
完全二叉树可以高效的利用空间
对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
将元素存储到数组中后,可以根据二叉树章节的性质5对树进行还原。假设i为节点在数组中的下标,则有:
- 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
- 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
- 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
3.堆的创建
堆的创建介绍两种方法,一个是向下调整,一个是向上调整
//创建大根堆的方法
public void createHeap() {
//从第一个叶子结点的根开始遍历
for(int parent = ( usedSize - 2) / 2;parent >= 0;parent--) {
//使每个根都是该树上的最大值
shiftDown(parent,usedSize);
}
}
//向下调整
private void shiftDown(int parent, int len) {
int cur = parent*2 + 1;
//使cur为该树的叶子节点
while (cur < len) {
//使cur为两个子结点中最大或最小值(创建大根堆为最大值,小根堆为最小值),
// <号创建大根堆,>号创建小根堆
if(cur + 1 < len && elem[cur] < elem[cur + 1]) {
cur++;
}
//<号创建大根堆,>号创建小根堆
if(elem[parent] < elem[cur]) {
int tmp = elem[parent];
elem[parent] = elem[cur];
elem[cur] = tmp;
//交换之后可能会影响子树的根情况
//因为使parent为交换的根的下标,重新去遍历子树
parent = cur;
cur = parent*2+1;
}else {
break;
}
}
}
如要创建小根堆,将上述代码中有注释标注的<改为>即可
建堆的时间复杂度为O(N)。
最坏情况为:从根一路比较到叶子结点,比较的次数为完全二叉树的高度,即时间复杂度为O(log2N)。
4.堆的插入
数据的插入分为两步
- 先将元素放入到底层空间中(注意:空间不够时需要扩容)
- 将最后新插入的节点向上调整,直到满足堆的性质
//插入数据方法
public void offer(int val) {
if(isFull()) {
elem = Arrays.copyOf(elem,usedSize*2);
}
elem[usedSize++] = val;
shiftUp(usedSize - 1);
}
//从child位置向上调整
public void shiftUp(int child) {
int parent = (child - 1)/2;
while(parent > 0) {
//>号是大根堆的插入,
// <号是小根堆的插入
if(elem[child] > elem[parent]) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[child] = tmp;
child = parent;
//去继续调整父亲结点所在的树
parent = (child - 1)/2;
} else {
//如果连自己这棵树都没有调整,就不会影响到父亲结点所在的树
break;
}
}
}
//判断堆是否满了
public boolean isFull() {
return usedSize == elem.length;
}
5.堆的删除
- 堆的删除一定删除的是堆顶元素
删除数据分为以下三个步骤
- 将堆顶元素对堆中最后一个元素交换
- 将堆中有效数据个数减少一个
- 对堆顶元素进行向下调整
public void poll() {
if(isEmpty() == true) {
return;
}
int tmp = elem[--usedSize];
elem[usedSize] = elem[0];
elem[0] = tmp;
shiftDown(0,usedSize);
}
//判断堆是否为空
public boolean isEmpty() {
return usedSize == 0;
}
全部代码
public class MyHeap {
public int[] elem;
public int usedSize;
public MyHeap() {
this.elem = new int[10];
}
public void initElem(int[] array) {
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
usedSize++;
}
}
//创建大小根堆的方法
public void createHeap() {
//从第一个叶子结点的根开始遍历
for(int parent = ( usedSize - 2) / 2;parent >= 0;parent--) {
//使每个根都是该树上的最大值
shiftDown(parent,usedSize);
}
}
//向下调整
private void shiftDown(int parent, int len) {
int cur = parent*2 + 1;
//使cur为该树的叶子节点
while (cur < len) {
//使cur为两个子结点中最大或最小值(创建大根堆为最大值,小根堆为最小值),
// <号创建大根堆,>号创建小根堆
if(cur + 1 < len && elem[cur] < elem[cur + 1]) {
cur++;
}
//<号创建大根堆,>号创建小根堆
if(elem[parent] < elem[cur]) {
int tmp = elem[parent];
elem[parent] = elem[cur];
elem[cur] = tmp;
//交换之后可能会影响子树的根情况
//因为使parent为交换的根的下标,重新去遍历子树
parent = cur;
cur = parent*2+1;
}else {
break;
}
}
}
//插入数据方法
public void offer(int val) {
if(isFull()) {
elem = Arrays.copyOf(elem,usedSize*2);
}
elem[usedSize++] = val;
shiftUp(usedSize - 1);
}
//从child位置向上调整
public void shiftUp(int child) {
int parent = (child - 1)/2;
while(parent > 0) {
//>号是大根堆的插入,
// <号是小根堆的插入
if(elem[child] > elem[parent]) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[child] = tmp;
child = parent;
parent = (child - 1)/2;
} else {
break;
}
}
}
//判断堆是否满了
public boolean isFull() {
return usedSize == elem.length;
}
public void poll() {
if(isEmpty() == true) {
return;
}
int tmp = elem[--usedSize];
elem[usedSize] = elem[0];
elem[0] = tmp;
shiftDown(0,usedSize);
}
//判断堆是否为空
public boolean isEmpty() {
return usedSize == 0;
}
//查看堆顶元素
public int peek() {
return elem[0];
}
}
二、常用接口介绍
1. PriorityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。
- 关于PriorityQueue的使用要注意:
- 使用时必须导入PriorityQueue所在的包,即:import java.util.PriorityQueue;
- PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
- 不能插入null对象,否则会抛出NullPointerException
- 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
- 插入和删除元素的时间复杂度为O(log2N)
- PriorityQueue底层使用了堆数据结构
- PriorityQueue默认情况下是小堆—即每次获取到的元素都是最小的元素
这是PriorityQueue的源码,可以看到第109行,是有Comparator类型的comparator。
这是PriorityQueue的一个构造方法,如果你传入了一个比较器,按住ctrl点进去
可以看到下面的方法。
让PriorityQueue中的自带比较器 == 传入的比较器
下面咱们来看一看offer方法的源码。
直接去看siftUp(向上调整)的源码
- 比较器 != null
就会调用咱们传入的比较器中重写过的compare方法。
- 比较器 == null
就会让key == 你传入的数据类型的Comparable类型的对象,
然后调用其中的compareTo方法来进行比较。
这也是为什么存入PriorityQueue的数据一定要是可以比较的,
如果要存储你自定义类型的数据,必须在创建PriorityQueue时传入与自定义类型适配的比较器,才不会报错。
2.PriorityQueue的构造方法
- 优先级队列的构造
构造器 | 功能介绍 |
---|---|
PriorityQueue() | 创建一个空的优先级队列,默认容量是11 |
PriorityQueue(intinitialCapacity) | 创建一个初始容量为initialCapacity的优先级队列,注意:initialCapacity不能小于1,否则会抛IllegalArgumentException异常 |
PriorityQueue(Collection<?extends E> c) | 用一个集合来创建优先级队列 |
//创建一个空的优先级队列,底层默认容量是11
PriorityQueue<Integer> queue1 = new PriorityQueue<>();
//创建一个空的优先级队列,
//底层的容量为initialCapacity此处initialCapacity == 100;
PriorityQueue<Integer> queue2 = new PriorityQueue<>(100);
ArrayList<Integer> list = new ArrayList<>();
list.add(4);
list.add(3);
list.add(2);
list.add(1);
//用ArrayList对象来构造一个优先级队列的对象
//q3中已经包含了三个元素
//此处的集合必须继承了Collection接口
//且集合的类必须与PriorityQueue中存放的数据同类型或者是子类
PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
System.out.println(q3.size());
System.out.println(q3.peek());
默认情况下,PriorityQueue队列是小堆,如果要创建大堆,就需要用户提供比较器
class IntCmp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
//o1 - o2为小堆
//o2 - o1为大堆
return o2-o1;
}
}
public class TestPriorityQueue {
public static void main(String[] args) {
//创建一个大堆
PriorityQueue<Integer> p = new PriorityQueue<>(new IntCmp());
}
}
或者可以直接传一个匿名内部类
PriorityQueue<Integer> queue4 = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
3.PriorityQueue的常用方法
方法名 | 功能介绍 |
---|---|
booleanoffer(E e) | 插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度O(log2N) ,注意:空间不够时候会进行扩容 |
E peek() | 获取优先级最高的元素,如果优先级队列为空,返回null |
E poll() | 移除优先级最高的元素并返回,如果优先级队列为空,返回null |
int size() | 获取有效元素的个数 |
void clear() | 清空 |
booleanisEmpty() | 检测优先级队列是否为空,空返回true |
总结
以上就是今天要讲的内容,本文仅仅简单介绍了优先级队列,优先级队列可以解决TOP-K问题,如果对你有帮助就点个赞吧。🥰🥰🥰
路漫漫,不止修身也养性。