一、PriorityQueue 的使用
- 概念
队列是一种先进先出(FIFO)的数据结构,但有时候,数据可能带有优先级,出队列时,可能需要优先级高的元素先出队列,在这种情况下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话。
- 常用接口介绍
1、JDK:
提供了两种类型的优先级队列:①PriorityQueue是线程不安全的,②PriorityBlockingQueue是线程安全的。
2、PriorityQueue使用前,应先导入包
import java.util.PriorityQueue;
【注意】
- 插入的元素不能为null,且元素之间必须要能够进行比较;
- 插入、删除------在插入和删除元素期间优先级队列中的元素会自定进行调整(不论怎么调整,0号位置的元素始终是最小的);插入和删除的时间复杂度: O(logN)。
- 底层结构—堆(暂时不用管什么是堆) 也可以通过某种方式使首元素始终最大
3、PriorityQueue包含的接口:
-
构造方式:
-
PriorityQueue()空的优先级队列:默认的容量为11 ;
PriorityQueue q1 = new PriorityQueue<>();
- PriorityQueue(capacity):指定初始化容量大小 (capacity不能小于1,如果小于1会抛异常 )
PriorityQueue p2 = new PriorityQueue<>(20);
- PriorityQueue(集合对象):用一个集合中的元素来构造优先级队列
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);
- 常用接口:
- offer(x) :插入元素---->O(logN)
- poll():删除元素
- size():获取有效元素个数
- isEmpty(): 检测是否为空
- clear():将优先级队列中的元素清空
public static void TestPriorityQueue() {
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("优先级队列不为空");
}
}
二、堆的概念及实现
-
概念
有一个集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
如图所示为小堆,根节点的值小于左右子树的值,反之则为大堆。 -
存储方式
堆是一棵完全二叉树,因此可以按照层序的规则采用顺序的方式存储。对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
对于具有n个结点的完全二叉树,按照从上到下、从左到右的顺序对节点从0开始编号,假设i为节点在数组中的下标,则有:
- 若i>0,双亲序号: (i-1)/2; i=0, i为根节点编号,无双亲节点.
- 若2i+1<n, 左孩子序号: 2i+1,否则无左孩子
- 若2i+2<n, 右孩子序号: 2i+2,否则无右孩子
【堆的创建】
- 向下调整
如图, 对于集合{ 27,15,19,18,28,34,65,49,25,37 },根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。
过程(以小堆为例):
1、parent为本次调整节点的下标,调整以parent为根的二叉树;
(调整之前,要保证parent的左右子堆已经满足小堆的性质)
2、检测parent是否满足小堆性质:将parent与其孩子的比较
- 满足:以parent为根节点的二叉树已经是小堆
- 不满足:说明parent比其孩子大,将parent与其较小的孩子进行交换,交换后,parent的向下移动,
可能导致其子树不满足小堆性质,继续调整;
private void shiftDown(int parent){
//使用child标记parent较小的孩子
//默认情况下,先标记左孩子(完全二叉树:可能有左孩子没有右孩子)
int child = parent * 2 + 1;
while (child<size){
//找parent中较小的孩子
if(res[child+1] < res[child]){//如果右孩子比左孩子小
child+=1;
}
//比较parent与child的大小
if(res[parent]>res[child]){
swap(parent,child);
//调整后,如果子树不满足小堆的性质,继续调整
parent=child;
child=parent*2+1;
}else{
return;//以parent为根的二叉树已经满足小堆性质
}
}
}
private void swap(int parent,int child){
int temp = res[parent];
res[parent]=res[child];
res[child]=temp;
}
- 堆的创建
对于根节点的左右子树不满足堆的特性,我们需要不断地进行调整:
public static void createHeap(int[] array) {
//找到第一个非叶子节点(根) child=parent*2+1 ----- parent = (child-1)/2
int lastLeaf= (size-1)-1>>1;
//调整很多个根节点 从下往上找叶子节点
for (int root = lastLeaf; root >=0 ; root--) {
shiftDown(root);//每一个根节点都要向下调整
}
}
- 堆的插入
堆的插入总共需要两个步骤:
- 先将元素放入到底层空间中(注意:空间不够时需要扩容)
- 将最后新插入的节点向上调整,直到满足堆的性质
private boolean offer(int x){
grow();//扩容
//1、将元素尾插到数组中
res[size++] = x;
//2、检测新元素插入后是否破坏小堆的性质
shiftUp(size-1);//向上调整
return true;
}
private void shiftUp(int child) {
int parent = (child - 1) >> 1;
if (res[parent] > res[child]) {
swap(parent, child);
//调整后,可能导致不满足堆的性质 继续向上调整
child = parent;
parent = (child - 1) >> 1;
} else {
return;
}
}
private void swap(int parent,int child){
int temp = res[parent];
res[parent]=res[child];
res[child]=temp;
}
private void grow(){
if(size>= res.length){
int oldCapacity = res.length;
int newCapacity = oldCapacity+ ((oldCapacity<64)?(oldCapacity + 2):(oldCapacity >> 1));
}
}
- 堆的删除
堆的删除一定删除的是堆顶元素:
- 将堆顶元素对堆中最后一个元素交换;
- 将堆中有效数据个数减少一个;
- 对堆顶元素进行向下调整;
private int poll(){
int a = res[0];
swap(0,size-1);//0号元素和新插入元素交换
size--;
shiftDown(0);//向下调整
return a;
}