数据结构(Java)—— 优先级队列(堆)
1. 概念
优先级队列是一种抽象数据类型(ADT),它允许队列中维护的元素按优先级排序,优先级最高的元素会优先被处理。
2. 使用
2.1 优先级队列的构造
构造器
| 功能介绍 |
PriorityQueue()
| 创建一个空的优先级队列,默认容量是11 |
PriorityQueue(int
initialCapacity)
|
创建一个初始容量为
initialCapacity
的优先级队列,注意:
initialCapacity不能小于
1
,否则会抛
IllegalArgumentException
异常
|
PriorityQueue(Collection<?
extends E> c)
| 用一个集合来创建优先级队列 |
代码示例:
public static void main(String[] args) {
// 创建一个空的优先级队列,底层默认容量是11
PriorityQueue<Integer> q1 = new PriorityQueue<>();
// 创建一个空的优先级队列,底层的容量为initialCapacity
PriorityQueue<Integer> q2 = new PriorityQueue<>(100);
ArrayList<Integer> list = new ArrayList<>();
list.add(4);
list.add(3);
list.add(2);
list.add(1);
// 用ArrayList对象来构造一个优先级队列的对象
// q3中已经包含了三个元素
PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
System.out.println(q3.size());
System.out.println(q3.peek());
}
运行结果如下:
注意:默认情况下,
PriorityQueue
队列是小堆,如果需要大堆需要用户提供比较器
![](https://i-blog.csdnimg.cn/direct/5c1270fbe15844adb242535afb4e90e1.png)
2.2 方法
函数名
| 功能介绍 |
boolean offer(E e)
|
插入元素
e
,插入成功返回
true
,如果
e
对象为空,抛出
NullPointerException
异常,注意:空间不够时候会进行扩容
|
E peek()
| 获取优先级最高的元素,如果优先级队列为空,返回null |
E poll()
| 移除优先级最高的元素并返回,如果优先级队列为空,返回null |
int size()
| 获取有效元素的个数 |
void clear()
| 清空 |
boolean isEmpty()
| 检测优先级队列是否为空,空返回true |
代码示例:
public static void main(String[] args) {
int[] arr = {4,1,9,2,8,0,7,3,6,5};
// 一般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好
// 否则在插入时需要不多的扩容
// 扩容机制:开辟更大的空间,拷贝元素,这样效率会比较低
PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
for (int e: arr) {
q.offer(e);
}
System.out.println(q.size()); // 打印优先级队列中有效元素个数
System.out.println(q.peek()); // 获取优先级最高的元素
// 从优先级队列中删除两个元素之和,再次获取优先级最高的元素
q.poll();
q.poll();
System.out.println(q.size()); // 打印优先级队列中有效元素个数
System.out.println(q.peek()); // 获取优先级最高的元素
q.offer(0);
System.out.println(q.peek()); // 获取优先级最高的元素
// 将优先级队列中的有效元素删除掉,检测其是否为空
q.clear();
if(q.isEmpty()){
System.out.println("优先级队列已经为空!!!");
}
else{
System.out.println("优先级队列不为空");
}
}
运行结果如下:
3. 模拟实现
JDK1.8
中的
PriorityQueue
底层使用了堆这种数据结构
,而堆实际就是在完全二叉树的基础上进行了一些调整。
3.1 堆
1. 概念
如果有一个
关键码的集合
K = {k0
,
k1
,
k2
,
…
,
kn-1}
,把它的所有元素
按完全二叉树的顺序存储方式存储 在一
个一维数组中
,并满足:
Ki <= K2i+1
且
Ki<= K2i+2
(Ki >= K2i+1
且
Ki >=K2i+2) i = 0
,
1
,
2…
,则
称为小堆
(
或大堆)
。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
2. 性质
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
3. 存储方式
从堆的概念可知,
堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储。
![](https://i-blog.csdnimg.cn/direct/de54d5bc0300457a95b314ae503c1e31.png)
注意:
1. 对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
2. 将元素存储到数组中后,假设i为节点在数组中的下标,则有:
- 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
- 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
- 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
3.2 堆的创建(以小根堆为例)
数据以集合{ 27,15,19,18,28,34,65,49,25,37 }为例。
向下调整
:
1.
让
parent
标记需要调整的节点,
child
标记
parent
的左孩子
(
注意:
parent
如果有孩子一定先是有左孩子
)
2.
如果
parent
的左孩子存在,即
:child < size
, 进行以下操作,直到
parent
的左孩子不存在:
1. parent
右孩子是否存在,存在找到左右孩子中最小的孩子,让
child
进行标记
2. 将
parent
与较小的孩子
child
比较,如果parent小于较小的孩子
child
,调整结束;
否则:交换parent
与较小的孩子
child
,交换完成之后,
parent
中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child
;
child = parent*2+1;
然后继续
。
![](https://i-blog.csdnimg.cn/direct/3699f4d2ea77400eb8e3d44996b76e40.png)
代码示例:
public int[] elem;
public int usedSize;
public TestHeap() {
this.elem = new int[10];
}
public void init(int[] array) {
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
usedSize++;
}
}
//把elem数组当中的数据 调整为小根堆
public void createHeap() {
for (int parent = (usedSize-1-1)/2; parent >= 0 ; parent--) {
siftDown(parent,usedSize);
}
}
private void swap(int i,int j) {
int tmp = elem[i];
elem[i] = elem[j];
elem[j] = tmp;
}
public void siftDown(int parent,int end) {
int child = 2*parent+1;
while (child < end) {
if(child+1 < end && elem[child] > elem[child+1]) {
child++;
}
//child下标 就是 左右孩子的最小值
if(elem[child] < elem[parent]) {
swap(child,parent);
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
运行结果如下:
注意:
1. 在调整以
parent
为根的二叉树时,必须要满足
parent
的左子树和右子树已经是堆了才可以向下调整。
2. 创建小根堆与大根堆的区别在于向下调整(siftDown),变化树中元素的比较关系就可以转换大小根堆。
3.3 插入
堆的插入总共需要两个步骤:
1.
先将元素放入到底层空间中
(
注意:空间不够时需要扩容
)
2.
将最后新插入的节点向上调整,直到满足堆的性质
![](https://i-blog.csdnimg.cn/direct/68aa8daf54fb4d1abea69c2db0128f00.png)
代码示例:
public void offer(int val) {
if(isFull()) {
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize] = val;
usedSize++;//11
siftUp(usedSize-1);
}
public void siftUp(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 boolean isFull() {
return usedSize == elem.length;
}
运行结果如下:
3.4 删除
- 将堆顶元素对堆中最后一个元素交换
- 将堆中有效数据个数减少一个
- 对堆顶元素进行向下调整
代码示例:
public int poll() {
if(isEmpty()) {
return -1;
}
int old = elem[0];
swap(0,usedSize-1);
usedSize--;
siftDown(0,usedSize);
return old;
}
public boolean isEmpty() {
return usedSize == 0;
}
运行结果如下:
注意:
1. 堆的删除一定删除的是堆顶元素。
2. 对堆进行删除前要判断对是否为空。
3.5 其他方法
1. 获取队顶元素:
代码示例:
public int peek() {
return elem[0];
}
运行结果如下:
2. 获取队列容量:
代码示例:
public int size(){
return usedSize;
}
4. 注意事项
1.
使用时必须导入
PriorityQueue
所在的包,即:
import java . util . PriorityQueue ;
2. PriorityQueue
中放置的
元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出
ClassCastException
异常
3.
不能
插入
null
对象,否则会抛出
NullPointerException
4.
没有容量限制,可以插入任意多个元素,其内部可以自动扩容
5. PriorityQueue
底层使用了堆数据结构
6.PriorityQueue
默认情况下是小堆
---
即每次获取到的元素都是最小的元素
本文是作者学习后的总结,如果有什么不恰当的地方,欢迎大佬指正!!!