优先级队列(堆) 与 Priority Queue

前言

这篇文章来和大家分享一下优先级队列与PriorityQueue基本知识,内部逻辑,具体使用.


一、什么是优先级队列(堆)

优先级队列(Priority Queue) 是一种特殊的队列数据结构,它的核心特点是不再遵循"先进先出(FIFO)" 的原则,而是根据元素的优先级来决定出队顺序。

核心原理

  1. 入队规则
    元素插入队列时,会根据自身的优先级被放置到合适的位置,保证队列始终是有序状态(按优先级升序或降序排列)。
  2. 出队规则
    每次出队操作都会取出优先级最高的元素,而不是最早入队的元素。

常见实现方式

优先级队列通常基于以下两种数据结构实现:

  • 堆(Heap)
    这是最常用、最高效的实现方式,分为最大堆最小堆
    • 最大堆:根节点是队列中优先级最高的元素,出队时取出根节点。
    • 最小堆:根节点是队列中优先级最低的元素,出队时取出根节点。
      堆的插入和删除操作的时间复杂度均为 O ( log ⁡ n ) O(\log n) O(logn),效率远高于普通数组或链表。
  • 有序数组/链表
    插入元素时直接按优先级排序,出队时直接取队首或队尾元素。
    缺点是插入操作的时间复杂度为 O ( n ) O(n) O(n),数据量大时效率较低。

与普通队列的区别

特性 普通队列 优先级队列
排序规则 先进先出(FIFO) 按元素优先级排序
出队对象 最早入队的元素 优先级最高的元素
常用实现 数组、链表 堆(最大堆/最小堆)

在上面我们提到了优先级队列,一般是使用堆进行实现的,那什么是堆?又存在什么性质呢?

二、什么是堆?

堆(Heap) 是一种完全二叉树 结构的数组对象,同时满足堆序性质,是实现优先级队列的核心数据结构。

堆的两个核心条件

(1)结构条件:完全二叉树

完全二叉树的定义是:

  • 除了最后一层,其他层的节点数都是满的;
  • 最后一层的节点都靠左排列,没有空缺。

这种结构的优势是可以用数组直接存储,无需额外的指针,通过下标就能快速定位任意节点的父节点和子节点。

  • 若节点下标为 i(数组下标从 0 开始):
    • 父节点下标:(i - 1) // 2
    • 左子节点下标:2 * i + 1
    • 右子节点下标:2 * i + 2

(2)堆序条件:父节点与子节点的大小关系

根据堆序的不同,堆分为两种:

  • 最大堆(大顶堆)
    每个父节点的值 其左右子节点的值,堆顶(根节点)是整个堆的最大值。
  • 最小堆(小顶堆)
    每个父节点的值 其左右子节点的值,堆顶(根节点)是整个堆的最小值。

注意:堆只要求父节点和子节点的大小关系,同一层的子节点之间没有顺序要求

堆的核心操作

堆的操作围绕"维护堆序性质"展开,常见操作有:

  1. 插入(offer)

    • 先将新元素添加到数组末尾(完全二叉树的最后一个位置);
    • 从下往上调整(称为 上浮/Shift Up),将新元素与父节点比较,若不满足堆序则交换,直到满足条件。
    • 时间复杂度: O ( log ⁡ n ) O(\log n) O(logn)
  2. 删除堆顶(poll)

    • 先取出堆顶元素(数组第一个元素);
    • 将数组最后一个元素移到堆顶;
    • 从上往下调整(称为 下沉/Shift Down),将堆顶元素与左右子节点比较,若不满足堆序则与更符合条件的子节点交换,直到满足条件。
    • 时间复杂度: O ( log ⁡ n ) O(\log n) O(logn)

三、常用代码手动实现

  • 这一部分的逻辑是较为简单的,小伙伴们如果是第一次接触,非常建议大家上手实现一下~
    我就都分成一个一个小的代码块了 大家在学习的时候也可以分成基本成员变量 ,成员方法,**辅助方法(在成员方法中被调用的小方法)**进行学习

基本方法

java 复制代码
import java.nio.channels.Pipe;
import java.util.Arrays;
import java.util.concurrent.BlockingDeque;

public class MyHeap {
    //成员变量
    public int[] elem;
    public int usedSize;

    /****
     *构造方法,容量实现
     */
    public MyHeap(int cap) {
        this.elem = new int[cap];
    }

    /***
     * 将传入的数组实例化到elem数组之中,方便后面的调整
     * @param arr
     */
    public void init(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            elem[i] = arr[i];
            usedSize++;
        }
    }

    /***
     * 建堆
     * @param
     */
    public void createHeap() {
        for (int parent = (usedSize - 1 - 1) / 2; parent >= 0; parent--) {
            siftDown(elem, parent);
        }

    }

    public void siftDown(int[] arr, int p) {
        //1.找到左右孩子的最大值
        //1.1左孩子一定存在 判断右边的孩子  最终确定孩子的最大值
        int child = 2 * p + 1;
        if (child + 1 < usedSize && arr[child + 1] > arr[child]) {
            child++;
        }

        //2.父亲与孩子最大值进行比较 进行交换 或者交换
        while (p < usedSize) {
            //孩子大 进行交换
            if (arr[child] > arr[p]) {
                swap(arr, p, child);
                //更新p的值
                p  = child;
                child = 2 * p + 1;
            }else {
                break;
            }
        }
    }

    public void siftUp(int[] arr, int child){
        // 找到父节点
        int parent = (child - 1) / 2;

        // 当孩子节点不是根节点时循环
        while (child > 0) {
            // 如果孩子节点比父节点大,则交换
            if (elem[child] > elem[parent]) {
                swap(elem,child, parent);
                // 继续向上调整
                child = parent;
                parent = (child - 1) / 2;
            } else {
                // 孩子节点已经比父节点小,满足大根堆性质
                break;
            }
        }


    }
    public void swap(int[] arr ,int p,int c){
        int tmp = arr[p];
        arr[p] = arr[c];
        arr[c]  =tmp;
    }

    public void offer(int val){
        //判断满
        if (isFull()){
            Arrays.copyOf(elem,2*elem.length);
        }

        elem[usedSize] = val;
        siftUp(elem,usedSize);
        usedSize++;

    }

    public int poll(){
        //判断是不是空
        if (isEmpty()){
            return -1;
        }

        int top = elem[0];
        elem[0] =elem[usedSize-1];
        usedSize--;
        //重新建堆
        siftDown(elem,0);
        return top;
    }

    public int peek(){
            //判断是不是空
            if (isEmpty()){
                return -1;
            }

            int top = elem[0];
            return top;
  }

  public int size(){
        return usedSize-1;
  }

  public void clear(){
      for (int i = 0; i < elem.length; i++) {
          elem[i] = 0;
      }
  }



    public boolean isFull(){
        return usedSize == elem.length;
    }

    public boolean isEmpty(){
        return elem.length == 0;
    }

}

调整方式的选择

为什么在offer时候选择向上调整,在poll的时候选择向下调整?

这个问题问到了堆操作的核心逻辑------调整方向完全由新元素的初始位置和堆序规则决定,目的是用最少的比较交换次数,快速恢复堆的性质。

我们分别拆解 offer(插入)和 poll(删除堆顶)两个操作的逻辑:

1. offer(插入元素):选择向上调整(上浮)的原因

  1. 新元素的初始位置固定
    堆是完全二叉树,插入元素时必须放在数组末尾 (也就是完全二叉树的最后一个空位),这是为了保证堆的结构条件不被破坏。
  2. 新元素的"目标位置"在上方
    堆序规则要求父节点和子节点满足大小关系(比如最大堆中父 ≥ 子)。新元素在数组末尾,它只有父节点,没有子节点。
    • 如果新元素比父节点更"符合堆顶要求"(比如最大堆里新元素比父节点大),就需要和父节点交换,一步一步往上挪。
    • 这个过程只需要和父节点单向比较 ,直到找到自己的正确位置,也就是向上调整
  3. 反向思考:如果此时向下调整,新元素根本没有子节点,调整就无从谈起。

2. poll(删除堆顶):选择向下调整(下沉)的原因

  1. 堆顶空缺的填补方式
    删除堆顶后,堆的结构被破坏,我们会把数组最后一个元素移到堆顶,填补空缺(这是保证完全二叉树结构的最优方式)。
  2. 新堆顶的"目标位置"在下方
    移到堆顶的这个元素,原本在数组末尾,大概率不满足堆序规则。此时它有左右两个子节点 ,没有父节点。
    • 我们需要把它和更符合堆序的子节点交换(比如最大堆里,和更大的子节点交换),一步一步往下挪。
    • 这个过程只需要和子节点单向比较 ,直到找到正确位置,也就是向下调整
  3. 反向思考:如果此时向上调整,新堆顶已经是根节点,没有父节点,调整无法进行。

四、PriorityQueue

  • 有了上面的手动是实现,我们对堆有了一个简单理解,在java官方中也有类似的结构已经封装好了,我们可以直接使用,这就是PriorityQueue.

在编程中,PriorityQueue优先级队列的实现类 ,不同编程语言的标准库中都有对应的实现,核心逻辑基于堆结构,遵循"优先级高的元素先出队"的规则。

Java 的 java.util.PriorityQueue 是基于最小堆实现的优先级队列,是最典型的实现之一。

1. 核心特性

  • 默认规则 :默认按元素的自然顺序升序排列,堆顶是优先级最低(值最小)的元素。
  • 自定义优先级 :可以通过传入 Comparator 接口的实现类,自定义元素的比较规则(比如改成最大堆)。
  • 底层结构 :基于动态数组实现的完全二叉树,支持自动扩容。
  • 线程安全性 :非线程安全;如果需要线程安全的版本,可使用 PriorityBlockingQueue

2. 常用方法

方法 功能 时间复杂度
add(E e) / offer(E e) 插入元素,维护堆序 O ( log ⁡ n ) O(\log n) O(logn)
peek() 获取堆顶元素(不删除) O ( 1 ) O(1) O(1)
poll() 移除并返回堆顶元素 O ( log ⁡ n ) O(\log n) O(logn)
remove(Object o) 移除指定元素 O ( n ) O(n) O(n)
isEmpty() 判断队列是否为空 O ( 1 ) O(1) O(1)
size() 获取队列元素个数 O ( 1 ) O(1) O(1)

3.Priority Queue 构造方法

Java 中的 PriorityQueue 位于 java.util 包下,其构造方法共有 7 个重载版本 ,核心是围绕 初始容量比较器(自定义优先级规则) 来设计的。

所有构造方法的底层逻辑都是:初始化一个基于动态数组的最小堆(默认) ,如果传入了 Comparator,则按照比较器规则维护堆序。

核心前提

  1. 默认排序规则 :如果没有指定比较器,PriorityQueue 会要求元素实现 Comparable 接口,按照自然顺序升序排列(最小堆)。
  2. 初始容量的作用:底层数组的初始长度,当元素数量超过容量时,会自动扩容(扩容规则:当前容量 < 64 时,扩容为 2 倍 + 2;≥ 64 时,扩容为 1.5 倍)。
  3. 线程安全性 :所有构造方法创建的 PriorityQueue 都是非线程安全 的,线程安全场景需使用 PriorityBlockingQueue

7 个构造方法详解

1. 无参构造方法
java 复制代码
public PriorityQueue()
  • 作用 :创建一个初始容量为 11PriorityQueue,遵循元素的自然顺序(最小堆)。

  • 要求 :存入的元素必须实现 Comparable 接口(如 IntegerString),否则会抛出 ClassCastException

  • 示例

    java 复制代码
    PriorityQueue<Integer> pq = new PriorityQueue<>();
    pq.offer(5);
    pq.offer(2);
    System.out.println(pq.peek()); // 输出 2(自然升序,最小堆)
2. 指定初始容量的构造方法
java 复制代码
public PriorityQueue(int initialCapacity)
  • 参数initialCapacity - 初始容量,必须 ≥ 1,否则抛出 IllegalArgumentException

  • 作用 :创建一个指定初始容量的 PriorityQueue,遵循元素的自然顺序。

  • 适用场景:提前知道元素数量,指定初始容量可以减少扩容次数,提升性能。

  • 示例

    java 复制代码
    // 初始容量 20,避免频繁扩容
    PriorityQueue<String> pq = new PriorityQueue<>(20);
    pq.offer("B");
    pq.offer("A");
    System.out.println(pq.peek()); // 输出 A(字符串自然升序)
3. 指定初始容量 + 比较器的构造方法
java 复制代码
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator)
  • 参数

    • initialCapacity:初始容量(≥ 1)。
    • comparator:自定义的比较器,用于指定优先级规则;可以为 null,此时等价于自然顺序。
  • 作用 :创建指定容量 + 自定义优先级的 PriorityQueue最常用的构造方法之一

  • 适用场景 :元素没有实现 Comparable,或者需要自定义优先级(比如最大堆)。

  • 示例(构建最大堆)

    java 复制代码
    // 初始容量 10,比较器为降序,构建最大堆
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>(10, Comparator.reverseOrder());
    maxHeap.offer(3);
    maxHeap.offer(1);
    maxHeap.offer(5);
    System.out.println(maxHeap.peek()); // 输出 5(最大堆顶)
4. 传入集合(Collection)的构造方法
java 复制代码
public PriorityQueue(Collection<? extends E> c)
  • 参数c - 要转换为优先级队列的集合(如 ArrayListHashSet)。

  • 作用

    • 如果集合 cSortedSet 或另一个 PriorityQueue,则新队列继承原集合的排序规则
    • 否则,按照自然顺序构建最小堆。
  • 容量 :新队列的初始容量等于集合 c 的大小。

  • 示例

    java 复制代码
    List<Integer> list = Arrays.asList(7, 2, 9);
    PriorityQueue<Integer> pq = new PriorityQueue<>(list);
    System.out.println(pq.peek()); // 输出 2(自然升序)
5. 传入优先级队列(PriorityQueue)的构造方法
java 复制代码
public PriorityQueue(PriorityQueue<? extends E> c)
  • 参数c - 另一个 PriorityQueue

  • 作用 :创建一个与原 PriorityQueue 相同容量、相同比较器、相同元素的新队列。

  • 注意 :是浅拷贝,元素本身不会被复制。

  • 示例

    java 复制代码
    PriorityQueue<Integer> src = new PriorityQueue<>(Comparator.reverseOrder());
    src.offer(5); src.offer(1);
    // 新队列和原队列一样是最大堆
    PriorityQueue<Integer> dest = new PriorityQueue<>(src);
    System.out.println(dest.peek()); // 输出 5
6. 传入有序集合(SortedSet)的构造方法
java 复制代码
public PriorityQueue(SortedSet<? extends E> c)
  • 参数c - 一个 SortedSet(如 TreeSet)。

  • 作用 :创建的新队列继承 SortedSet 的比较器和元素 ,容量等于 SortedSet 的大小。

  • 示例

    java 复制代码
    // TreeSet 默认自然升序
    SortedSet<String> set = new TreeSet<>(Arrays.asList("C", "A", "B"));
    PriorityQueue<String> pq = new PriorityQueue<>(set);
    System.out.println(pq.peek()); // 输出 A

构造方法核心对比表

构造方法 初始容量 排序规则 适用场景
无参 11 自然顺序 快速创建最小堆,元素是 Comparable 类型
指定容量 自定义 自然顺序 已知元素数量,减少扩容
容量 + 比较器 自定义 自定义规则 自定义优先级(如最大堆)
传入 Collection 集合大小 自然顺序(SortedSet/PQ 除外) 直接将集合转为优先级队列
传入 Comparator(Java8+) 11 自定义规则 无需指定容量的自定义优先级场景

总结

  • 到这里我的分享就先结束了~,希望对你有帮助
  • 我是dylan 下次见~
    • 无限进步
相关推荐
全栈独立开发者4 小时前
点餐系统装上了“DeepSeek大脑”:基于 Spring AI + PgVector 的 RAG 落地指南
java·人工智能·spring
dmonstererer4 小时前
【k8s设置污点/容忍】
java·容器·kubernetes
super_lzb4 小时前
mybatis拦截器ParameterHandler详解
java·数据库·spring boot·spring·mybatis
程序之巅4 小时前
VS code 远程python代码debug
android·java·python
我是Superman丶4 小时前
【异常】Spring Ai Alibaba 流式输出卡住无响应的问题
java·后端·spring
墨雨晨曦884 小时前
Nacos
java
罗湖老棍子4 小时前
【例4-6】香甜的黄油(信息学奥赛一本通- P1345)
算法·图论·dijkstra·floyd·最短路算法·bellman ford
不染尘.4 小时前
进程切换和线程调度
linux·数据结构·windows·缓存
invicinble4 小时前
seata的认识与实际开发要做的事情
java
jghhh015 小时前
基于C#实现与三菱FX系列PLC串口通信
开发语言·算法·c#·信息与通信