优先级队列 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集合框架中提供了PriorityQueuePriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。
在这里插入图片描述
关于PriorityQueue的使用要注意:

  1. 使用时必须导入PriorityQueue所在的包,即:import java.util.PriorityQueue;
  2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常。
  3. 不能插入null对象,否则会抛出NullPointerException异常。
  4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
  5. 插入和删除元素的时间复杂度为O(log2N)
  6. PriorityQueue底层使用了数据结构。
  7. 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方法:
在这里插入图片描述


改变大堆小堆时:

  1. 自定义类:改变compareTo()方法即可。(相减的前后顺序)
  2. 包装类:自己实现比较器!:
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方法:
在这里插入图片描述


总结:

  1. 当没有传数组容量的时候,默认是11。
  2. 当没有传入比较器的时候,元素必须是可比较的。
  3. 优先使用的是比较器来比较。

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问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都
不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前K个元素来建堆:
    前k个最大的元素,则建小堆;
    前k个最小的元素,则建大堆。
  2. 用剩余的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小的值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yyhgo_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值