1.优先级队列
1.1概念
前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但是有些情况下,操作的数据可能带有优先级时,可能需要优先级高的元素先入队列,该场景中,使用队列显然不合适,比如在手机上玩游戏的时候,如果有来电,我们应当让系统先处理打进来的电话;在学校排座位的时候,可以先让部分成绩好的选作为,再让从低到高的同学选座位。
在这种情况下,数据结构应当提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)
2.优先级队列的模拟实现
JDK1.8的PreorityQueue底层使用了堆这种数据结构,而堆实际就是在完成完全二叉树的基础上进行了一些调整。
2.1堆的概念
如果有一个关键码的集合K={k0,k1,k2,...,kn-1},把它的所有元素按照完全二叉树的顺序储存方式储存在一个一维数组中,并满足ki<=k2i+1且ki<=k2i+2或ki>=k2i+1且ki>=k2i+2,i=0,1,2...,则称为小堆或大堆。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
【堆的性质】
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一颗完全二叉树。
2.2堆的储存方式
从堆的概念可知,堆是一颗完全二叉树,因此可以层序的规则采取顺序的方式来高效储存。
【注意】对于 非完全二叉树,则不适用使用顺序方式来储存,因为普通二叉树的有很多的节点是null,我们需要表示出来一些位置为空,这将会导致对于 空间的利用率很低,有很多的空间只是为了占位置。
将元素储存到数组中后,可以根据二叉树章节的性质对树进行还原。假设i为节点在数组中的下标,则有
如果i为0,则i表示的节点为根节点,否则i的双亲节点为(i-1)/2;
如果2*i+1小于节点个数,则节点i的左孩子节点下标是2*i+1,否则没有左孩子;
如果2*i+2小于节点个数,则则节点i的右孩子节点下标是2*i+2,否则没有右孩子。
2.3堆的创建
2.3.1堆向下调整
对于集合{27,15,19,18,28,34,65,49,25,37}中的数据,我们如何将其创建成堆呢?(以创建小根堆为例)
向下调整过程(以小根堆为例):
让parent标记需要调整的节点,child标记parent的左孩子(注意:我们要判断是否有左孩子)
如果parent的左孩子存在,即child<size,进行以下操作,直到parent的左孩子不存在
parent的右孩子是否存在,存在找到左右孩子中最小的孩子,用child进行标记;
将parent与较小的孩子进行child进行比较,看是否需要调整:如果调整,我们进行调整后,让parent=child,在进行同样的循环,直到不交换元素或没有孩子节点。
public class Test {
//向下调整,小根堆
public void shiftDown(int[] array, int parent) {
int size = array.length;
if(size == 0) {
return;
}
int child = parent * 2 + 1;
int tmp;
while(child < size) {
if(child + 1 < size && array[child] > array[child + 1]) {
child++;
}
if(array[child] < array[parent]) {//孩子小,进行交换
tmp = array[parent];
array[parent] = array[child];
array[child] = tmp;
parent = child;
child = parent * 2 + 1;
}else {
break;
}
}
}
public static void main(String[] args) {
Test t = new Test();
int[] array = new int[]{ 27,15,19,18,28,34,65,49,25,37 };
t.shiftDown(array, 0);
System.out.println(Arrays.toString(array));
}
}
【注意】在调整以parent为根的二叉树时,必须要满足parent的左子树和右子树已经是堆了才可以向下调整。
最坏的情况:从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为O(log2N)
2.3.2堆的创建
对于普通的队列,如{1,5,3,8,7,6},即根节点的左右子树不满足堆的特性,又该如何调整呢?
//堆的创建
public void createHeap(int[] array) {
//找到第一个要调整的父节点
int root = ((array.length - 2) >> 1);
for (int i = root; i >= 0; i--) {
shiftDown(array, i);
}
public static void main(String[] args) {
int[] array1 = new int[]{1,5,3,8,7,6 };
t.createHeap(array1);
System.out.println(Arrays.toString(array1));
}
2.3.3建堆的时间复杂度
为了简化,我们使用满二叉树来进行时间复杂度的计算。
2.4根的插入和删除
2.4.1堆的插入
堆的插入总共需要两个步骤:
先将元素放入到底层的空间中(注意:空间不足时要进行扩容);
将最后新插入的元素向上调整,直到满足堆的性质。
//向上调整 小根堆
public void shiftUp(int[] array, int child) {
if(child <= 0) {
return;
}
int parent;
while(child > 0) {
parent = (child - 1) / 2;
if(array[child] < array[parent]) {
int tmp = array[child];
array[child] = array[parent];
array[parent] = tmp;
child = parent;
}
else {
return;
}
}
}
【解释】我们如果在小根堆中,再放入一个元素,我们使用向上调整即可,再次获得小根堆。
2.4.2堆的删除
【注意】堆的删除一定是堆顶的元素(最大值或最小值)。具体如下:
将堆顶元素对堆中最后一个元素交换;
将队中有效数据个数减少一个;
堆顶元素进行向下调整。
2.5用堆模拟实现优先级队列
package Demo;
import java.util.Arrays;
/**
* Describe:模拟实现优先级队列
* User:lenovo
* Date:2023-01-25
* Time:19:51
*/
public class MyPriorityQueue {
private int[] array = new int[11];//在实际中也是默认为11;
private int size = 0;
//插入元素
public void offer(int e) {
if(size == array.length) {
if(size == 0) {
array[0] = e;
size++;
return;
}
else{
array = Arrays.copyOf(array, 2*array.length);
}
}
array[size++] = e;
//向上调整
shiftUp(array, size - 1);
}
private void shiftUp(int[] array, int child) {
if(child <= 0) {
return;
}
int parent;
while(child > 0) {
parent = (child - 1) / 2;
if(array[child] < array[parent]) {
int tmp = array[child];
array[child] = array[parent];
array[parent] = tmp;
child = parent;
}
else {
return;
}
}
}
//删除元素
public int poll() {
if(size == 0) {
throw new RuntimeException("优先级队列为空");
}
int ret = array[0];
size--;
array[0] = array[size];
shiftDown(array, 0);
return ret;
}
//向下调整,小根堆
private void shiftDown(int[] array, int parent) {
int size = array.length;
if(size == 0) {
return;
}
int child = parent * 2 + 1;
int tmp;
while(child < size) {
if(child + 1 < size && array[child] > array[child + 1]) {
child++;
}
if(array[child] < array[parent]) {//孩子小,进行交换
tmp = array[parent];
array[parent] = array[child];
array[child] = tmp;
parent = child;
child = parent * 2 + 1;
}else {
break;
}
}
}
//查看堆顶元素
public int peek() {
if(size == 0) {
throw new RuntimeException("优先级队列为空");
}
return array[0];
}
//开始检测
public static void main(String[] args) {
MyPriorityQueue a = new MyPriorityQueue();
a.offer(1);
a.offer(2);
a.offer(3);
System.out.println(a.poll());
System.out.println(a.poll());
}
}
【常见习题】
1. 下列关键字序列为堆的是()
A: 100,60,70,50,32,65 B: 60,70,65,50,32,100 C: 65,100,70,32,50,60
D: 70,65,100,32,50,60 E: 32,50,100,70,65,60 F: 50,100,70,65,60,32
2. 已知小根堆为8,15,10,21,34,16,12,关键字8删除后重新建队,在这过程中需要比较的次数是(3)
3. 最小堆[0,3,2,5,7,4,6,8],在删除栈顶元素0之后,其结果是([2,3,4,5,7,8,6])
3.常见的接口
3.1PriorityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue.
关PriorityQueue的使用要注意:
使用时必须要导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则就会抛出ClassCastException异常
不能插入null对象,否则就会抛出NullPointerException异常
没有容量的限制,可以插入任意多个元素,其内部空间可以自动扩容;
插入和删除元素的时间复杂度都是O(log2N);
PriorityQueue底层使用了堆数据结构;
PriorityQueue默认情况下是小堆——即每次获取到的元素都是最小的元素;
3.2 ProrityQueue常用接口介绍
1.优先级队列的构造
此处只介绍几种常用的构造方法:
构造器 | 功能 |
PriorityQueue() | 创建一个空的优先级队列,默认容量是11 |
PriorityQueue(int initialCapacity) | 创建一个初始容量为initialCapacity的优先级队列,注意:initicalCapacity不能小于1,否则将会抛出IIIegalArgumentException异常 |
PriorityQueue(Collection<? extends E> c) | 用一个集合来创建优先级队列 |
public static void main(String[] args) {
//创建一个空的优先级队列,默认容量为11
PriorityQueue<Integer> q1 = new PriorityQueue<>();
//创建一个空的优先级队列,容量为给定值
PriorityQueue<Integer> q2 = new PriorityQueue<>(100);//大小为100
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
//用ArrayList对象来构造一个优先级队列的对象
//q3中已经包含了三个元素
PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
System.out.println(q3.size());
System.out.println(q3.peek());
}
默认情况下,PriorityQueue队列为小堆,如果需要大堆需要用户提供比较器。
用户自己定义的比较器:直接实现了了Comparator接口,然后重写该接口中compare方法即可
//直接实现Comparator接口,来实现大根堆
class IntCmp implements Comparator<Integer> {
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
}
public class Test {
public static void main(String[] args) {
PriorityQueue<Integer> p = new PriorityQueue<>(new IntCmp());
p.offer(1);
p.offer(2);
p.offer(3);
p.offer(4);
p.offer(5);
System.out.println(p.peek());
}
}
我们直接将在创建优先级队列的时候,把比较器给它就行;
此时创建出来的就是一个大根堆。
2.插入/删除/获取优先级队列
函数名 | 功能介绍 |
boolean offer(E e) | 插入元素e,插入成功返回true,如果e对象为空,将抛出NullPointerException异常,时间复杂度O(log2N),注意空间不够时候会进行自动扩容。 |
E peek() | 获取优先级最高的元素,如果优先级队列为空,返回null |
E pool() | 移除优先级最高的元素并返回,如果优先级队列为空,返回null |
int size() | 获取有效元素的个数 |
void clear() | 清空所有内容 |
boolean isEmpty() | 检测优先级队列是否为空,空返回true |
优先级队列的扩容说明:
如果容量小于64时,是按照oldCapacity的2倍方式扩容的;
如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的;
如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容的。(大概为21亿多)
3.3 OJ练习
获取堆中前k个最大值(或最小值)。(时间复杂度尽可能的低)
import java.util.PriorityQueue;
class IntCmp implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
}
class Solution {
public int[] smallestK(int[] arr, int k) {
PriorityQueue<Integer> q = new PriorityQueue<>(new IntCmp());
int[] ret = new int[k];
if(arr == null || k <= 0) {
return ret;
}
for (int i = 0; i < k; i++) {
q.offer(arr[i]);
}
int size = arr.length;
for (int i = k; i < size; i++) {
if(q.peek() > arr[i]) {
q.poll();
q.offer(arr[i]);
}
}
int i = 0;
while(!q.isEmpty()) {
ret[i++] = q.poll();
}
return ret;
}
}
【解析】
我们以前k个最小值举例;
我们需要的是K个元素大小的空间,用于存放最小值(我们并不需要多大空间,再多的空间实际上我们并用不到后面的数据。我们使用大根堆进行存放,大根堆里存放最小值,arr[0]是最小值中的最大值,我们只需要和它比较即可。
我们先存放前k个元素,放入堆中;
数组后面的元素,我们都要与堆的首元素进行比较(这是堆的最大的数);如果q.peek()>array[i],我们删除首元素,插入array[i];反之,无操作
4.堆的应用
4.1ProrityQueue的实现
用堆最为底层结构封装优先级队列
4.2 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤
建堆
升序:建大堆
降序:建小堆
2.利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握向下调整,就可以完成堆排序
【常见习题】
,1.一组记录排序码为(5,11,7,2,3,12),利用堆排序的方法建立的初始堆为(17,11,7,2,3,5)
4.3 Top-k问题
TOP-K问题:即数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都是比较大(3.3中有讲)
比如:世界500前、富豪榜、游戏中前100的活跃玩家等
对于Top-K问题,能想到的最简单方法是直接排序,但是数据量过大,排序是很麻烦的。最佳的方式就是用堆来解决,基本思路(上面3.3)