当前位置: 首页 > news >正文

优先级队列(PriorityQueue)

优先级队列

概念

前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如 果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。

在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象一个是添加新的对象。这种数 据结构就是优先级队列(Priority Queue)。


优先级队列的模拟实现

JDK1.8中的PriorityQueue底层使用了这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整。

堆的概念

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一 个一维数组中,并满足:Ki 且 Ki= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大 堆)。将根节点最大的堆叫做最大堆或大根堆根节点最小的堆叫做最小堆或小根堆

堆的性质

1.堆中某个节点的值总是不大于或不小于其父节点的值;

2.堆总是一棵完全二叉树。

                            

堆的存储方式

从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储

注意:对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。

将元素存储到数组中后,可以根据二叉树章节的性质5对树进行还原。假设i为节点在数组中的下标,则有:

1.如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2

2.如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子

3.如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子


堆的创建
堆向下调整

对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆呢?

                            

仔细观察上图后发现:根节点的左右子树已经完全满足堆的性质,因只需将根节点向下调整好可。

向下过程(以小堆为例):

1. 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)

2. 如果parent的左孩子存在,即:child < size, 进行以下操作,直到parent的左孩子不存在

        1)parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标记

        2)将parent较小的孩子child比较,如果:

                (1)parent小于较小的孩子child,调整结束

                (2)否则:交换parent与较小的孩子child,交换完成之后,parent大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后继续2。

堆的创建

那对于普通的序列{ 1,5,3,8,7,6 },即根节点的左右子树不满足堆的特性,又该如何调整呢?

结论1:当我们采用向下调整去建堆的时候,时间复杂度为O(n);


代码示例:大根堆的【小根堆同样的道理】

package PriorityQueue;

import java.util.Arrays;

public class TestHeap {
    private int[] elem;  // 创建一个数组
    private int usedSize;  // 规定一个数组长度

    // 再来一个构造方法
    public TestHeap(){
        this.elem = new int[10];  // 初始化一个数组,长度先暂且给一个10,不够再扩容
    }



    @Override
    public String toString() {
        return "TestHeap{" +
                "elem=" + Arrays.toString(elem) +
                '}';
    }

    // 初始化一个数组
    public  void initHeap(int[] array){
        for (int i = 0; i < array.length; i++) {
            elem[i] = array[i];
            usedSize++;  // 放一个就加1
        }
    }


    /**
     * 创建一个大根堆
     */
    public void createHeap(){
        for (int parent = (usedSize-1-1)/2; parent >= 0 ; parent--) {
            shiftDown(parent,usedSize);
        }
    }

    private void shiftDown(int parent, int usedSize) {
        // 重新定义一个孩子节点的下标
        int child = (2*parent)+1;
        while (child<usedSize){
            // 进来这个循环的条件是“树还没调整完”
            if (child+1<usedSize && elem[child] < elem[child+1]){
                // 如果右孩子的值比左孩子大且没有越界
                child++;
            }
            if (elem[child] > elem[parent]){
                // 孩子节点的值比父母节点的大【左节点右节点都行】
                swap(child,parent);
                parent = child;
                child = 2*parent+1;
            }else {
                break; // 本身是大根堆了
            }
        }
    }

    // 交换函数【就是看那个值大,就拿父母节点于孩子节点交换】
    private void swap(int i,int j){
        int tmp = elem[i];
        elem[i] = elem[j];
        elem[j] = tmp;
    }
}

测试代码:

/**
     * 测试大根堆的方法
     * @param args
     */
    public static void main(String[] args) {
        TestHeap testHeap = new TestHeap();
        int[] array = {27,15,19,18,28,34,65,49,25,37};
        testHeap.initHeap(array);
        testHeap.createHeap();
        System.out.println(testHeap.toString());
        System.out.println("=======================================================");
    }

堆的插入与删除

堆的插入

堆的插入总共需要两个步骤:

1. 先将元素放入到底层空间中(注意:空间不够时需要扩容)

扩容的方法:

2. 将最后新插入的节点向上调整,直到满足堆的性质

代码示例:

/**
     * 判断数组满没满,这是往数组里面插数据的必备前提
     */
    public boolean idFull(){
        return usedSize == elem.length;  // 当数据个数等于属于长度,说明已经满了
    }


    /**
     * 接下来往数组里面插元素
     */
    public void offer(int val){
        // 如果数组已经满了就需要扩容
        if (idFull()){
            this.elem = Arrays.copyOf(elem,2*elem.length);
        }
        this.elem[usedSize] = val;
        // 向上调整
        shiftUp(usedSize);
        usedSize++;
    }

    /**
     * 向上调整的代码
     * @param
     */
    private void shiftUp(int child) {
        int parent = (child-1)/2;
        while (child > 0){
            // 当child大于0,说明还没调整完
            if (elem[child] > elem[parent]){
                swap(child,parent);
                child = parent;
                parent = (child-1)/2;
            }else {
                break;
            }
        }

    }

堆的删除

注意:堆的删除一定删除的是堆顶元素。具体如下:

1. 将堆顶元素对堆中最后一个元素交换

2. 将堆中有效数据个数减少一个【usedSize--】

3. 对堆顶元素进行向下调整

4.注意:这里要分清楚什么是向上调整?什么是向下调整?

代码示例:

 /**
     * 向下调整代码
     */
    public int poll(){
        int tem = elem[0];
        swap(0,usedSize-1);
        usedSize--;
        shiftDown(0,usedSize);
        return tem;
    }
    private void shiftDown(int parent, int usedSize) {
        // 重新定义一个孩子节点的下标
        int child = (2*parent)+1;
        while (child<usedSize){
            // 进来这个循环的条件是“树还没调整完”
            if (child+1<usedSize && elem[child] < elem[child+1]){
                // 如果右孩子的值比左孩子大且没有越界
                child++;
            }
            if (elem[child] > elem[parent]){
                // 孩子节点的值比父母节点的大【左节点右节点都行】
                swap(child,parent);
                parent = child;
                child = 2*parent+1;
            }else {
                break; // 本身是大根堆了
            }
        }
    }

    // 交换函数【就是看那个值大,就拿父母节点于孩子节点交换】
    private void swap(int i,int j){
        int tmp = elem[i];
        elem[i] = elem[j];
        elem[j] = tmp;
    }

注意:小编这里的代码可能惠用到前面的一些方法,但是这一小编只把主要的代码给展示出来。


常用接口介绍

PriorityQueue的特性

Java集合框架中提供了PriorityQueuePriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。

          

关于PriorityQueue的使用要注意:

1. 使用时必须导入PriorityQueue所在的包,即:

import java.util.PriorityQueue;

2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常.

那重写方法能不能改变不能比较得现状呢?

回答:

3. 不能插入null对象,否则会抛出NullPointerException(空指针异常)

4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容

5. 插入和删除元素的时间复杂度为O(log2(N))

6. PriorityQueue底层使用了堆数据结构

7. PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素

那能不能变成大根堆?

回答:可以的。如果需要大堆需要用户提供比较器


PriorityQueue常用接口介绍

优先级队列的构造

此处只是列出了PriorityQueue中常见的几种构造方式

注:第三个调用的是一个比较器。【只要实现collection接口的,都可以用它来构造数据】

代码展示:

public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue =
                new PriorityQueue<>();
    }

插入/删除/获取优先级最高的元素

代码展示:

    /**
     * PriorityQueue的构造
     */
    public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue =
                new PriorityQueue<>();

        // 往里面添加元素
        priorityQueue.offer(10);
        priorityQueue.offer(5);
        priorityQueue.offer(9);
        priorityQueue.offer(7);
        priorityQueue.offer(77);

        // 测试可见默认情况下是一个小根堆+删除元素
        System.out.print(priorityQueue.poll()+" ");
        System.out.print(priorityQueue.poll()+" ");
        System.out.println();
        System.out.println("=========");


        // 获得有效数据
        int size = priorityQueue.size();
        System.out.println("堆的大小为:"+size);
        System.out.println("==========");


        // 判断队列是否为空 + 清空 + 再次判断队列是否为空
        boolean result1 = priorityQueue.isEmpty();
        System.out.println(result1);
        priorityQueue.clear();
        boolean result2 = priorityQueue.isEmpty();
        System.out.println(result2);
    }

扩容方式:


Top-k问题

TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

核心思路:

1.当要找前K个最大值的时候,就建立一个K个节点的小堆

代码展示:方法+测试

/**
     * 找前三个最大的元素:建小堆
     */
    public int[] smalllestK(int[] array,int k){
        int[] ret = new int[k];  // 建立一个数组来存放topk的元素
        if (array == null || k <= 0){
            return ret;  // 直接返回一个ret;【就是什么都没有】
        }
        PriorityQueue<Integer> priorityQueue =
                new PriorityQueue<>();
        // 先用前k个元素建立一个小根堆
        for (int i = 0; i < k; i++) {
            priorityQueue.offer(array[i]);
        }
        // 再遍历数组中剩下的元素与小堆的堆顶元素比较
        for (int i = k; i < array.length ; i++) {
            int top = priorityQueue.peek();  // 先喵一眼,先不出
            if (array[i] > top){
                priorityQueue.poll();
                priorityQueue.offer(array[i]);
            }
        }
        for (int i = 0; i < k; i++) {
            ret[i] = priorityQueue.poll();
        }
        return ret;
    }




    /**
     * 测试找topK
     */
    public static void main(String[] args) {
        int[] array = {12,34,56,54,34231,23,56,89,21,11,3,4};
        Test test = new Test();
        int [] ret = test.smalllestK(array, 3);
        System.out.println(Arrays.toString(ret));
    }

代码展示2:当要求前K个小的元素,应该建立K个节点的大根堆

比较器:

class IntCmp implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1);
    }
}

方法代码:

/**
     * 找后三个最小的元素:建大堆
     */
    public int[] biglestK(int[] array,int k){
        int[] ret = new int[k];  // 建立一个数组来存放topk的元素
        if (array == null || k <= 0){
            return ret;  // 直接返回一个ret;【就是什么都没有】
        }
        PriorityQueue<Integer> priorityQueue =
                new PriorityQueue<>(new IntCmp());
        // 先用前k个元素建立一个小根堆
        for (int i = 0; i < k; i++) {
            priorityQueue.offer(array[i]);
        }
        // 再遍历数组中剩下的元素与小堆的堆顶元素比较
        for (int i = k; i < array.length ; i++) {
            int top = priorityQueue.peek();  // 先喵一眼,先不出
            if (array[i] < top){
                priorityQueue.poll();
                priorityQueue.offer(array[i]);
            }
        }
        for (int i = 0; i < k; i++) {
            ret[i] = priorityQueue.poll();
        }
        return ret;
    }

堆的应用

PriorityQueue的实现

作为底层结构封装优先级队列

堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

1.建堆

升序:建大堆

1)调整为大根堆

2)让第一个元素和最后一个未排序的元素进行交换

    // 1.升序
    public void heapSort(){
        int end = usedSize-1;
        while (end>0){
            swap(0,end);
            shiftDown(0,end);
            end--;
        }
    }

在这里面的所有代码的方法在这个博客中都有涉及到,注意方法名字,就可以找到对应的代码了。

降序:建小堆

private void shiftDownMIN(int parent, int usedSize) {
        // 重新定义一个孩子节点的下标
        int child = (2*parent)+1;
        while (child<usedSize){
            // 进来这个循环的条件是“树还没调整完”
            if (child+1<usedSize && elem[child] > elem[child+1]){
                // 如果右孩子的值比左孩子大且没有越界
                child++;
            }
            if (elem[child] < elem[parent]){
                // 孩子节点的值比父母节点的大【左节点右节点都行】
                swap(child,parent);
                parent = child;
                child = 2*parent+1;
            }else {
                break; // 本身是大根堆了
            }
        }
    }

主代码:

// 2.降序
    public void heapSortDesc() {
        int end = usedSize - 1;  // 最后一个元素的索引
        while (end > 0) {
            swap(0, end);        // 将当前最小值放到数组末尾
            shiftDownMIN(0, end); // 调整为小根堆
            end--;
        }
    }

总的测试代码:

/**
     * 测试堆排序的
     */
    public static void main(String[] args) {
        TestHeap testHeap = new TestHeap();
        int[] array = {27,15,19,18,28,34,65,49,25,37};
        testHeap.initHeap(array);
        testHeap.createHeap();
        System.out.println(testHeap.toString());
        System.out.println("=======================================================");

        testHeap.heapSort();
        System.out.println(testHeap.toString());
        System.out.println("=======================================================");

        testHeap.heapSortDesc();
        System.out.println(testHeap.toString());
        System.out.println("=======================================================");

    }

2. 利用堆删除思想来进行排序

建堆堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。


解析难点

class IntCmp implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1);
    }
}
PriorityQueue<Integer> priorityQueue =
                new PriorityQueue<>(new IntCmp());

为什么上述代码这么写就变成了大根堆呢?


相关文章:

  • 全文 - MLIR Toy Tutorial Chapter 1: Toy Language and AST
  • BM100-K系列开关量输入信号隔离器
  • 菱形虚拟继承的原理
  • WPF 浅述ToolTipService.ShowOnDisabled
  • Flask接口开发--GET接口
  • 路由选型终极对决:直连/静态/动态三大类型+华为华三思科配置差异,一张表彻底讲透!
  • Java Collection API增强功能系列之一 Arrays.asList()
  • 如何分析和解决服务器的僵尸进程问题
  • nginx服务配置练习
  • [蓝桥杯 2023 省 A] 异或和之和
  • P5356 [Ynoi Easy Round 2017] 由乃打扑克 Solution
  • Redis分布式寻址算法
  • XXL-Job 二次分片是怎么做的?有什么问题?怎么去优化的?
  • ARCGIS PRO SDK ProWindow自定义窗口DataGrid控件的应用
  • langchain-ollama的ragflow简单实现
  • 车载以太网网络测试 -23【TCPUDP通信示例】
  • 模糊规则激活方法详解(python实例对比)
  • 【Tauri2】001——安装及运行
  • shadcn如何给dialog增加关闭按钮和隐藏右上角关闭按钮
  • 重写ring3 API函数
  • 优化网络营商环境,上海严厉打击涉企网络谣言、黑灰产等违法犯罪
  • 公募基金行业迎系统性变革:基金公司业绩差必须少收费
  • 外交部回应西班牙未来外交战略:愿与之一道继续深化开放合作
  • 央行:5月8日起,下调个人住房公积金贷款利率0.25个百分点
  • 长沙天心阁举办古琴音乐会:文旅向深,让游客听见城市的底蕴
  • 上海畅通“外转内”,外贸优品成“香饽饽”