【Java 集合 & 数据结构】优先队列 PriorityQueue

2023-11-10

一、概述

优先队列 PriorityQueue 是 接口 Queue的实现,可以对其中元素进行排序,可以放基本的包装类型或自定义类型,对于包装类型默认为升序,对于自定义类型需手动实现 Comparator 接口。

实现:
PriorityQueue 采用 堆排序,头是按指定排序方式的最小元素,堆排序只能保证根室最大 / 最小,整个堆并不是有序的。若想按特定顺序遍历,可先调用toArray方法将其转为数组,然后使用Arrays.sort()排序遍历。

二、结构

优先队列是允许至少下列两种操作的数据结构:insert(插入)、deleteMin(删除最小者),在Java 中体现为 offer 方法和 poll 方法,它们等价于入队、出队操作。

优先队列示意图
二叉堆(binary heap)
一般借助二叉堆实现优先队列,它有两个性质:结构性堆序性,一般使用数组实现即可:
数组实现
堆是一棵完全二叉树,对数组中任一位置i上的元素,其左子节点在位置 2i 上,右子节点在 (2i + 1) 上,它的父节点在 ⌊i / 2⌋ 上。

堆序性质(heap-order property)
让操作快速执行的性质是 堆序性质,最小元位于根上,任一节点元素都小于其后裔元素。

基本操作:
insert(插入)—— percolate up(上滤)
为将一个元素插入堆中,在下一个可用位置处创建一个空穴。若X放入不破坏堆序,则完成插入;否则,我们将空穴的父节点元素移入该空穴,原父节点成为新的空穴,重复此过程,直至满足堆序性质即可,这种策略称为 上滤
插入 + 上滤
deleteMin(删除最小元)—— percolate down(下滤)
删除13之后,试图再次正确地将末置位31放入堆中,除非堆内所有元素相等,否则必定因破坏堆序性质而无法放入。可将较小子元素14置入空穴,空穴下滑一层,重复该过程,直至寻找到合适的空穴,这种策略称为 下滤

删除最小元 + 下滤

三、解析

Java 中内置了优先队列的实现 PriorityQueue 类(线程不安全),我们可以通过对它的解析,进一步理解优先队列的实现。

1. 核心属性

PriorityQueue 类内置了很多属性,对其构成、使用十分重要:

① 默认容量大小

//队列内置数组默认长度
private static final int DEFAULT_INITIAL_CAPACITY = 11;

代表队列内置数组默认长度

② 默认 Comparator 接口

@SuppressWarnings("serial") // Conditionally serializable
private final Comparator<? super E> comparator;

我们在使用 自定义类型 实例作为优先队列的元素时,需完成以下操作之一:

  • 自定义类本身实现 Comparable 接口,并实现相应的方法
  • 将实现 Comparator 接口的 匿名内部类 (可使用lambda表达式替换)作为参数传入构造方法, 该引用 指向创建的内部类实例
PriorityQueue<Car> pq = new PriorityQueue<>(new Comparator<Car>() {
	@Override
	public int compare(Car c1, Car c2) {
		return c1.getId() - c2.getId();
	}
});

若未完成指定操作,则会抛出ClassCastException异常,这是因为内部comparator默认为空,则认为类型本身已实现Comparable接口,并尝试进行类型转换:
在这里插入图片描述
③ 变化量

//注释原文
//The number of times this priority queue has been structurally modified
//注释翻译
//优先队列被更改的次数
transient int modCount;     // non-private to simplify nested class access

当 结构 / 元素 被更改时,变化量会增加
④ 平衡二叉堆(数组)

/** 注释原文
  * Priority queue represented as a balanced binary heap: the two
  * children of queue[n] are queue[2*n+1] and queue[2*(n+1)].  The
  * priority queue is ordered by comparator, or by the elements'
  * natural ordering, if comparator is null: For each node n in the
  * heap and each descendant d of n, n <= d.  The element with the
  * lowest value is in queue[0], assuming the queue is nonempty.
  */
/** 注释翻译
  *  优先队列 基于 平衡二叉堆,queue[n]的两个孩子为queue[2*n+1]、queue[2*(n+1)]。
  *  优先队列根据Comparator接口的实现排序,或根据元素的自然顺序
  *  若 comparator 属性为空,对于堆中每个节点n 都有其每个后裔节点d(n <= d)。
  *  若队列非空,最小的元素保存在queue[0]
  */
transient Object[] queue; // non-private to simplify nested class access

优先队列底层基于该数组实现(二叉堆载体)

2. 核心方法

PriorityQueue 类内置了很多实用方法:

☯ offer 方法 (入队列)

public boolean offer(E e) {
    if (e == null)	//【空值】:传入元素为空
        throw new NullPointerException();
    modCount++;	//结构/元素被更改(插入)
    int i = size;	//队列大小
    if (i >= queue.length)	//【扩容】:队列大小 >= 数组大小
        grow(i + 1);
    siftUp(i, e);	//【入值】:插入元素
    size = i + 1;	//队列大小 +1
    return true;	//成功插入元素
}

grow - 数组扩容

/**
 * 数组扩容
 * Increases the capacity of the array.
 * 
 * @param 最小期望容量
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    int oldCapacity = queue.length;	//旧数组长度
    // Double size if small; else grow by 50%
    
    //ArraysSupport类的newLength方法可计算新数组的长度
    //传入三个参数:旧长度、最小增长、期望增长
    //newLength(int oldLength, int minGrowth, int prefGrowth)
    int newCapacity = ArraysSupport.newLength(oldCapacity,
            minCapacity - oldCapacity, /* minimum growth 最小增长长度*/
            oldCapacity < 64 ? oldCapacity + 2 : oldCapacity >> 1
                                       /* preferred growth 期望增长:
                                       			旧长度 <  64:增长2
                                       			旧长度 >= 64:增长原先50%*/);
    queue = Arrays.copyOf(queue, newCapacity);
}

siftUp - 插入元素

private void siftUp(int k, E x) {
    if (comparator != null)		//未传入 comparator 实例
        siftUpUsingComparator(k, x, queue, comparator);
    else	//传入 comparator 实例
        siftUpComparable(k, x, queue);
}

若我们传入了一个构造器,我们就调用siftUpUsingComparator(k, x),否则调用 siftUpComparable(k, x)方法,这里以siftUpComparable为例:

siftUpComparable - 插入元素

private static <T> void siftUpComparable(int k, T x, Object[] es) {
    Comparable<? super T> key = (Comparable<? super T>) x;	//将目标元素强转为 Comparable
    while (k > 0) {	//第k个元素 > 0
        int parent = (k - 1) >>> 1;	//记录第k个位置的父节点索引
        Object e = es[parent];	//记录父节点元素
        if (key.compareTo((T) e) >= 0)	//调用compareTo方法比较两元素,若该元素大于父节点元素,证明无需再“上滤(percolate up)”
            break;	//退出循环
        //父元素与k 交换位置
        es[k] = e;	
        k = parent;
    }
    es[k] = key;	//放入元素
}

☯ poll 方法 (出队列)

public E poll() {
	//记录队列数组
    final Object[] es;
    //需要返回的元素:结果
    final E result;
	
    if ((result = (E) ((es = queue)[0])) != null) {	//queue[0]不为空
        modCount++;		//元素出队列,元素更改,变化值 +1
        final int n; 	//
        final E x = (E) es[(n = --size)];  //记录末置位值
        es[n] = null;	//末置位置空
        if (n > 0) {	//末置位索引 > 0
            final Comparator<? super E> cmp; //comparator 引用
            if ((cmp = comparator) == null)	//comparator 为空
                siftDownComparable(0, x, es, n);	//未使用 Comparator 方法
            else
                siftDownUsingComparator(0, x, es, n, cmp);	//使用 Comparator 方法
        }
    }
    return result;		//返回结果
}

同样,若我们传入了一个构造器,我们就调用siftDownUsingComparator(k, x),否则调用 siftDownComparable(k, x)方法,这里以siftDownComparable为例:

siftDownComparable - 删除元素

private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
    // assert n > 0;
    Comparable<? super T> key = (Comparable<? super T>)x;	//将目标元素强转为 Comparable
    int half = n >>> 1;           // loop while a non-leaf
    while (k < half) {
        int child = (k << 1) + 1; // assume left child is least
        Object c = es[child];		//保存子节点元素
        int right = child + 1;		//右子节点
        if (right < n &&
            ((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
            c = es[child = right];
        if (key.compareTo((T) c) <= 0)	//调用compareTo方法比较两元素,若该元素小于子节点元素,证明无需再“下滤(percolate down)”
            break;	//退出循环
        //子节点与k 交换位置
        es[k] = c;
        k = child;
    }
    es[k] = key;	//放入元素
}

☯ peek 方法 (队头元素 = 最小元)

public E peek() {
    return (E) queue[0];
}

四、特点

☯ 优点

插入、删除 复杂度O(n)

  • 插入、删除操作 复杂度都是O(log2 n),无需频繁移动元素,相比于普通数组的复杂度O(n),十分高效
  • 删除、插入操作,会自动调整位置,保证优先级最高元素始终作为队头元素,方便使用

☯ 缺点

  • 线程不安全,线程安全版本:PriorityBlockQueue,使用ReentrantLock加锁保护
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Java 集合 & 数据结构】优先队列 PriorityQueue 的相关文章

随机推荐

  • 在多线程中使用tensorRT

    仅记录 转自https www coder work article 4985246 import pycuda autoinit Create CUDA context import pycuda driver as cuda Main
  • Ubuntu ssh连接access deny

    一 尝试了修改配置的方法 不能解决问题 1 修改ssh配置文件vim etc ssh sshd config 设置为允许root远程登录 2 找到PermitRootLogin prohibie password 修改为 PermitRoo
  • windows11 使用 wsl2 安装 archLinux

    windows11 使用 wsl2 安装 archLinux 下载 archLinux 下载 tar gz 文件 下载地址 https mirrors tuna tsinghua edu cn archlinux iso latest 启用
  • 编译ROCKSDB总结

    Rocksdb是挺好的一个东西 就是取得一个可用的库太麻烦 之前我是用的rocksdbsharp里面他有编译好windows 和 linux的库 兼 容性还挺好 ubuntu win10 直接跑没毛病 可惜他是去年build的了 我要用的c
  • C++ - 强引用和弱引用

    原来 我认为 为什么会有引用计数这样的技术 是为了内存自动回收和节省内存 但是读完下面的几节后 内存自动回收是一个原因 但是节省内存并不是真正的原因 真正的原因是有些对象如果被复制在现实中是不合事实的 为什么有引用计数 C 中存在两种语义
  • vite vue3 规范化与Git Hooks

    优质资源分享 学习路线指引 点击解锁 知识定位 人群定位 Python实战微信订餐小程序 进阶级 本课程是python flask 微信小程序的完美结合 从项目搭建到腾讯云部署上线 打造一个全栈订餐系统 Python量化交易实战 入门级 手
  • 在Windows Server2016中安装SQL Server2016

    SQL Server2016安装硬 软件条件 点击打开链接 WinServer2016的安装参见 在虚拟机中安装Windows Server2016 1 SQL Server2016下载地址 1 SQL Server2016安装包 2016
  • SuperPunch - unity3D拳击小游戏项目源码

    SuperPunch是一个完整的项目 准备发布并且适合移动设备 它包含构建顶头拳击游戏的所有必要内容 特征 移动友好的纹理 分层的 包括 SVG 文件 包括 PNG文件 包括 C 脚本 包括文档 包括6架战斗机 包括战士动画 闲置 拳击 受
  • QChart入门教程-绘制正弦曲线

    1 创建界面 将widget作为容器进行绘图 并将widget提升为QChartView类 1 1 单击widget 右键中选择 提升 提升的类名称中填写 QChartView 会自动生成头文件名 选择 添加 将类和头文件添加进要提升的类中
  • ElasticSearch第十八讲 ES-Master节点职责和ES是如何做到数据实时性的

    Elasticsearch Master 节点的职责 由主节点负责ping 所有其他节点 判断是否有节点已经挂掉 创建或删除索引 决定分片在节点之间的分配 稳定的主节点对集群的健康是非常重要的 虽然主节点也可以协调节点 路由搜索和从客户端新
  • 6.84 C++ 遍历数组的几种方式

    1 计算出数组长度进行遍历 数组类型确定 数组中每个元素本身的字节大小就已经确定 利用 sizeof 函数可以计算出数组长度 而后利用 for 循序进行数组的遍历 2 使用类似 foreach 的方式进行遍历 C 中也可使用类似 forea
  • AVI文件与WAV文件格式

    AVI 与WAV文件都属于RIFF文件 因此都遵循RIFF文件的格式要求 先看看RIFF文件的格式 第一 RIFF 大小 AVI WAV 数据 第二 RIFF 文件中实际的数据通常采用列表 list 和块 Chunk 的形式表示 列表结构为
  • 智能门锁电路图_蓝牙门锁原理图一览 蓝牙智能门锁工作原理介绍

    蓝牙智能门锁工作原理是什么 蓝牙门锁原理图步骤详解 蓝牙智能门锁主要是通过手机开锁的方式来解锁 相比传统门锁 蓝牙门锁更加便捷 此外 还可以通过手机APP来实现门锁实时管理 访客管理 是符合智能家居时代对门锁要求的电子产品 那么蓝牙智能门锁
  • tesseract api C++使用例子

    转自 https code google com p tesseract ocr wiki APIExample APIExample API examples Updated Aug 12 2014 by theraysm gmail c
  • unsigned int 和 signed int 的区别

    unsigned int 和 signed int 的区别 对于 int 类型 默认是带有正负号的 也就是说 int 等同于 signed int signed int 等同于int 都能表示正负数 1 signed int 可以表示正整数
  • SQL优化之LIMIT语法, limit n,m 和 limit n有什么区别?

    在某些面试题中会遇到这样的问答或笔试题 limit 0 1 和 limit 1有什么区别 要准确回答这个问题就等深入明白limit一个参数和两个参数的本质区别 limit n m 中的第一次参数n表示的游标的偏移量 初始值为0 第二个参数m
  • Codeforces Round #367 (Div. 2)【贪心、差分、DP、字典树、二维链表】

    Codeforces Round 367 Div 2 A Beru taxi 就是问 我们知道一个点 从其他点到它的最少花费的时间是多少 include
  • 三次握手,四次挥手白话文

    三次握手和四次挥手是TCP协议中用于建立和断开连接的过程 三次握手 Three way Handshake 客户端向服务器发送一个SYN 同步 包 其中包含一个随机的初始序列号 表示客户端请求建立连接 服务器收到客户端的SYN包后 向客户端
  • 2022互联网精英副业指南,看到程序员的我笑了~

    不得不说 互联网人收入高 如果你以为互联网人收入高是因为工资高 年终奖丰厚 那你就错了 其实 还有一个原因是他们搞起了副业 副业千万条 闲鱼第一条 万万没想到的是 互联网人在闲鱼上赚钱也与众不同 甚至都一个比一个拼 https mmbiz
  • 【Java 集合 & 数据结构】优先队列 PriorityQueue

    优先队列 PriorityQueue 一 概述 二 结构 三 解析 1 核心属性 2 核心方法 offer 方法 入队列 poll 方法 出队列 peek 方法 队头元素 最小元 四 特点 优点 缺点 一 概述 优先队列 PriorityQ