数据结构——优先级队列(PriorityQueue)

1.优先级队列

优先级队列可以看作队列的另一个版本,队列的返回元素是由是由插入顺序决定的,先进先出嘛,但是有时我们可能想要返回优先级较高的元素,比如最大值?这种场景下就由优先级队列登场。
优先级队列底层是由堆实现的,可以说理解了堆就理解了优先级队列。

2.堆的概念

堆可以看作一颗完全二叉树。它将数据以完全二叉树的顺序存储在数组中。堆又分为大根堆小根堆,如果以完全二叉树的方式看待大小根堆。
大根堆就是对于这棵树及该树的每一棵子树都满足左右子树节点值小于根节点值
小根堆就是对于这棵树及该树的每一棵子树都满足左右子树节点值大于根节点值
具体图如下:
在这里插入图片描述

3.实现堆

堆是在一维数组中存储的,所以我们首先需要一个数组去存储我们堆中数据,然后我们还可以通过变量usedSize去表示当前堆中已有元素,具体代码如下:

public class MyHeap {
    private int [] elem;
    private int usedSize;
    public  MyHeap(){//构造方法
        this.elem=new int[10];
        this.usedSize=0;
      }
    }

为方便演示,我们直接将参数数组中元素“复制”给堆中对应元素。具体代码如下:

public void initHeap(int [] array){
        for (int i = 0; i <array.length ; i++) {
            elem[i]=array[i];
            usedSize++;
        }
    }
//运行结果:MyHeap{elem=[10, 26, 98, 75, 15, 64, 85, 32, 12, 100], usedSize=10}

显然此时的堆只能看做为一个数组,其元素按照完全二叉树顺序排列如下图,既不是大根堆也不是小根堆。
在这里插入图片描述

我们要让它变成一个堆就必须对其元素调整,本文演示调整大根堆,那么,如何调整为大根堆?
我们的思路是:从最后一颗子树开始,一颗一颗调整直到根节点,那么对于每一棵树:我们先找到其左右节点的最大值,然后拿着这个最大值跟父节点比较,比父节点大就交换,不如它大就不动。如何找到最后一棵子树:最后一个叶节点对应的父节点,若父节点为p,子节点为i,则有i=2*p+1,p=(i-1)/2。叶子节点下标为usedSize-1,那么其父节点下标就是(usedSize-1-1)/2。所以我们可以写个for循环去遍历调整每棵二叉树。存在一种情况:我在交换父节点和子节点值之后的堆依然不符合大根堆规则,比如上图中75和26换,换完之后26显然比32小,所以还需要再进行交换,我们可以在一次交换过后让parent=child;child=2*parent+1这样继续往下调整,但是我们也不能不限制的往下递归,所以需要一个结束条件,那就是child<usedSize。具体代码如下:

    for(int parent=(usedSize-2)/2;parent>=0;parent--){
            siftDown(usedSize,parent);
        }
    }
 //针对每一棵树的调整
    public void siftDown(int usedSize,int parent){
        int child=2*parent+1;
     while(child<usedSize){
         if(child+1<usedSize&&elem[child]<elem[child+1]){
             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;
         }
     }

offer

作用:此方法用于向堆中添加元素
思路:首先判断堆满没,满了扩容,然后将元素插入到堆中末尾,随后向上调整,将该元素调整到合适位置。
由于usedSize我们可以在类中直接访问到,所以我们push之后的调整只传一个当前末尾节点的下标。然后根据此下标推算出其父节点。若父节点为p,子节点为i,则有i=2*p+1,p=(i-1)/2,如果插入节点大于其父节点就交换,然后child=parent,parent=(child-1)/2,注意此时parent是一级一级往上走的,所以这种调整方式也叫向上调整,同理,刚才那个parent一级一级往下走的就是向下调整。直到根节点遍历完毕或者该插入节点遇到了比它大的父节点就结束循环。具体代码如下:

 public void push(int val){
        if(isFull()){
            elem=Arrays.copyOf(elem,2*elem.length);
        }
        elem[usedSize]=val;
        signUp(usedSize);//这里usedSize作为末尾节点下标传进去
        usedSize++;
    }

    private void signUp(int child) {
        int parent=(child-1)/2;
        while(parent>-1){
            if(elem[parent]<elem[child]){
                //交换
                int tmp=elem[parent];
                elem[parent]=elem[child];
                elem[child]=tmp;
                //调整
                child=parent;
                parent=(child-1)/2;
            }
            else {break;}
        }
    }
    private boolean isFull() {
        return usedSize==elem.length;
    }

2.2poll

作用:此方法用于拿出堆顶元素
思路:首先将堆顶元素与堆末元素交换,随后将usedSize减一,也就是逻辑上删除了它,等下次push就会覆盖它,但是交换后的堆一定不符合堆的性质,所以还需sfitDown将堆调整为正确的堆。具体代码如下:

 public int poll(){
        int val=elem[0];

        int tmp=elem[0];
        elem[0]=elem[usedSize-1];
        elem[usedSize-1]=tmp;
        usedSize--;
        siftDown(usedSize,0);
        return val;
    }

3.优先级队列的基本特性

1.优先级队列中的元素必须是能比较大小的,不然会抛出ClassCastException异常。
2.优先级队列中不能插入null对象,不然会抛出NullPointerException。
3.优先级队列默认实现小根堆。

3.1如何改为大根堆

 PriorityQueue<Integer> queue1=new PriorityQueue<>(Comparator.reverseOrder());

4.top-k问题

给定一组数据,求前K个最大/小的数据。
题目链接:https://leetcode.cn/problems/smallest-k-lcci
思路1:利用优先级队列的特性。看到那句 以任意顺序 我就知道这道题是为优先级队列量身定做的。具体代码实现:

     public int[] smallestK(int[] arr, int k) {
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        //非空校验
        int[] answer = new int[k];//根据题意,注意初始化大小一定要和返回数据数量一致
        if (arr == null || k <= 0) {
             int [] a=new int[0];
             return a;
        }
        for(int i=0;i<arr.length;i++){
            queue.offer(arr[i]);
        }
        for(int i=0;i<k;i++){
            answer[i]=queue.poll();
        }
        return answer;
    }

思路2:用堆排序。
如果求前K个最大值,我们就建小根堆。
如果求前K个最小值,我们就建大根堆。

本题求最小值,我们建大根堆,先把K个数据放到堆中,然后假定它们就是要返回的数据,因为我建的是大根堆,堆顶元素一定是目前堆中最小元素的“最大者”,可以把它理解为“门槛”,然后遍历剩余元素与这个“门槛”比较,如果比“门槛”也就是当前堆顶元素小那我就删除当前堆顶元素同时将这个元素插入进堆,然后继续遍历直到结束,最后返回堆中元素即可。
求最大值思路类似,我建小根堆这样堆顶元素就是当前堆中元素的最小值,如果你比堆顶元素大你就可以入堆,原来的元素被删除。(成王败寇这一块/.)
本题具体代码如下:

public int[] smallestK(int[] arr, int k) {
        if(arr==null||k<=0){
            int [] a=new int[0];
            return a;
        }
        PriorityQueue<Integer> queue=new PriorityQueue<>(Comparator.reverseOrder());
        for(int i=0;i<k;i++){
            queue.offer(arr[i]);
        }
        for(int i=k;i<arr.length;i++){//注意这里i从k开始遍历
            int peekVal=queue.peek();
            if(peekVal>arr[i]){
                queue.poll();
                queue.offer(arr[i]);
            }
            else{
                continue;
            }
        }
        int [] answer=new int[k];
        for(int i=0;i<k;i++){
            answer[i]=queue.poll();
        }
        return answer;
    }

That’s all.

### Java 优先级队列 PriorityQueue 的使用与实现 #### 创建 PriorityQueue 对象 在 Java 中,`PriorityQueue` 是一种特殊的队列,它能够根据元素的自然顺序或者通过指定比较器定义的顺序自动排列元素。创建 `PriorityQueue` 对象可以通过无参构造函数完成[^1]。 ```java PriorityQueue<String> priorityQueue = new PriorityQueue<>(); ``` 上述代码片段展示了如何实例化一个默认的小根堆形式的 `PriorityQueue`,其中字符串类型的元素会依据其字典序进行排序。 #### 数据结构基础——堆 `PriorityQueue` 的底层实现依赖于 **堆** 这种数据结构[^2]。具体而言,默认情况下,`PriorityQueue` 使用的是小根堆(Min Heap),即父节点总是小于等于子节点;如果需要大根堆,则可以自定义比较器来改变这种行为。 当向 `PriorityQueue` 插入新元素时,这些元素会被放置到合适的位置以维持堆属性不变。同样,在移除元素时,也是从堆顶取出最高优先级的元素并重新调整剩余部分保持堆特性。 #### 自然顺序 vs 定制比较逻辑 除了依靠对象自身的可比较接口 (`Comparable`) 来决定次序外,还可以传入定制化的 `Comparator` 实现更灵活的控制方式[^3]: ```java // 默认按升序存储整数 PriorityQueue<Integer> intPQAscend = new PriorityQueue<>(); // 利用 Comparator 构建降序排列的 Integer 队列 PriorityQueue<Integer> intPQDescend = new PriorityQueue<>(Collections.reverseOrder()); ``` 这里分别演示了两种不同方向上的数值型优先级管理方法:前者遵循常规从小到大的原则;后者则借助工具类反转原有关系从而达成相反效果。 #### 主要操作方法概览 以下是几个常用的 API 方法用于操控 `PriorityQueue` 实例: - 添加单个项至队尾:`add(E e)` 或者 `offer(E e)` - 获取但不删除头部(最大/最小值):`peek()` - 移除头结点的同时返回它的值:`poll()` 需要注意的一点是由于内部采用了动态数组作为容器载体,因此容量是可以扩展增长的. 最后附上一段综合运用以上知识点的例子程序供参考学习: ```java import java.util.PriorityQueue; import java.util.Comparator; public class Main { public static void main(String[] args){ // 初始化一个基于 String 类型的大顶堆 PriorityQueue<String> pqCustomized = new PriorityQueue<>( new Comparator<String>() { @Override public int compare(String o1, String o2) { return o2.compareTo(o1); // 反转标准字符序列得到逆序结果 } } ); pqCustomized.add("Banana"); pqCustomized.offer("Apple"); pqCustomized.offer("Orange"); System.out.println(pqCustomized.peek()); // 输出 Orange (首字母 O 大写排最前) while(!pqCustomized.isEmpty()){ System.out.print(pqCustomized.poll()+" "); /*依次打印 Orange Banana Apple */ } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值