堆 和 优先级队列(超详细讲解,就怕你学不会)

news2025/1/19 14:35:24

优先级队列

  • 一、堆的概念特性
  • 二、堆的创建
    • 1、向下调整算法
    • 2、向下调整建堆
    • 3、向下调整建堆的时间复杂度
  • 三、堆的插入
    • 1、向上调整算法实现插入
    • 2、插入创建堆的时间复杂度
  • 三、堆的删除
  • 四、Java集合中的优先级队列
    • 1、PriorityQueue 接口概述及模拟实现
    • 2、如何创建大根堆?


一、堆的概念特性

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆(大根堆);每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆(小根堆)。

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


将元素存储到数组中后,可以根据二叉树的性质对树进行还原:

假设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,否则没有右孩子

二、堆的创建

1、向下调整算法

示例:将集合{75,20,70,30,50,90,80,60,40}调整为小根堆

通过分析,已知集合的 “左右子树均满足小堆的性质” ,我们只需要将根节点向下调整到合适的位置,使集合整体为小堆即可。具体来说,我们可以细化为如下调整方法:

  1. 每次调整以待调整结点、它的左右孩子结点构成的子树为单元进行“向下调整”
  2. 每个单元以待调整结点为根节点,将左右孩子结点的最小值(小堆)和根节点进行比较,如果根节点<min(左孩子,右孩子)调整结束,否则,待调整根节点和[min(左孩子,右孩子)]交换,交换完成后,继续重复这个步骤直到待调整结点为当前子树单元中的最小值,或待调整结点不在存在孩子结点,调整结束。

按照以上思路,下面是详细的代码实现:

	/**
     * 小根堆->向下调整算法
     * @param parent 待调整结点
     * @param len 数组长度
     */
    private void shiftDown(int[] array,int parent, int len) {
        int child = 2 * parent + 1;//根据完全二叉树性质,找到左孩子
        while (child < len) {
        	//child<len判断孩子合法性
            //满足child<len至少存在左孩子
            if (child + 1 < len && array[child] > array[child + 1]) {
            	//存在右孩子,且右孩子为左右孩子最小值
                child++;
            }
            //此时child一定是左右孩子的最小值的下标
            if (elem[child] < elem[parent]) {
            	//满足条件,就交换
                int tmp = array[parent];
                array[parent] = array[child];
                array[child] = tmp;
				//交换完成后,继续以新的位置向下调整
                parent = child;
                child = 2 * parent + 1;
            } else {
            	//array[parent] < array[child]调整结束
                break;
            }
        }
    }

小结:

  1. 在调整以parent为根的二叉树时,必须要满足 parent 的左子树和右子树已经是堆了才可以向下调整。
  2. 向下调整算法的最坏情况为从根一直比较到叶子,比较的次数为二叉树的高度,时间复杂度为O(log₂N)

2、向下调整建堆

在上面的探讨中,我们知道可以使用向下调整算法,将左右子树为堆的完全二叉树序列调整为堆,那么如果给出任意的完全二叉树序列(左右子树不满足堆的特性),我们又该如何调整为堆呢?

思路: 我们已知使用向下调整算法,parent的左右子树必须满足堆的特性,对于任意普通完全二叉树序列,显然不能直接使用向下算法进行调整。不过我们知道一颗完全二叉树是由一颗颗左右子树构成的,虽然一颗普通的完全二叉树不能直接使用向下调整算法,但是倒数第一个非叶子结点构成的子树一定可以使用向下调整算法,所以如果我们可以先将下面的子树调整为堆,在继续对子树的根结点进行调整,这样根节点的左右子树就满足了堆的特性,可以直接使用向下调整算法。就这样一直向上对根结点进行向下调整,直到0下标对应的根节点调整完毕,整颗完全二叉树序列就满足了堆的特性了。

例如以序列{50,70,40,90,20,10,80,30,60}为例,调整后为{10,20,40,30,70,50,80,90,60}

具体实现:

    /**
     * 向下调整建堆
     * @param array
     */
    public void creatHeap(int[] array) {
    	//清晰了思路之后,建堆就非常简单了
    	//只需从最后一个非叶子结点开始,直到找到下标为0的根节点
    	//每遇到一个结点向下调整,调用shiftDown即可
        for (int parent = (array.length-1-1)/2; parent >= 0 ; parent--) {
            shiftDown(array,parent,array.length);   	 	
        }
    }

3、向下调整建堆的时间复杂度

假设序列为满叉树,假设树高为h,则最坏情况下第K层的2^(k-1)个结要向下移动h-k层。

三、堆的插入

1、向上调整算法实现插入

堆的插入相对来说较为简单,主要分为以下两步:

  1. 每次将新节点插入到堆的最后一个节点,因为底层维护的是一个一维数组,空间不够时要扩容。
  2. 将新插入的节点 向上调整,直到满足堆的性质。


所以说堆的插入并不难理解,核心就是对向上调整算法的实现:

  1. 每次调整以新节点构成的子树为单元,进行“向上调整”。
  2. 由于是在堆的基础上进行插入,所以每次调整只需将新节点和根节点进行比较,如果 根节点<新节点(小堆),调整结束,否则,根节点和新节点交换,交换完成后,继续重复这个步骤直到根节点<新节点,或,调整完下标为0的根节点,调整结束。

向上调整代码实现:

    /**
     * 向上调整算法->小根堆
     * @param child 插入下标
     * elem 底层维护的一维数组
     */
    private void shiftUp(int child) {
        int parent = (child-1)/2;根据完全二叉树性质,找双亲
        while (child >0) {
        	//child>0判断孩子节点的合法性
            if (elem[child]<elem[parent]) {
            	//满足条件,交换
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;
				//交换完成后,继续以新的位置向上调整
                child = parent;
                parent = (child-1)/2;
            } else {
            	//elem[child]>elem[parent],调整结束
                break;
            }
        }
    }

堆的插入代码实现:

    /**
     * 插入元素,也可以向上调整建堆
     * @param val
     * @return
     * elem 内部维护数组
     * usedSize 有效长度
     */
    public boolean offerHeap(int val) {
        if (isFull()) {
            //扩容
            elem = Arrays.copyOf(elem,elem.length*2);
        }
        elem[usedSize++]=val;
        shiftUp(usedSize-1);
        return true;
    }

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

小结:

  1. 堆的插入是在堆的基础上进行的插入。
  2. 向上调整算法的最坏情况为从叶子节点一直比较到根节点,比较的次数为二叉树的高度,时间复杂度为O(log₂N)

2、插入创建堆的时间复杂度

最坏情况下,假设堆为一颗满二叉树,的高度为h

三、堆的删除

规定:堆的删除一定删除的是堆顶元素

思路:由于堆的底层维护的是一个一维数组,所以每次删除,我们先将堆顶元素和堆的最后一个元素交换,然后让一维数组的size --,最后将交换后的堆顶元素 向下调整 即可。

  1. 判断堆是否为空,空堆不能删除
  2. 堆顶元素与堆尾元素交换
  3. 内部维护数组的有效数据减少 1
  4. 新的堆顶元素向下调整


代码实现:

    /**
     * 删除堆顶元素
     */
    public void pollHeap() {
        //判空
        if (isEmpty()) {
            return;
        }
        //交换
        int tmp = elem[usedSize-1];
        elem[usedSize-1] = elem[0];
        elem[0] = tmp;
        //删除+向下调整
        shiftDown(0,--usedSize);
    }
    public boolean isEmpty() {
        return usedSize == 0;
    }

四、Java集合中的优先级队列

1、PriorityQueue 接口概述及模拟实现

上面我们花那么多时间介绍堆,就是在为Java集合框架中的PriorityQueue做铺垫:

PriorityQueue,即优先级队列。优先级队列可以保证每次取出来的元素都是队列中的最小或最大的元素(Java优先级队列默认每次取出来的为最小元素)。JDK1.8中的PriorityQueue底层使用了堆这种数据结构。

PriorityQueue 注意事项:

  1. Java集合框架中提供了PriorityQueuePriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的。
  2. 使用时必须导入PriorityQueue所在的包import java.util.PriorityQueue;
  3. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常
  4. 因为 null 无法进行比较和排序,因此不能插入null对象,否则会抛出NullPointerException
  5. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
  6. 插入和删除元素的时间复杂度为log₂N
  7. PriorityQueue底层使用了堆数据结构,默认情况下是小堆,如果创建大堆,需要在构造方法中传入比较器。

常用的构造方法

构造器功能介绍
PriorityQueue()创建一个空的优先级队列,默认容量是11
PriorityQueue(intinitialCapacity)创建一个初始容量为initialCapacity的优先级队列,注意:initialCapacity不能小于1,否则会抛IllegalArgumentException异常
PriorityQueue(Comparator c)传入比较器,构造大堆

常用的接口

函数名功能介绍
boolean offer(E e)插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,空间不够时候会进行扩容
E peek()获取优先级最高的元素,如果优先级队列为空,返回null
E poll()移除优先级最高的元素并返回,如果优先级队列为空,返回null
int size()获取有效元素的个数
void clear()清空
boolean isEmpty()检测优先级队列是否为空,空返回true

模拟实现PriorityQueue
由于 PriorityQueue 底层使用的是 这种数据结构,所以PriorityQueue中的这些接口函数可以参考上面堆的操作,下面给出完整代码,大家自行理解:

public class PriorityQueue {
    public int[] elem;//数组
    public int usedSize;//有序长度

    public PriorityQueue(){
        elem = new int[11];
    }


	//1.判满
    public boolean isFull() {
        return usedSize==elem.length;
    }
	
	//2.判空
	public boolean isEmpty() {
        return usedSize == 0;
    }
	
	//3.插入元素
    public boolean offerHeap(int val) {
        if (isFull()) {
            //扩容
            elem = Arrays.copyOf(elem,elem.length*2);
        }
        elem[usedSize++]=val;
        shiftUp(usedSize-1);
        return true;
    }
    
    /**
     * 向上调整算法(小根堆)
     * @param child
     * elem 为底层维护的一维数组
     */
    private void shiftUp(int child) {
        int parent = (child-1)/2;
        while (child >0) {
            if (elem[child]<elem[parent]) {
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;

                child = parent;
                parent = (child-1)/2;
            } else {
                break;
            }
        }
    }

    //4.删除堆顶元素
    public void pollHeap() {
        //判空
        if (isEmpty()) {
            return;
        }
        //交换
        int tmp = elem[usedSize-1];
        elem[usedSize-1] = elem[0];
        elem[0] = tmp;
        //删除+向下调整
        shiftDown(0,--usedSize);
    }
	
	 /**
     * 向下调整算法(小根堆)
     * @param parent 待调整结点
     * @param len 数组长度
     */
    private void shiftDown(int parent, int len) {
        //有左孩子
        int child = 2 * parent + 1;//这里是+1!!!
        while (child < len) {
            //有右孩子
            if (child + 1 < len && elem[child] > elem[child + 1]) {
                child++;
            }
            //此时childe一定是左右孩子的最小值的下标
            if (elem[child] < elem[parent]) {
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;

                parent = child;
                child = 2 * parent + 1;
            } else {
                break;
            }
        }
    }

	//5.清空
	public boolean isEmpty() {
        return usedSize == 0;
    }
}

2、如何创建大根堆?

我们已知默认情况下,PriorityQueue 是 小堆,如果创建大堆需要用户提供比较器,关于比较器我在 简介Object类+接口实例(深浅拷贝、对象数组排序)章节中已有过相关介绍,大家可点击连接自行参考,这里就不做过多冗余介绍了。

下面我演示一下创建大根堆常用的 3 3 3 中方式(以下这 3 种创建方式,本质上是没有区别的):

方式1: 直接创建比较类,实现 C o m p a r a t o r Comparator Comparator 接口,在构造方法中传入,比较类对象。

class Integercmp implements Comparator<Integer>{
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1);
    }
}
public class Demo {
	 public static void main1(String[] args) {
        Integercmp cmp = new Integercmp();
        Queue<Integer> maxHeap = new PriorityQueue<>(cmp);
    }
}

方式2: 使用匿名内部类

public class Demo {
	public static void main2(String[] args) {
        Queue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
    }
}

方式3: 使用 L a m b d a Lambda Lambda 表达式

public class Demo {
    public static void main3(String[] args) {
        Queue<Integer> maxHeap = new PriorityQueue<>((o1,o2)->{
            return o2.compareTo(o1);
        });
    }
}

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

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

相关文章

基于YOLOv8模型的五类动物目标检测系统(PyTorch+Pyside6+YOLOv8模型)

摘要&#xff1a;基于YOLOv8模型的五类动物目标检测系统可用于日常生活中检测与定位动物目标&#xff08;狼、鹿、猪、兔和浣熊&#xff09;&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的目标检测&#xff0c;另外本系统还支持图片、视频等格式的结果可视化与…

【Java 动态数据统计图】动态数据统计思路案例(动态,排序,数组)四(116)

需求&#xff1a;&#xff1a;前端根据后端的返回数据&#xff1a;画统计图&#xff1b; 1.动态获取地域数据以及数据中的平均值&#xff0c;按照平均值降序排序&#xff1b; 说明&#xff1a; X轴是动态的&#xff0c;有对应区域数据则展示&#xff1b; X轴 区域数据降序排序…

加速出海丨美格智能SLM320模组再获德国电信认证

近日&#xff0c;美格智能Cat.1模组SLM320再获国际顶尖运营商德国电信认证&#xff0c;可以支持客户在德国电信网络所覆盖的德国、荷兰、波兰、奥地利、捷克、斯洛伐克、匈牙利等欧洲多国市场部署物联网应用&#xff0c;是美格智能走向全球市场的又一重要突破。 日前&#xff0…

通达信指标:显示全部的DRAWICON函数图标

**指标使用说明&#xff1a;**指标名称&#xff08;DRAWICON图标&#xff0c;幅图显示&#xff09;&#xff0c;需要查看图标的时候&#xff0c;要选择上市天数>92天的股票&#xff0c;才能正常的显示全部的图标&#xff0c;否则是无法显示的&#xff0c;以下的写法也是指标…

客服如何减轻工作压力?浅析客服压力管理方法

在现代商业领域中&#xff0c;客服是一项非常重要的工作&#xff0c;负责根据客户需求提供解决方案。客服工作不仅需要一定的专业知识和技能&#xff0c;还需要面对各种复杂、多变的情况&#xff0c;并拥有强大的应对压力的能力。客服从业人员的工作压力往往非常大&#xff0c;…

C++初阶语法——内部类

前言&#xff1a;内部类&#xff0c;顾名思义是定义在类中的类&#xff0c;许多人会以为它属于外部的类&#xff0c;实际上并不是&#xff0c;它们是两个独立的类&#xff0c;但是内部类受外部类类域的限制。 目录 一.概念二.特性1.内部类和外部类相互独立2.内部类是外部类的友…

极客时间-茹炳晟《软件测试52讲》-学习笔记-

测试基础知识篇&#xff08;11讲&#xff09; 01 你真的懂测试吗&#xff1f;从“用户登录”测试谈起 测试用例设计框架 基于功能性需求和非功能性需求思考&#xff1a; 功能性需求使用等价类划分、边界值分析、错误推断法设计用例 非功能性需求考虑安全&#xff08;信息的保存…

《Java极简设计模式》第04章:建造者模式(Builder)

作者&#xff1a;冰河 星球&#xff1a;http://m6z.cn/6aeFbs 博客&#xff1a;https://binghe.gitcode.host 文章汇总&#xff1a;https://binghe.gitcode.host/md/all/all.html 源码地址&#xff1a;https://github.com/binghe001/java-simple-design-patterns/tree/master/j…

星际争霸之小霸王之小蜜蜂(三)--重构模块

目录 前言 一、为什么要重构模块 二、创建game_functions 三、创建update_screen() 四、修改alien_invasion模块 五、课后思考 总结 前言 前两天我们已经成功创建了窗口&#xff0c;并将小蜜蜂放在窗口的最下方中间位置&#xff0c;本来以为今天将学习控制小蜜蜂&#xff0c;结…

基于IMX6ULLmini的Linux裸机开发系列三:按键检测输入

目录 开启GPIO5对应的时钟 设置引脚复用 设置GPIO5_IO1输入模式 设置检测电平 部分代码 button.c led.c main.c 在原理图上找到对应的引脚后即可以根据对应的图表找到真正在板字上的引脚&#xff0c;这里的 SNVS_TAMPER1对应实际的引脚是GPIO5_IO1 P1357页附近有GPIO5对…

华为OD机试 - 数字字符串组合倒序 - 正则表达式(Java 2023 B卷 100分)

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路1、熟读题意&#xff0c;大概理解为&#xff1a;2、我理解 “-”作为连接符使用时作为字符串的一部分 的意思是&#xff1a;3、解决本题的关键是正则表达式的使用。 五、Java算法源码六、效果展示1、输入2、输出…

142页大型制造企业IT蓝图、信息化系统技术架构规划与实施方案PPT

导读&#xff1a;原文《142页大型制造企业IT蓝图、信息化系统技术架构规划与实施方案PPT》&#xff08;获取来源见文尾&#xff09;&#xff0c;本文精选其中精华及架构部分&#xff0c;逻辑清晰、内容完整&#xff0c;为快速形成售前方案提供参考。 完整版领取方式 完整版领取…

新能源电动车充电桩控制主板的三大保护功能

新能源电动车充电桩控制主板的三大保护功能 你是否曾经遇到过电动车充电时电流过大&#xff0c;电压不稳定&#xff0c;或者电池过热的情况?这些问题都可能会给你的电动车带来安全隐患。那么&#xff0c;如何避免这些问题的发生呢?让我们一起来探讨一下充电桩控制主板的保护功…

这款编程语言,竟然没有程序员使用,却人丁兴旺

人丁兴旺 现在&#xff0c;编程语言种类繁多&#xff0c;比如Java、Python、C等&#xff0c;每种语言都有其特定的应用场景和优势。 然而&#xff0c;让我们感到惊奇的是&#xff0c;有一款编程语言&#xff0c;它并没有广大的程序员使用&#xff0c;却依然能够在编程世界中独…

初阶C语言-结构体

&#x1f31e; “少年有梦不至于心动&#xff0c;更要付诸行动。” 今天我们一起学习一下结构体的相关内容&#xff01; 结构体 &#x1f388;1.结构体的声明1.1结构的基础知识1.2结构的声明1.3结构成员的类型1.4结构体变量的定义和初始化 &#x1f388;2.结构体成员的访问2.1结…

04-微信小程序常用组件-基础组件

04-微信小程序常用组件-基础组件 文章目录 基础内容icon 图标案例代码 text 文本案例代码 progress 进度条案例代码 微信小程序包含了六大组件&#xff1a; 视图容器、 基础内容、 导航、 表单、 互动和 导航。这些组件可以通过WXML和WXSS进行布局和样式设置&#xff0c…

我还不知道?Android组件化插件化模块化

Android组件化、插件化和模块化是针对Android应用程序开发的一种架构设计思想和开发方式。 组件化&#xff08;Componentization&#xff09;&#xff1a; 组件化是将一个大型的Android应用程序拆分成多个独立的组件&#xff08;Module&#xff09;&#xff0c;每个组件可以独…

SUMO 创建带有停车位的充电站 在停车位上充电

前言 SUMO提供的Charging Station是没有停车位的&#xff0c;车辆只有在通过充电站区域或者停在充电站区域内时才能被充电&#xff0c;这时充电的车辆就会占用道路。然而&#xff0c;真实世界中的情况通常是充电站设在路边&#xff0c;且提供一定量的车位用于停车&#xff0c;…

探索OLED透明屏的多领域应用:技术驱动的未来之窗

OLED透明屏作为一项具有突破性技术的新兴产品&#xff0c;正在各个领域展现无限的可能性。 其独特的透明性和高对比度&#xff0c;使其在智能家居、汽车行业、商业展示、航空航天、教育与培训以及医疗健康等领域中得到广泛应用。 那么&#xff0c;尼伽将详细探讨OLED透明屏在…

程序的DAC检查与LSM简介

程序的DAC检查 在Linux中&#xff0c;程序的DAC&#xff08;Discretionary Access Control&#xff0c;自主访问控制&#xff09;检查是指操作系统对程序执行期间对文件和资源的访问权限进行的检查。 Linux使用一种基于权限的访问控制模型&#xff0c;其中每个文件和资源都与…