1.优先级队列
1.1概念
前面介绍过队列,
队列是一种先进先出
(FIFO)
的数据结构
,但有些情况下,
操作的数据可能带有优先级
,一般出队列时,可能需要优先级高的元素先出队列
,该中场景下,使用队列显然不合适。 在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加
新的对象
。这种数据结构就是
优先级队列
(Priority Queue)
。
1.3常用接口
1.2.1 PriorityQueue的特性
Java
集合框架中提供了
PriorityQueue
和
PriorityBlockingQueue
两种类型的优先级队列,
PriorityQueue
是线程不安全的,
PriorityBlockingQueue
是线程安全的
。
关于
PriorityQueue
的使用要注意:
1.
使用时必须导入
PriorityQueue
所在的包,即:
import java.util.PriorityQueue;
2. PriorityQueue
中放置的
元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出
ClassCastException
异常
3.
不能
插入
null
对象,否则会抛出
NullPointerException
4.
没有容量限制,可以插入任意多个元素,其内部可以自动扩容
5.
插入和删除元素的时间复杂度为
O(log_2N)
6.
PriorityQueue
底层使用了堆数据结构
1.2.2 PriorityQueue
常用接口介绍
1.
优先级队列的构造 (常见)。
import
java
.
util
.
PriorityQueue
;
2.插入/删除/获取优先级最高的元素
2.优先级队列的模拟实现
2.1堆的概念
如果有一个
关键码的集合
K = {k0
,
k1
,
k2
,
…
,
kn-1}
,把它的所有元素
按完全二叉树的顺序存储方式
存储
在一个一维数组中
,并满足:
Ki <= K2i+1
且
Ki<= K2i+2
(Ki >= K2i+1
且
Ki >= K2i+2) i = 0
,
1
,
2…
,则
称为
小堆
(
或大堆
)
。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
a. 堆中某个节点的值总是不大于或不小于其父节点的值;
b. 堆总是一棵完全二叉树。
2.2 堆的存储方式
从堆的概念可知,
堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储
,
注意:对于
非完全二叉树,则不适合使用顺序方式进行存储
,因为为了能够还原二叉树,
空间中必须要
存储空节点,就会导致空间利用率比较低
。
将元素存储到数组中后,可以根据二叉树章节的性质
5
对树进行还原。假设
i
为节点在数组中的下标,则有:
a. 如果
i
为
0
,则
i
表示的节点为根节点,否则
i
节点的双亲节点为
(i - 1)/2
b. 如果
2 * i + 1
小于节点个数,则节点
i
的左孩子下标为
2 * i + 1
,否则没有左孩子
c. 如果
2 * i + 2
小于节点个数,则节点
i
的右孩子下标为
2 * i + 2
,否则没有右孩子
2.3堆的创建
2.3.1堆向下调整
对于集合
{ 27,15,19,18,28,34,65,49,25,37 }
中的数据,如果将其创建成堆呢?
仔细观察上图后发现:
根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可
。
向下过程(以小堆为例):
1.
让
parent
标记需要调整的节点,
child
标记
parent
的左孩子
(
注意:
parent
如果有孩子一定先是有左孩子)
2.
如果
parent
的左孩子存在,即
:child < size
, 进行以下操作,直到
parent
的左孩子不存在
parent右孩子是否存在,存在找到左右孩子中最小的孩子,让
child
进行标记
将parent
与较小的孩子
child
比较,如果:parent小于较小的孩子
child
,调整结束。
否则:交换
parent
与较小的孩子
child
,交换完成之后,
parent
中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child
;
child =parent*2+1; 然后继续
2
。
public void shiftDown(int[] array, int parent) {
// child先标记parent的左孩子,因为parent可能右左没有右
int child = 2 * parent + 1;
int size = array.length;
while (child < size) {
// 如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记
if(child+1 < size && array[child+1] < array[child]){
child += 1;
}
//
if (array[parent] <= array[child]) {
break;
}
else{
// 将双亲与较小的孩子交换
int t = array[parent];
array[parent] = array[child];
array[child] = t;
// parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
parent = child;
child = parent * 2 + 1;
}
}
}
注意:在调整以parent为根的二叉树时,必须要满足parent的左子树和右子树已经是堆了才可以向下
调整。
堆向上调整(小堆为例)
public static void shiftUp(int[] arr, int size, int index) {
int child = index;
int parent = (child - 1) / 2;
while (child > 0) {
if (arr[parent] > arr[child]) {
// 交换两个元素
int tmp = arr[parent];
arr[parent] = arr[child];
arr[child] = tmp;
} else {
break;
}
child = parent;
parent = (child - 1) / 2;
}
}
2.3.2堆的创建
对于普通序列,根节点的左右子树不满足堆的特性:
public static void createHeap(int[] array) {
// 找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整
int root = ((array.length-2)>>1);
for (; root >= 0; root--) {
shiftDown(array, root);
}
}
2.4 堆的插入与删除
堆的插入:
1.
先将元素放入到底层空间中
(
注意:空间不够时需要扩容
)
2.
将最后新插入的节点向上调整,直到满足堆的性质
堆的删除:
1.
将堆顶元素对堆中最后一个元素交换
2.
将堆中有效数据个数减少一个
3.
对堆顶元素进行向下调整
public static void createHeap(int[] array) {
for (int i = (array.length - 1 - 1) / 2; i >= 0; i--) {
shiftDown(array, array.length, i);
}
}
// 这就表示当前存储堆的数组
private int[] arr = new int[100];
private int size = 0;
// 往堆中插入元素
public void offer(int val) {
if (size >= arr.length) {
// 插入失败, 已经满了.
// 也可以实现扩容逻辑.
return;
}
// 先是把这个元素给尾插到数组末尾
arr[size] = val;
size++;
// 把最后的这个元素进行向上调整
shiftUp(arr, size, size - 1);
}
// 类似的, 也可以使用向上调整(基于 offer) 的方式来建堆
public void createHeap2(int[] arr) {
// 循环遍历数组, 把元素通过 offer 方法插入即可
for (int x : arr) {
offer(x);
}
}
// 获取堆顶元素
public Integer peek() {
if (size == 0) {
return null;
}
return arr[0];
}
// 删除操作(一定是删除堆顶的元素)
public Integer poll() {
if (size == 0) {
return null;
}
int result = arr[0];
// 交换 0 号元素和 size - 1 号元素
int tmp = arr[0];
arr[0] = arr[size - 1];
arr[size - 1] = tmp;
// size--, 把最后的元素干掉
size--;
// 从 0 号元素开始, 往下进行向下调整
shiftDown(arr, size, 0);
return result;
}