优先级队列 PriorityQueue
一、概念
前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话。
在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。
二、优先级队列的模拟实现
JDK1.8中的PriorityQueue底层使用了堆的数据结构,而堆实际就是在完全二叉树的基础之上进行了一些元素的调整。
博客:堆讲解博客链接
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* Description:
* User: 95439
* Date: 2022-10-30
* Time: 14:53
*/
public class MyPriorityQueue {
public int[] elem;
public int usedSize;
public static final int DEFAULT_SIZE = 10;
public MyPriorityQueue(){
elem = new int[DEFAULT_SIZE];
}
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-1-1)/2; parent >= 0 ; parent--) {
// 统一的调整方案
shiftDown(parent,usedSize);
}
}
/**
*
* @param parent 每个父亲节点
* @param len 每颗子树调整的结束位置必须<len
*/
private void shiftDown(int parent,int len){
int child = 2*parent+1;
// 必须保证有左孩子
while(child < len){
// 有右孩子时才判断(下行语句的顺序不能变!)
if(child+1 < len && elem[child+1] > elem[child]){
child++;
}
// 到这里,child下标一定是左右孩子最大值的下标
if(elem[child] > elem[parent]){
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
parent = child;
child = 2*parent+1;
}else{
// 因为是从下面调上来的,所以若符合大根堆下面一定也符合,可以直接break
break;
}
}
}
public void offer(int val){
if(isFull()){
// 扩容
elem = Arrays.copyOf(elem,2*elem.length);
}
this.elem[usedSize] = val;
usedSize++;
// 再次调整为大根堆
shiftUp(usedSize-1);
}
private void shiftUp(int child){
int parent = (child-1)/2;
while(parent >= 0){ // 这里一定是parent>=0 或者 是child>而不是parent> (顺着逻辑想一下)
if(elem[child] > elem[parent]){
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
child = parent;
parent = (child-1)/2;
}else{
break;
}
}
}
public boolean isFull(){
return usedSize == elem.length;
}
public int poll(){
if(isEmpty()){
return -1;
}
int tmp = elem[0];
elem[0] = elem[usedSize-1];
elem[usedSize-1] = tmp;
usedSize--;
// 保证依然是大根堆
shiftDown(0,usedSize);
return tmp;
}
public boolean isEmpty(){
return usedSize == 0;
}
public int peek(){
if(isEmpty()){
return -1;
}
return elem[0];
}
}
public class Test {
public static void main(String[] args) {
MyPriorityQueue myPriorityQueue = new MyPriorityQueue();
int[] array = {27,15,19,18,28,34,65,49,25,37};
myPriorityQueue.initElem(array);
myPriorityQueue.createHeap();
myPriorityQueue.offer((80));
System.out.println(myPriorityQueue.poll());
System.out.println(myPriorityQueue.peek());
System.out.println(myPriorityQueue.peek());
System.out.println("fewfwefwe"); // 方便调试
}
}
三、集合类 PriorityQueue
3.1 PriorityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。
关于PriorityQueue的使用要注意:
- 使用时必须导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
。 - PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常。
- 不能插入null对象,否则会抛出NullPointerException异常。
- 没有容量限制,可以插入任意多个元素,其内部可以自动扩容。
- 插入和删除元素的时间复杂度为O(log2N)。
- PriorityQueue底层使用了堆数据结构。
- PriorityQueue默认情况下是小堆,即每次获取到的元素都是最小的元素。
3.2 PriorityQueue常用方法介绍
3.2.1 源码
private static final int DEFAULT_INITIAL_CAPACITY = 11;
transient Object[] queue; // non-private to simplify nested class access
private int size = 0;
private final Comparator<? super E> comparator;
构造:
调用offer
方法:
第一次offer,我们没有产生比较,直接放到了0下标。
不是第一个元素,则调用siftUp
方法:
没有传比较器,进入siftUpComparable
方法:
改变大堆小堆时:
- 自定义类:改变
compareTo()
方法即可。(相减的前后顺序) - 包装类:自己实现比较器!:
class IntCmp implements Comparator<Integer>{
public int compare(Integer o1,Integer o2){
// return o2 - o1;
return o2.compareTo(o1);
}
}
PriorityQueue<Integer> p = new PriorityQueue<>(new IntCmp());
或者使用匿名内部类:
(可以理解为:这里有一个类实现了Comparator这个接口,并且重写了它的方法)
构造大堆:(传入了比较器)
调用offer
方法:
不是第一个元素,则调用siftUp
方法:
传入了比较器,所以进入siftUpUsingComparator
方法:
总结:
- 当没有传数组容量的时候,默认是11。
- 当没有传入比较器的时候,元素必须是可比较的。
- 优先使用的是比较器来比较。
3.2.2 优先级队列的构造
此处只是列出了PriorityQueue中常见的几种构造方式,其他的可以参考帮助文档。
构造器 | 功能介绍 |
---|---|
PriorityQueue() | 创建一个空的优先级队列,默认容量是11 |
PriorityQueue(int initialCapacity) | 创建一个初始容量为initialCapacity的优先级队列,注意:initialCapacity不能小于1,否则会抛IllegalArgumentException异常 |
PriorityQueue(Collection<? extends E> c) | 用一个集合来创建优先级队列 |
注意:默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户提供比较器(详见上面源码)。
3.2.3 插入/删除/获取优先级最高的元素
函数名 | 功能介绍 |
---|---|
boolean offer(E e) | 插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度 O(log2N) ,注意:空间不够时候会进行扩容 |
E peek() | 获取优先级最高的元素,如果优先级队列为空,返回null |
E poll() | 移除优先级最高的元素并返回,如果优先级队列为空,返回null |
int size() | 获取有效元素的个数 |
void clear() | 清空(引用类型都先置为null) |
boolean isEmpty() | 检测优先级队列是否为空,空返回true |
3.2.4 扩容方式
注意:以下是JDK 1.8中,PriorityQueue的扩容方式:
优先级队列的扩容说明:
- 如果容量小于64时,是按照oldCapacity的2倍方式扩容的;
- 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的;
- 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容。
四、优先级队列的应用
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
思路:
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都
不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
- 用数据集合中前K个元素来建堆:
前k个最大的元素,则建小堆;
前k个最小的元素,则建大堆。 - 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素。
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
例题:设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。
OJ链接
方法一:(依然不能解决数据量大的问题)
我们使用优先级队列(小堆):
代码实现:
class Solution {
public int[] smallestK(int[] arr, int k) {
// 建立一个小根堆
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
// 取出数组中的每个元素放入小根堆中
for(int i = 0;i < arr.length; i++){
priorityQueue.offer(arr[i]);
}
// 弹出K个元素,存放在ret中返回
int[] ret = new int[k];
for(int i = 0;i < k; i++){
ret[i] = priorityQueue.poll();
}
return ret;
}
}
时间复杂度: O(Nlog2N+Klog2N)
(建堆时相当于向上调整建堆,时间复杂度为O(Nlog2N))
(K一般很小,可以忽略)
方法二:
1.先将这组数据当中的前K个元素,建立为大根堆;
2.从第K+1个元素开始,每次和堆顶元素进行比较,如果i下标的元素小于堆顶元素,则出堆、入堆。
区别:没有整体建堆,而是大小为K的堆。
代码实现:
class Solution {
public int[] smallestK(int[] arr, int k) {
// 参数合法性
if(arr == null || k <= 0){
return new int[0];
}
// 建立一个容量为k的大根堆
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(k,new Comparator<Integer>(){
public int compare(Integer o1,Integer o2){
return o2.compareTo(o1);
}
});
// 先入满
for(int i = 0;i < k; i++){
priorityQueue.offer(arr[i]);
}
// 进行比较与出堆入堆
for(int i = k;i < arr.length; i++){
if(priorityQueue.peek() > arr[i]){
priorityQueue.poll();
priorityQueue.offer(arr[i]);
}
}
// 放进数组ret返回
int[] ret = new int[k];
for(int i = 0;i < k; i++){
ret[i] = priorityQueue.poll();
}
return ret;
}
}
时间复杂度:O(K*log2K+(N-K)*log2K),即 O(Nlog2K)。
(建堆时相当于向上调整建堆,时间复杂度为O(Nlog2N))
那么如果要求第K小的呢?
思路:
1.建立一个大小为K的大根堆;
2.遍历剩下的N-K个元素…
(与方法二求前K个最小的步骤是一样的)
3.直接弹出堆顶元素,就是第K小的值。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/118602.html