数据结构——优先队列

news2024/9/23 3:32:54

文章目录

  • 一、基本介绍
  • 二、基本操作
  • 三、实现
    • 1 实现的思路
    • 2 大顶堆实现
      • 2.1 概念
      • 2.2 完全二叉树的实现方式
      • 2.3 优先队列的图示
      • 2.4 对于基本操作实现的讲解
        • 2.4.1 检查队列是否为空 ( isEmpty )
        • 2.4.2 检查队列是否已满 ( isFull )
        • 2.4.3 查看 ( peek )
        • 2.4.4 插入 ( offer )
        • 2.4.5 删除 ( poll )
      • 2.5 Priority 接口
      • 2.6 Entry 类
      • 2.7 PriorityQueue 类
      • 2.8 测试类
      • 2.9 测试结果
  • 四、应用
    • 1. 排序问题
    • 2. 图算法
    • 3. 任务调度
    • 4. 事件驱动仿真
    • 5. 数据压缩
    • 6. 网络路由算法
    • 7. 缓存管理
  • 五、总结


一、基本介绍

优先队列(Priority Queue)是一种特殊的 队列,它的元素有 优先级 属性,按照优先级出队,元素的 优先级越高越先出队

二、基本操作

  • 插入offer):向优先队列中添加一个 具有优先级属性 的元素。
  • 删除poll):从优先队列中移除 优先级最高 的元素。
  • 查看peek):查看 优先级最高 的元素。
  • 检查队列是否为空isEmpty)。
  • 检查队列是否已满isFull)。

三、实现

1 实现的思路

优先队列的实现比较多样,例如有:

  • 无序数组实现
    • 插入:将元素插入到原先最后一个元素的下一个位置,时间复杂度为 O ( 1 ) O(1) O(1)
    • 删除和查看:寻找优先级最高的元素,时间复杂度为 O ( n ) O(n) O(n) n n n 为队列中元素的个数。
  • 有序数组实现
    • 插入:将元素插入到合适优先级的位置,时间复杂度为 O ( n ) O(n) O(n)
    • 删除和查看:直接找优先级最高的元素,时间复杂度为 O ( 1 ) O(1) O(1)
  • 大顶堆实现
    • 插入:通过二叉堆的特性,将元素 上浮 到合适优先级的位置,时间复杂度为 O ( log ⁡ n ) O(\log{n}) O(logn)
    • 删除:将二叉堆顶部的元素 下潜 到底部,将其移出,时间复杂度为 O ( log ⁡ n ) O(\log{n}) O(logn)
    • 查看:直接查看二叉堆顶部的元素即可,时间复杂度为 O ( 1 ) O(1) O(1)

注意:本文只讲解 大顶堆实现,其他实现比较简单,但时间复杂度比较高。

2 大顶堆实现

2.1 概念

要讲解大顶堆实现,需要先了解几个概念:

  • 大顶堆:是一种 二叉堆,性质为:父节点的值 大于 子节点的值
  • 二叉堆:使用 完全二叉树 的树形结构实现的堆。
  • 完全二叉树:是特殊的 二叉树,性质为:只有最后一层的节点没有满,且最后一层的节点只会出现在最后一层的左侧

2.2 完全二叉树的实现方式

对于完全二叉树,有两种实现方式:

  • 标准实现,即基于类型引用 TreeNode parent, left, right; 来实现。
  • 基于数组的实现,其父节点和子节点的对应关系如下:
    • 以下两个假设是建立在 根节点放在数组中索引为 0 0 0 的位置 这个基本条件之上的。
    • 假设父节点的索引为 p p p,则其左子节点的索引为 2 p + 1 2p + 1 2p+1,右子节点的索引为 2 p + 2 2p + 2 2p+2
    • 假设子节点的索引为 c c c,则其父节点的索引为 c / 2 c / 2 c/2。注意:根节点没有父节点

如果完全二叉树的节点个数是一定的,则推荐使用基于数组的实现,否则就老实使用标准实现。

2.3 优先队列的图示

以下是大顶堆实现的优先队列的图示:
alt text
说明:由于节点之间并未通过引用直接相连,而是通过逻辑运算将其“连接”起来,所以箭头是虚线。

2.4 对于基本操作实现的讲解

2.4.1 检查队列是否为空 ( isEmpty )

在优先队列中存储一个值 size,表示 优先队列的元素个数,如果 size == 0,则说明队列为空。

2.4.2 检查队列是否已满 ( isFull )

在本实现中,优先队列底层是一个 固定长度的 数组 data,用来存储元素,如果 size == data.length,则说明队列已满。

如果想要一个能够 自动扩容 的优先队列,使用 ArrayList 来代替数组即可。此时就不需要检查队列是否已满了。

2.4.3 查看 ( peek )

根节点的优先级最大,直接返回根节点 data[0] 即可。

2.4.4 插入 ( offer )

如果队列已满,则无需插入。否则执行插入操作:

  • 假设 待插入元素 被放到最后一个元素的下一个位置。
  • 然后将 待插入元素 与上浮到合适的位置,即依次与 比 待插入元素的优先级 小的元素 作“交换”,类似 插入排序
  • 最后将 待插入元素 放到合适的位置,并增加 size

以下是插入 200 的示例:

  • 先将其放到 79 的下一个位置。
    alt text
  • 由于 200 的优先级大于 100,所以进行交换。
    alt text
  • 由于 200 的优先级大于 133,所以进行交换。
    alt text
2.4.5 删除 ( poll )

如果队列为空,则无需删除。否则执行删除操作:

  • 先保存 堆顶元素,用于将其返回。
  • 然后交换 堆顶元素最后的元素,并缩小 size,让最后的索引指向 null(便于 GC 回收内存)。
  • 接着将 被置换到堆顶的元素 下潜到合适的位置,即依次与 比 被置换元素的优先级 大的元素 作交换。注意:此处的交换不只针对两个元素,而是三个元素,在代码中能看到这点。
  • 最后返回保存的 堆顶元素

以下是删除优先级最大的元素的示例:

  • 交换 133 和 79。
    alt text
  • 让索引 5 指向 null
    alt text
  • 在 122, 111, 79 三个数中,由于 122 最大,所以将 79 与其交换。
    alt text

2.5 Priority 接口

/**
 * 优先级接口
 * 一个类实现该接口后,可以获取该类的对象的优先级
 */
public interface Priority {
    int priority(); // 获取该对象的优先级
}

2.6 Entry 类

/**
 * 实现了 Priority 接口的类,是能放入 优先队列 的元素,用于测试优先队列
 */
public class Entry implements Priority {
    private String value; // 具体存储的值
    private int priority; // 优先级

    public Entry(String value, int priority) {
        this.value = value;
        this.priority = priority;
    }

    @Override
    public int priority() {
        return priority;
    }

    @Override
    public String toString() {
        return value + ":" + priority;
    }
}

2.7 PriorityQueue 类

/**
 * 基于大顶堆实现的优先队列
 *
 * @param <E> 放入队列的元素类型,必须实现 Priority 接口
 */
public class PriorityQueue<E extends Priority> {
    /**
     * 向队尾插入值,并将其放到合适的位置
     *
     * @param value 待插入值
     * @return 若队列已满,则返回 false;否则返回 true,表示插入成功
     */
    public boolean offer(E value) {
        if (isFull()) {
            return false;
        }

        int child = up(value); // 将插入值 上浮 到合适位置
        data[child] = value; // 在合适位置赋值
        size++; // 元素数量加一
        return true;
    }

    /**
     * 获取优先级最大的元素,并将其取出
     *
     * @return 如果队列非空,则返回队首的值;否则返回 null
     */
    public E poll() {
        if (isEmpty()) {
            return null;
        }

        E value = (E) data[0]; // 保存优先级最大的元素,之后将其返回
        data[0] = data[--size]; // 交换 优先队列中 最后一个元素 与 优先级最大的元素
        data[size] = null; // help GC
        down(0); // 将被交换到索引为 0 的最后一个元素 下潜 到合适位置
        return value;
    }

    /**
     * 获取优先级最大的元素,但不将其取出
     *
     * @return 如果队列非空,则返回队首的值;否则返回 null
     */
    public E peek() {
        if (isEmpty()) {
            return null;
        }
        return (E) data[0];
    }

    /**
     * 检查优先队列是否为空
     *
     * @return 如果优先队列为空,则返回 true;否则返回 false
     */
    public boolean isEmpty() {
        return (size == 0);
    }

    /**
     * 检查优先队列是否已满
     *
     * @return 如果优先队列已满,则返回 true;否则返回 false
     */
    public boolean isFull() {
        return (size == data.length);
    }

    public PriorityQueue(int capacity) {
        data = new Priority[capacity];
    }

    /**
     * 将插入的值 上浮 到合适的位置(下潜 比插入值优先级小的 元素)
     *
     * @param value 插入的值
     * @return 合适位置的索引
     */
    private int up(E value) {
        int child = size; // 获取待插入元素的索引
        int parent = getParent(child); // 获取其父节点的索引

        // 类似于 插入排序
        while (child > 0 // 直到 到达根节点
                && value.priority() > data[parent].priority()) { // 或者 待插入元素的优先级 小于等于 其父节点的优先级
            data[child] = data[parent]; // 将 父节点的值 赋值给 子节点,表示下潜父节点
            child = parent; // 将 子节点 更新到 父节点 处
            parent = getParent(parent); // 将 父节点 更新到 父节点的父节点 处
        }

        return child; // 返回待插入元素元素的合适索引
    }

    /**
     * 将指定的索引 下潜 到合适的位置(上浮 比指定值优先级大的 元素)
     *
     * @param parent 指定索引
     */
    private void down(int parent) {
        int left = getLeft(parent); // 获取左子节点的索引
        int right = left + 1; // 获取右子节点的索引
        int max = parent; // max 是父节点和两个子节点中,优先级最大的元素的索引。一开始假设 父节点 的优先级最大
        if (left < size // 防止 left 超过已有的元素个数
                && data[max].priority() < data[left].priority()) { // 寻找 左子节点 和 优先级最大元素 中优先级更大的元素索引
            max = left;
        }
        if (right < size // 防止 right 超过已有的元素个数
                && data[max].priority() < data[right].priority()) { // 寻找 右子节点 和 优先级最大元素 中优先级更大的元素索引
            max = right;
        }
        
        if (max == parent) { // 如果父节点的优先级最大,则不需要下潜父节点
            return;
        }
        
        swap(parent, max); // 下潜 父节点 到 更大的子节点 处,然后 max 就成为被下潜节点的索引了
        down(max); // 递归检查这个节点,并在必要时下潜它
    }

    /**
     * 获取指定 子节点 对应的 父节点 的索引
     * @param child 子节点的索引
     * @return 其对应的父节点的索引
     */
    private static int getParent(int child) {
        return (child - 1) >> 1;
    }

    /**
     * 获取指定 父节点 对应的 左子节点 的索引
     * @param parent 父节点的索引
     * @return 其对应的左子节点的索引
     */
    private static int getLeft(int parent) {
        return (parent << 1) + 1;
    }

    /**
     * 交换 data 中的两个指定索引的元素
     * @param idx1 指定索引1
     * @param idx2 指定索引2
     */
    private void swap(int idx1, int idx2) {
        Priority temp = data[idx1];
        data[idx1] = data[idx2];
        data[idx2] = temp;
    }

    private Priority[] data;    // 储存数据的数组
    private int size;           // 优先队列的元素个数
}

2.8 测试类

public class Test {
    public static void main(String[] args) {
        PriorityQueue<Entry> queue = new PriorityQueue<>(5); // 构建一个长度为 5 的优先队列
        
        // 先添加 5 个元素
        queue.offer(new Entry("task1", 4));
        queue.offer(new Entry("task2", 3));
        queue.offer(new Entry("task3", 2));
        queue.offer(new Entry("task4", 5));
        queue.offer(new Entry("task5", 1));

        System.out.println(queue.offer(new Entry("task6", 6))); // 优先队列已满,无法添加新元素

        System.out.println(queue.peek()); // 查看优先级最大的元素

		// 依次删除优先级最大的元素
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());

        System.out.println(queue.poll()); // 优先队列为空,无法删除
    }
}

2.9 测试结果

false	// offer()
task4:5 // peek()
task4:5 // poll()
task1:4 // poll()
task2:3 // poll()
task3:2 // poll()
task5:1 // poll()
null	// poll()

四、应用

1. 排序问题

  • 查找第 k 个最小元素:通过维护一个大小为 k 的优先队列(最小堆),可以高效地 找到数据集合中的第 k 个最小元素
  • 堆排序:堆排序算法使用优先队列(通常是二叉堆)作为底层数据结构,通过 不断删除堆顶元素(即当前最小值)并重新调整堆结构,实现数据的排序。

2. 图算法

  • 最短路径算法:如 Dijkstra 算法,利用优先队列(最小堆)来不断选择 当前未处理节点中距离源点最近的节点,逐步构建最短路径树。
  • 最小生成树算法:如 Prim 算法,在构建最小生成树的过程中,也使用了优先队列(最小堆)来选择 当前未加入生成树集合中权重最小的边

3. 任务调度

  • 系统任务调度:在操作系统中,任务调度器可以根据任务的优先级来 分配 CPU 时间片,优先处理优先级高的任务。
  • 多线程编程:在多线程编程中,可以使用优先队列来 管理线程的执行顺序,确保优先级高的线程能够优先获得执行机会。

4. 事件驱动仿真

  • 顾客排队算法:在模拟顾客排队等待服务的场景中,可以使用优先队列来 管理顾客的优先级(如根据等待时间、顾客重要性等因素),确保优先级高的顾客能够优先获得服务

5. 数据压缩

  • 赫夫曼编码:赫夫曼编码是一种广泛使用的数据压缩算法,它根据 字符出现的频率 构建优先队列(通常是最小堆),然后基于队列中的元素构建赫夫曼树,最终生成压缩编码。

6. 网络路由算法

  • 路由选择:在网络路由算法中,路由器可以使用优先队列来 管理路由表中的路由信息,确保在多个可能的路由中选择优先级最高(如延迟最小、带宽最大)的路由。

7. 缓存管理

  • 缓存替换策略:在缓存管理系统中,如操作系统的页面置换算法,可以使用优先队列来 管理缓存中的页面,根据页面的优先级(如访问频率、最近访问时间等)来决定哪些页面应该被替换出缓存。

五、总结

优先队列是一种特殊的队列,具有 优先级越大,越靠前 的性质,一般使用 大顶堆 来实现。此外,它可以应用到 排序、各种图(和 树)的算法 等多个领域,这些应用充分利用了优先队列能够 高效管理具有优先级元素 的能力,从而提高了系统的性能和效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1993911.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算数学精解【5】-prolog计算精解(1)

文章目录 概述什么是prolog安装 基础控制台增加规则参考文献 概述 什么是prolog Prolog&#xff08;Programming in Logic&#xff09;是一种面向演绎推理的逻辑型程序设计语言&#xff0c;最早于1972年由柯尔麦伦纳&#xff08;Colmeraner&#xff09;及其研究小组在法国马赛…

Python教程(十三):常用内置模块详解

目录 专栏列表1. os 模块2. sys 模块3. re 模块4. json 模块5. datetime 模块6. math 模块7. random 模块8. collections 模块9. itertools 模块10. threading 模块 总结 专栏列表 Python教程&#xff08;十&#xff09;&#xff1a;面向对象编程&#xff08;OOP&#xff09;P…

uniapp h5本地预览pdf教程 (含白屏|跨域解决方案)

第一步 下载pdf.js 很多pdf.js版本在真机ios环境都会白屏 经测试后2.5.207版本比较稳定&#xff0c;Android和IOS环境PDF文件都能加载成功 下载地址 https://github.com/mozilla/pdf.js/releases/tag/v2.5.207https://github.com/mozilla/pdf.js/releases/tag/v2.5.207第二步 解…

leetcode50. Pow(x, n),快速幂算法

leetcode50. Pow(x, n)&#xff0c;快速幂算法 实现 pow(x, n) &#xff0c;即计算 x 的整数 n 次幂函数&#xff08;即&#xff0c;xn &#xff09;。 示例 1&#xff1a; 输入&#xff1a;x 2.00000, n 10 输出&#xff1a;1024.00000 示例 2&#xff1a; 输入&#xff…

贵阳高新区:加强数字人才培育 引领数字经济未来

在近期举行的贵阳高新区&#xff08;贵州科学城&#xff09;2024年科技创新与成果交流夏季活动中&#xff0c;来自清华大学2022级大数据&#xff08;贵州&#xff09;全日制工程硕士专业的学生们展示了他们在城市公交数据挖掘、通勤线路优化、场景数据的稳定训练以及营运车辆风…

数据分析:多诊断指标ROC分析

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 介绍 pROC::roc函数能够使用一个指标(predictor)去区分两个或多个分组(response),并计算95%置信区间的原理基于以下几个关键点: ROC曲线:ROC曲线是一种图形表示,用于展示分类模型在所有…

【轨物洞见】从电磁感应现象的发现和应用理解科学、技术、工程的关系

电磁感应现象是19世纪以来人类最伟大的发现。有了这个发现&#xff0c;才有后来电和无线电这两大改变人类命运的技术&#xff0c;第二次、第三次工业革命才会发生&#xff0c;人类财富才会猛增、我们的生活才会越来越美好。 让我们来回顾一下电磁感应现象的发现和应用&#xff…

《系统架构设计师教程(第2版)》第13章-层次式架构设计理论与实践-05-数据架构规划与设计

文章目录 1 数据库设计与类的设计融合2. 数据库设计与XML设计融合 教材本节实在太敷衍了&#xff0c;没什么有用的内容 1 数据库设计与类的设计融合 不存在唯一正确的数据模型&#xff0c;然而却存在好的数据模型好模型的目标 将工程项目整个生存期内的花费减至最小 而不是单纯…

KL 散度(python+nlp)

python demo KL 散度&#xff08;Kullback-Leibler divergence&#xff09;&#xff0c;也称为相对熵&#xff0c;是衡量两个概率分布之间差异的一种方式。KL 散度是非对称的&#xff0c;也就是说&#xff0c;P 相对于 Q 的 KL 散度通常不等于 Q 相对于 P 的 KL 散度。 一个简…

zabbix7.0TLS-05-快速入门-触发器

文章目录 1 概述2 查看触发器3 添加触发器4 验证触发器5 查看问题6 问题恢复 1 概述 监控项用于收集数据&#xff0c;但是我们并不能时刻观测每个监控项的数据&#xff0c;看看哪个监控项的数据超过了正常可接受的数值或状态&#xff0c;比如 CPU 负载高于 90%、磁盘使用率低于…

不平衡数据:Handling Imbalanced Dataset with SMOTE导致ValueError ⚖️

不平衡数据&#xff1a;Handling Imbalanced Dataset with SMOTE导致ValueError ⚖️&#x1f4c8; 不平衡数据&#xff1a;Handling Imbalanced Dataset with SMOTE导致ValueError ⚖️&#x1f4c8;摘要引言详细介绍什么是不平衡数据集&#xff1f;⚖️SMOTE简介&#x1f4c8…

加密案例分享:电子设备制造行业

企业核心诉求选择 1.某企业规模庞大&#xff0c;分支众多&#xff0c;数据安全管理方面极为复杂&#xff1b; 2.企业结构复杂&#xff0c;包括研发、销售、财务、总部、分部、办事处、销售等单位连结成为一个庞大的企业组织&#xff0c;数据产生、存储、流转、使用、销毁变化…

Selenium + Python 自动化测试08(截图)

我们的目标是&#xff1a;按照这一套资料学习下来&#xff0c;大家可以独立完成自动化测试的任务。 上一篇我们讨论了滑块的操作方法&#xff0c;本篇文章我们讲述一下截图的操作方法。希望能够帮到爱学的小伙伴。 在实际的测试项目组中我们经常要截屏保存报错信息&#xff0c…

做个一套C#面试题

1.int long float double 分别是几个字节 左到右范围从小到大&#xff1a;byte->short->int->long->float->double 各自所占字节大小&#xff1a;1字节、2字节、4字节、8字节、4字节、8字节 2.System.Object四个公共方法的申明 namespace System {//// 摘要…

C#如何解决引用类型的“深度”克隆问题

前言 在C#中我们new一个引用类型的对象称为对象1&#xff0c;如果我们再次new一个引用类型的对象称为对象2&#xff0c;如果直接将第一个对象直接赋值给第二个对象&#xff0c;然后如果我们这时候改变对象2的值&#xff0c;你会发现对象1的值也会被更改&#xff0c;这就是引用…

在ubuntu系统上安装nginx以及php的部署

1、安装依赖包 apt-get install gcc apt-get install libpcre3 libpcre3-dev apt-get install zlib1g zlib1g-dev sudo apt-get install openssl sudo apt-get install libssl-dev 2、到nginx官方下载 官方地址&#xff1a;nginx: download 图中下载的nginx1.22版本&#…

Python | Leetcode Python题解之第322题重新安排行程

题目&#xff1a; 题解&#xff1a; class Solution:def findItinerary(self, tickets: List[List[str]]) -> List[str]:def dfs(curr: str):while vec[curr]:tmp heapq.heappop(vec[curr])dfs(tmp)stack.append(curr)vec collections.defaultdict(list)for depart, arri…

element-ui周选择器,如何获取年、周、起止日期?

说明 版本&#xff1a;vue2、element-ui2.15.14 element-ui的日期选择器可以设为周&#xff0c;即typeweek&#xff0c;官方示例如下&#xff1a; 如果你什么都不操作&#xff0c;那么获取的周的值为&#xff1a; value1: Tue Aug 06 2024 00:00:00 GMT0800 (中国标准时间)如…

分布式存储ceph知识点整理

一、Ceph概述 如何选择存储 底层协议兼容性产品要有定位&#xff0c;功能有所取舍针对特定市场的应用存储被市场认可的存储系统 稳定性是第一位的性能第二数据功能要够用 一&#xff09;存储分类 1、本地存储 本地的文件系统&#xff0c;不能在网络上用。 如&#xff1a;ext3、…

WPF学习(11)-ToolTip控件(提示工具)+Popup弹出窗口

ToolTip控件 ToolTip控件继承于ContentControl&#xff0c;它不能有逻辑或视觉父级&#xff0c;意思是说它不能以控件的形式实例化&#xff0c;它必须依附于某个控件。因为它的功能被设计成提示信息&#xff0c;当鼠标移动到某个控件上方时&#xff0c;悬停一会儿&#xff0c;…