【用Java学习数据结构系列】用堆实现优先级队列

news2024/9/22 4:00:13

看到这句话的时候证明:此刻你我都在努力

加油陌生人

个人主页:Gu Gu Study
专栏:用Java学习数据结构系列
喜欢的一句话: 常常会回顾努力的自己,所以要为自己的努力留下足迹

喜欢的话可以点个赞谢谢了。
作者:小闭


优先级队列(Priority Queue)

优先级队列是一种抽象数据类型(ADT),它存储一组元素,每个元素都有一个与之关联的优先级。在优先级队列中,元素的访问顺序取决于它们的优先级,而不是它们被插入的顺序。优先级最高的元素总是最先被移除。

优先级队列的关键特性包括:

  1. 优先级规则:元素根据其优先级进行排序。通常有两种优先级规则:
    • 最大优先级:最高优先级的元素(数值最大)最先被移除。
    • 最小优先级:最高优先级的元素(数值最小)最先被移除。
  1. 插入操作:允许将新元素添加到队列中,并根据其优先级放置在正确的位置。
  2. 删除操作:移除当前优先级最高的元素。这通常被称为“弹出”(pop)操作。
  3. 查找操作:有时优先级队列支持查找当前优先级最高的元素,这被称为“查看”(peek)或“顶部”(top)操作。
  4. 动态性:优先级队列能够动态地插入和删除元素,而不仅仅是静态地存储数据。

优先级队列的常见应用:

  • 任务调度:在操作系统中,优先级队列用于管理进程或线程的调度,其中每个任务都有一个优先级。
  • 事件驱动模拟:在模拟中,事件根据其发生的时间点(优先级)被处理。
  • 图算法:在图算法如迪杰斯特拉(Dijkstra)算法和普里姆(Prim)算法中,优先级队列用于选择下一个要处理的顶点或边。
  • 数据压缩:例如霍夫曼编码,使用优先级队列来构建最优前缀编码。
  • 网络流问题:在解决最大流问题时,优先级队列用于快速找到增广路径。

实现优先级队列的数据结构:

  • 数组:简单但效率不高,因为插入和删除操作可能需要移动大量元素。
  • 链表:可以快速插入和删除,但查找特定元素可能较慢。
  • 二叉堆:最常用的实现方式,特别是二叉最小堆和二叉最大堆,它们提供了对数时间复杂度的插入和删除操作。
  • 平衡二叉搜索树:如AVL树或红黑树,提供了对数时间的插入、删除和查找操作。
  • 斐波那契堆:在某些操作(如删除最小元素)上非常高效,但插入和合并操作可能较慢。

优先级队列是一种非常有用的数据结构,它在需要根据元素的相对重要性进行操作的场景中非常有用。

但今天我们学习的是用二叉堆来实现优先级队列,主要了解其中的原理。


堆(Heap)

数据结构中的“堆”(Heap)是一种特殊的完全二叉树,它满足以下性质:

  1. 堆序性:在最大堆中,每个节点的值都不小于其子节点的值;在最小堆中,每个节点的值都不大于其子节点的值。
  2. 完全二叉树:除了最后一层外,每一层都被完全填满,并且最后一层的所有节点都尽可能地向左排列。

堆通常用于实现优先队列,它支持以下操作:

  • 插入(Insert):在堆的末尾添加一个新元素,然后通过上浮(Percolate Up)操作来恢复堆的性质。
  • 删除最大/最小元素(Extract Max/Min):移除最大(在最大堆中)或最小(在最小堆中)的元素,通常这个元素是堆的根节点。然后,将最后一个元素移动到根位置,并进行下沉(Percolate Down)操作来恢复堆的性质。
  • 查找最大/最小元素(Get Max/Min):在最大堆中返回根节点的值,在最小堆中也是返回根节点的值。
  • 堆排序(Heap Sort):通过构建一个最大堆,然后反复移除堆顶元素并重建堆,可以实现数组的原地排序。

堆可以用数组来实现,其中数组的索引与树中的位置有直接的对应关系。对于数组中的任意元素,其父节点和子节点的索引可以通过以下公式计算:

  • 父节点索引:(i-1)/2(其中i是当前节点的索引)
  • 左子节点索引:2*i + 1
  • 右子节点索引:2*i + 2

堆的实现通常需要对数组进行操作,以确保在进行插入和删除操作时,能够快速地维护堆的性质。堆是一种非常高效的数据结构,特别是在需要频繁插入和删除最大或最小元素的场景中。


堆的两种储存方式

我们上面说过堆的堆序性,以及堆其实是一颗完全二叉树。那么下面就展示了堆的两种储存方式。大根堆和小根堆 。我们先看下图。

为什么使用完全二叉树呢?因为其实堆的储存结构是一个线性数组,如果使用非完全二叉树则就会比较浪费空间。

小根堆:其根节永远小于他的两个左右孩子。

大根堆:其根节永远大于他的两个左右孩子。

了解完了堆的储存方式后,我们就该模拟实现一下,来加深一下对堆的理解。


堆的模拟实现

首先要模拟堆的实现我们就要了解一下堆的调整方式。

堆有向上调整和向下调整。

那么我们怎么了解这两个调整呢?

顾名思义,向下调整就是在给定节点处往下调整,使这个节点往下形成的子树成为一个堆(大根堆或小根堆)

堆的向下调整

向下调整从给点的节点,不断往下调整,我这里假设是调整为小根堆。那么调整的过程就是看两个孩子节点是否比父节点小,如果小就交换一下两个节点的值就可以了。这里调整结束的条件就是child大于树的节点的个数,所以while的条件为(child<=len)。

代码实现)(小根堆为例):

public static void shiftDown(int[] array, int parent,int len) {

    int child=2*parent+1;  //父节点的左孩子
    while(child<=len){

        if(child+1<=len&&array[child+1]<array[child]){
            child=child+1;

        }

        if(array[child]<array[parent]){
            swap(array,child,parent);
        }

        parent=child;
        child=2*child+1;

    }

}

private static void swap(int[] array, int child, int parent) {
    int tmp=array[child];
    array[child]=array[parent];
    array[parent]=tmp;

}

使用向下调整建成小根堆

使用向下调整创建小根堆的步骤主要就是,找最后一个节点的父节点(即:(a.length-1-1)/2)开始,不断往前,每一个节点都进行一个向下调整,调整完毕后,就可以创建成一个小根堆了。如果要创建大根堆只需要改一下向下调整的比较部分即可。

import java.util.Arrays;

public class T {

    public static void main(String[] args) {
        int[] a={273,34,67,22,11,66,8,3};
        for (int i = (a.length-1-1)/2; i >=0 ; i--) {
            shiftDown(a,i,a.length-1);
        }

        System.out.println(Arrays.toString(a));


    }

    public static void shiftDown(int[] array, int parent,int len) {

        int child=2*parent+1;  //父节点的左孩子
        while(child<=len){

            if(child+1<=len&&array[child+1]<array[child]){
                child=child+1;

            }

            if(array[child]<array[parent]){
                swap(array,child,parent);
            }

            parent=child;
            child=2*child+1;



        }



    }

    private static void swap(int[] array, int child, int parent) {
        int tmp=array[child];
        array[child]=array[parent];
        array[parent]=tmp;

    }


}

注意:堆的向下调整主要是运用在建堆,即:给定你一个数组,直接将这个数组创建成大根堆或小根堆。

想要一个一个插入的话,我们是要用到向上调整的。


堆的向上调整

堆的向上调整与向下调整相似,只是调整方向是不断往上的,所以在我们插入数据时(通常是尾插)那么我们就得使用向上调整了,因为这样我们只需要调整这个节点就好了。如果我们向下调整的话,还是需要将每个节点进行调整。

代码实现(小根堆为例):

private static void swap(int[] array, int child, int parent) {
    int tmp=array[child];
    array[child]=array[parent];
    array[parent]=tmp;

}


public static void shiftUp(int[] array, int child) {

    int parent=(child-1)/2;
    while(child>0){
        if(array[child]<array[parent]){
            swap(array,child,parent);
            child=parent;
            parent=(child-1)/2;

        }else {
            break;
        }


    }
}

使用向上调整一个一个插入创建小根堆

我们随机生成一个随机数,在加入数组,然后在进行调整即可。这样就是逐一插入实现小根堆,大根堆同理。

private static void swap(int[] array, int child, int parent) {
    int tmp=array[child];
    array[child]=array[parent];
    array[parent]=tmp;

}


public static void shiftUp(int[] array, int child) {

    int parent=(child-1)/2;
    while(child>0){
        if(array[child]<array[parent]){
            swap(array,child,parent);
            child=parent;
            parent=(child-1)/2;

        }else {
            break;
        }


    }
}

public static void main(String[] args) {
    int[] arr=new int[10];
    Random random=new Random();

    for (int i = 0; i < 10; i++) {
        int n= random.nextInt(100);
        arr[i]=n;
        shiftUp(arr,i);
    }

    System.out.println(Arrays.toString(arr));
}

检验一下结果,确实是一个小根堆。


向下调整和向上调整的总结

向下调整:适用于给定一个数组,要将数组中的元素建成堆的形式,这时我们用向下调整的话是比较合适的,相比与向上调整是比较快的从时间复杂度上看。

向上调整:适用于要将数据一个一个插入,使其每次插入完成后还是堆的形式,这时因为数据通常是插入在数组尾端然后在进行调整。所以只需要调用向上调整一下为节点就可以,而这是向下调整做不到的。


模拟实现优先级队列的方法接口

如下:简单实现了offer,poll,peek方法。

offer:将数据进行尾插后,直接对该节点进行向下调整。

poll:我们将头节点数据,用另一个变量进行储存后,与最后一个节点进行调换位置然后进行一次i向下调整。

peek:查看根节点的数据。

注意:我们这里吧数组当作一颗完全二叉树,即数组的下标相当于层序遍历的顺序对应的那个节点。


public void offer(int e) {
        array[size++] = e;
        shiftUp(array,size - 1);
    }

    public int poll() {
        int oldValue = array[0];
        array[0] = array[--size];
        shiftDown(array,0,size-1);
        return oldValue;
    }

    public int peek() {
        return array[0];
    }

以下是全部代码:

public class MyPriorityQueue {


    private int[] array = new int[100];
    private int size = 0;



    public  void shiftDown(int[] array, int parent, int len) {

        int child = 2 * parent + 1;  //父节点的左孩子
        while (child <= len) {

            if (child + 1 <= len && array[child + 1] < array[child]) {
                child = child + 1;

            }

            if (array[child] < array[parent]) {
                swap( child, parent);
            }

            parent = child;
            child = 2 * child + 1;


        }


    }

    private  void swap( int child, int parent) {
        int tmp = array[child];
        array[child] = array[parent];
        array[parent] = tmp;

    }


    public  void shiftUp(int[] array, int child) {

        int parent = (child - 1) / 2;
        while (child > 0) {
            if (array[child] < array[parent]) {
                swap(child, parent);
                child = parent;
                parent = (child - 1) / 2;

            } else {
                break;
            }


        }
    }





    public void offer(int e) {
        array[size++] = e;
        shiftUp(array,size - 1);
    }

    public int poll() {
        int oldValue = array[0];
        array[0] = array[--size];
        shiftDown(array,0,size-1);
        return oldValue;
    }

    public int peek() {
        return array[0];
    }

    public static void main(String[] args) {
        MyPriorityQueue queue=new MyPriorityQueue();
        queue.offer(1);
        queue.offer(4);
        queue.offer(33);
        queue.offer(9);
        queue.offer(14);
        queue.offer(6);


        for (int i = 0; i < queue.size; i++) {
            System.out.print(queue.array[i]+" ");

        }

        System.out.println();

        queue.poll();
        for (int i = 0; i < queue.size; i++) {
            System.out.print(queue.array[i]+" ");

        }
    }
    
    }

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

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

相关文章

如何在Java中实现用户列表的下载功能

在现代的Web应用中&#xff0c;用户管理是一个常见的需求。用户可能需要查看和下载他们的个人信息或者用户列表。本文将介绍如何使用Java和Spring框架实现用户列表的下载功能&#xff0c;具体采用Excel格式。 一、项目准备 首先&#xff0c;确保你的项目中已经引入了Spring B…

力扣 42.接雨水

文章目录 题目介绍解法 题目介绍 解法 法一&#xff1a;通过计算每个位置 i 能够捕获的雨水量&#xff0c;然后将他们相加。 具体做法是&#xff1a;创建两个数组&#xff1a;preMax 和 sufMax 分别用来存储每个位置左边和右边的最大高度&#xff0c;则每个位置 i 可以捕获的…

【巧用ddddocr破解算术运算验证码的经典示范】

计算型验证码 算术验证码&#xff0c;也叫计算型验证码, 计算型验证码其实是一种特殊的字符型验证码&#xff0c;只不过在它的基础上增加了数字运算。   计算型验证码在将人类视觉和计算机视觉的差异作为区分用户和电脑的依据的同时&#xff0c;还加上了逻辑运算&#xff0c…

数据结构_1.1、数据结构的基本概念

1、基本概念 数据&#xff1a;是信息的载体&#xff0c;是描述客观事物属性的数、字符及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。数据是计算机程序加工的原料。 数据元素&#xff1a;数据元素是数据的基本单位&#xff0c;通常作为一个整体进行考虑和处理…

Java高级Day48-JDBC-API和JDBC-Utils

127.JDBC API 128.JDBC-Utils public class JDBCUtils {//这是一个工具类&#xff0c;完成mysql的连接和关闭资源//顶柜相关的属性&#xff08;4个&#xff09;&#xff0c;因为只需要一份&#xff0c;因此做成staticprivate static String user;//用户名private static Stri…

【速成Redis】04 Redis 概念扫盲:事务、持久化、主从复制、哨兵模式

前言&#xff1a; 前三篇如下&#xff1a; 【速成Redis】01 Redis简介及windows上如何安装redis-CSDN博客 【速成Redis】02 Redis 五大基本数据类型常用命令-CSDN博客 【速成Redis】03 Redis 五大高级数据结构介绍及其常用命令 | 消息队列、地理空间、HyperLogLog、BitMap、…

python有main函数吗

python和C/Java不一样&#xff0c;没有主函数一说&#xff0c;也就是说python语句执行不是从所谓的主函数main开始的。 当运行单个python文件时&#xff0c;如运行a.py&#xff0c;这个时候a的一个属性__name__是__main__。 当调用某个python文件时&#xff0c;如b.py调用a.p…

基于微信小程序的童装商城的设计与实现+ssm(lw+演示+源码+运行)

童装商城小程序 摘 要 随着移动应用技术的发展&#xff0c;越来越多的用户借助于移动手机、电脑完成生活中的事务&#xff0c;许多的传统行业也更加重视与互联网的结合&#xff0c;由于城镇人口的增加&#xff0c;人们去商场购物总是排着长长的队伍&#xff0c;对于时间紧的人…

数据类型转换中存在的问题分析

本文档包含内容有&#xff1a; 数据类型转换中的隐式类型转换存在的风险&#xff1b; 整型提升存在的风险 标准算数转换存在的风险数据类型转换中存在的数据类型范围溢出风险&#xff1b;数据类型转换中存在的数据精度问题&#xff08;数据截断&#xff09;。 隐式类型转换&a…

此框架你到底了解多少???

1.简述对Spring中IOC/DI的理解 IOC&#xff1a;控制反转&#xff0c;将创建和管理的对象的任务交给外部的Spring容器 DI&#xff1a;依赖注入&#xff0c;对象之间存在依赖关系&#xff0c;创建对象时&#xff0c;对其依赖的对应直接进行赋值 2.有哪些依赖注入的方式 基于注…

【计算机网络】详解UDP套接字网络字节序IP地址端口号

一、网络字节序 我们已经知道, 内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出; 接收主机把从网络上接到…

软考中级软设背诵内容

冯诺依曼结构、哈佛结构 冯诺依曼结构: 程序指令和数据都采用二进制表示 程序指令和数据在同一个存储器中混合 程序的功能都由中央处理器&#xff08;CPU&#xff09;执行指令来实现 程序的执行工作由指令进行自动控制 SRAM、DRAM 与DRAM相比&#xff0c;SRAM集成率低、功…

页面布局实现-左侧横向滑动展示隐藏数据,右侧固定展示操作按钮。可上下滑动联动

效果图 1.布局排版 <LinearLayoutandroid:layout_width"match_parent"android:layout_height"match_parent"android:orientation"vertical"android:padding"1dp"><LinearLayoutandroid:id"id/lltList"android:lay…

Java:Clonable 接口和拷贝

一 Clonable 接口 在 Java SE 中&#xff0c;Cloneable 是一个标记接口&#xff08;Marker Interface&#xff09;&#xff0c;它位于 java.lang 包中。这个接口的主要目的是标识实现该接口的类能够被合法地克隆&#xff08;即可以调用 Object 类中的 clone() 方法&#xff09…

Electron应用程序打包后运行报错cannot find module ‘@vue/cli-service‘

本项目打包运行后报错问题的解决办法&#xff0c;类似于其他cannot find module XXX’的报错&#xff0c;也基本可以解决 文章目录 electron应用程序打包后运行报错排查问题解决办法 electron应用程序打包后运行报错 错误如下&#xff1a; 提示找不到该模块 排查问题 本项…

互联网广告产品基础知识

一 计价与效果 广告产品如何估算收入&#xff1f; 一种是从需求侧计算&#xff1a;按照广告主数量进行拟合&#xff1b;一种是从供给侧计算&#xff1a;按照曝光量和千次曝光单价进行拟合。 需求侧 从需求侧&#xff0c;也就是广告主侧&#xff0c;来计算广告产品的总收入&…

Linux命令:用于创建新的用户组的命令行工具groupadd 详解

目录 一、概述 二、组标识符GID 1、定义 &#xff08;1&#xff09;标识符 &#xff08;2&#xff09;与UID的关系 2、GID的作用 &#xff08;1&#xff09;用户组管理 &#xff08;2&#xff09;文件权限控制 &#xff08;3&#xff09;用户权限管理 &#xff08;4&…

threejs性能优化之gltf文件压缩threejs性能优化之glb文件压缩

在使用Three.js进行3D图形开发时&#xff0c;GLTF&#xff08;GL Transmission Format&#xff09;文件因其高效性和灵活性而广受欢迎。然而&#xff0c;随着模型复杂度的增加&#xff0c;GLTF文件的大小也会显著增加&#xff0c;这可能会对加载时间和渲染性能产生负面影响。为…

插入与冒泡排序(C++)

\一、插入排序 1 简介 插入排序&#xff0c;也称为直接插入排序&#xff0c;其排序思想和我们平时打扑克牌时排序类似。 2 算法步骤 将第一个元素看作已排序序列&#xff0c;第二个到最后一个看作未排序序列。 第二个元素&#xff0c;与之前已排序号的序列进行对比&#x…

【我的 PWN 学习手札】tcache stash with fastbin double free —— tcache key 绕过

参考看雪课程&#xff1a;PWN 探索篇 前言 tcache key 的引入使得 tcache dup 利用出现了困难。除了简单利用 UAF 覆写 key 或者House Of Karui 之外&#xff0c;还可以利用 ptmalloc 中的其他机制进行绕过。 一、Tcache Stash with Fastbin Double Free 之前是 double free …