数据结构<堆>

news2024/9/20 18:27:28

🎇🎇🎇作者:
@小鱼不会骑车
🎆🎆🎆专栏:
《数据结构》
🎓🎓🎓个人简介:
一名专科大一在读的小比特,努力学习编程是我唯一的出路😎😎😎
在这里插入图片描述

优先级队列(堆)

  • 1. 优先级队列
    • 1.1 概念
  • 2. 优先级队列的模拟实现
    • 2.1 堆的概念
    • 2.2 堆的存储方式
    • 2.3 堆的创建
      • 2.3.1 堆向下调整
      • 2.3.2 建堆的时间复杂度
      • 2.3.3 堆的向上调整
        • 插入举例:
        • 建堆举例:
      • 2.3.4 堆的删除

1. 优先级队列

1.1 概念

小鱼在之前的一篇博客中详细的介绍过队列,队列是一种先进先出(FIFO)的数据结构,但是有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,例如我们在追剧时,如果是电池没电了,手机会优先提醒我们该充电了,而不是在等到你退出追剧的时候才弹出来,这时候就体现了优先级的重要性,包括打游戏时突然来电话,万一是很重要的电话,没有第一时间接到,是不是就可能产生很严重的后果!

在这种情况下,数据结构应该提高两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象.这种数据结构就是优先级队列(Priority Queue).

2. 优先级队列的模拟实现

JDK1.8中的PriorityQueue底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整.

2.1 堆的概念

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

如下图:

在这里插入图片描述
在这里插入图片描述

2.2 堆的存储方式

从堆的概念可知,堆是一颗完全二叉树,因此可以按照层序遍历的规则采用顺序的方式来高效存储.

对于非完全二叉树实现的堆,如下图,该二叉树会造成空间上的浪费.

在这里插入图片描述
因此:对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低

完全二叉树实现的堆如下图:

在这里插入图片描述

2.3 堆的创建

2.3.1 堆向下调整

对于集合{ 23,25,64,35,18,29,33,14,16,27 }中的数据,如果将其创建成堆呢?

我们先把它的逻辑结构画出来!

在这里插入图片描述

我们需要将该结构调整为大根堆.

大家先看过程,一边观看过程,一边理解思路.

步骤一: 找到最后一颗含有孩子节点的树

在这里插入图片描述

步骤二:

  • 判断左右孩子结点是否存在
  • 找出左右孩子节点的最大值(大根堆)
  • 比较该子树的根节点也就是下标4对应的结点,判断下标为九的结点是否大于根节点
  • 如果大于根节点,那就交换,如果小于则不进行交换

步骤三:由于大于子树的根节点,那么进行交换

在这里插入图片描述

步骤四:下标为4的这个结点对应的子树已经是大根堆了,下一步对下标为3的这个结点,将该树改为大根堆

在这里插入图片描述

步骤五:重复步骤二,将下标为2的结点对应的子树调整为大根堆

在这里插入图片描述

步骤六:对下标为1的结点所对应的树进行调整,依旧是重复上述操作

在这里插入图片描述

步骤七:此时该子树并没有调整完,需要注意!!!由于下标为1的结点和下标为3的结点进行了交换,那么还需要进行一步操作就是判断,被交换值的下标为3的结点是否是下标为3的节点所对应的这颗树中的最大值!

在这里插入图片描述

步骤八:现在对下标为0的结点进行调整(下标为2和下标为0的结点进行了交换),红色框对应的是该结点所对应的树,绿色的框是正在进行比较的树

在这里插入图片描述

步骤九:由于前面进行了交换,将下标为2的值进行了修改,此时并不能确认下标为2所对应的树是否还是大根堆,所以需要重复步骤二的操作,也就是找出左右孩子结点的最大值进行比较.

在这里插入图片描述

此时,该完全二叉树也就变成了一个大根堆,任何一颗子树的根节点都大于它的任何一个孩子节点.

如下图(每一个框框所对应的树的根节点都大于它的孩子结点):

在这里插入图片描述
上述的操作我们称之为向下调整.

此时我们再去观看下面的过程是不是更容易理解.

向下过程(以大堆为例):

  1. 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)
  2. 如果parent的左孩子存在,即:child < size, 进行以下操作,直到parent的左孩子不存在:
  • parent右孩子是否存在,存在找到左右孩子中最大的孩子,让child指向左右孩子最大值所对应的下标.
  • 将parent与较大的孩子child比较,如果:
  • parent大于较小的孩子child,调整结束
  • 否则:交换parent与较大的孩子child,交换完成之后,parent中小的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child;child=parent*2+1; 然后继续2的操作。

难点1:其实对于这道题的难点,就是找向下调整的边界,什么时候停止调整呢?
难点2:为什么要从最后一颗含有子节点的树开始向下调整.

讲解难点一:首先我们要知道,如果想要进行向下调整那么需要最基本的条件就是它有孩子结点,也就是至少存在左孩子,如果左孩子都不存在,那么该节点就不需要进行调整,此时问题就变成了,如何判断左孩子是否存在!

我们看下图:

在这里插入图片描述

下标为4的结点是有孩子结点的,孩子结点的下标是9,在二叉树中我们学过,给父亲节点下标,求左右孩子结点的下标,根据公式(求左孩子下标)child = parent * 2 +1.那么它是否有右孩子结点呢?我们看图片可以很清楚的看到他是没有右孩子结点的,我们假设它有右孩子结点,我们求出右孩子结点的下标:4*2+2 = 10,此时右孩子结点的下标为10,但是我们回归该图,该图只有十个结点,又因为下标是从0开始的,那么下标值最高为9,此时右孩子下标是10,说明以及越界了,并不包含右孩子,此时我们就可以找到这个边界,也就是child < 二叉树结点的个数,为了证明该判断的合理性我们试求下标5的孩子节点是否存在,答案是不存在( 5 * 2 + 1 = 11 11 > 二叉树结点的个数,不符合条件),下标6也如此,都是不存在孩子结点的.

讲解难点2:为什么要从最后一个含有子节点的树进行向下调整?

我们先举个反例:

在这里插入图片描述

若我们只对0下标对应的结点进行向下调整,大家看上图,此时左右子树都小于根节点,所以不再进行向下调整,但是可以看到,绿色框框里面的树还并不是大根堆,所以从0开始调整是有可能会出错的!

由于我们是从最后一颗子树进行调整的,那么假如我们调整到了下标为1的结点所对应的子树,我们可以保证,比下标1大的下标(例如2,3,4…)所对应的结点的子树一定是大根堆, 而且我们还可以得到一个信息就是:

在这里插入图片描述

代码如下:

   public void createHeap(){
   //从最后一个结点的父亲结点开始向下调整
        for (int parent = (UsedSize-1-1)/2; parent >=0 ; parent--) {
         	//UsedSize就是结束条件,也就是节点个数
            shiftDown(parent,UsedSize);
        }
    }
    /**
     * 父亲下标和每棵树的结束下标
     * @param parent
     */
    //大根堆
    private void shiftDown(int parent,int len) {
    //接受了父亲节点
    //求出子节点下标
        int child=2*parent+1;
        //判断孩子节点下标是否越界
        while (child<len) {
        //判断右节点是否存在,如果存在找出左右节点最大值(顺序不能颠倒)
          if(child+1<len && elem[child]<elem[child+1]) {
              child++;
          }
          //子节点大于父亲节点就交换
          if(elem[child]>elem[parent]) {
              swap(elem,parent,child);
              //此时的父亲节点指向被修改后的子节点
              parent=child;
              //子节点依旧是该父亲结点所对应的左孩子结点
              child=2*parent+1;
          }else {
          //如果没有交换就直接退出
              break;
          }
        }
    }
    private void swap(int[]array,int left,int right) {
        int tmp=array[left];
        array[left]=array[right];
        array[right]=tmp;
    }

我们来算一下向下调整的时间复杂度,大家看下图:

在这里插入图片描述

时间复杂度分析
最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为O(logN)

2.3.2 建堆的时间复杂度

我们按照最坏情况考虑,也就是每个结点都需要进行调整,并且每个结点调整的高度都是它所在层数到叶子层数的距离.

在这里插入图片描述
则需要移动总的步数为:

T(n) = 2 ^ 0 * ( h - 1 ) + 2 ^ 1*(h - 2) + 2 ^ 2 * ( h - 3) + 2 ^ 3 ( h - 4 ) +…+ 2 ^ ( h - 3 ) * 2 + 2 ^ ( h - 2 ) * 1 ①
2 * T(n) = 2 ^ 1 * ( h - 1 ) + 2 ^ 2
(h - 2) + 2 ^ 3 * ( h - 3) + 2 ^ 4 ( h - 4 ) *+…+ 2 ^ ( h - 2 ) * 2 + 2 ^ ( h - 1 ) * 1 ②
② - ① 错位相减:

T(n) = 1 - h + 2 ^ 1 + 2 ^ 2 + 2 ^ 3 + 2 ^ 4 +…+2(h-2)+2^(h-1)
T(n) = 2 ^ 0 + 2 ^ 1 + 2 ^ 2 + 2 ^ 3 + 2 ^ 4 +…+2(h-2)+2^(h-1) - h
T(n) = 2 ^ h - 1 - h
n = 2 ^ h -1
h = log(n+1)
T(n) = n - log(n+1) ≈ n

因此:建堆的时间复杂度为O(N)

2.3.3 堆的向上调整

刚才讲完了向下调整,那么这次我们讲解一下什么是向上调整.

向上调整一般使用的范围是在建堆的时候,还有插入结点的时候.

插入举例:

向上调整需要一个前提,就是当前的堆需要是大根堆(小根堆),我们以大根堆举例:

在这里插入图片描述

假如99这个结点是新插入的,需要将该树的结构重新改为大根堆,那么需要进行向上调整,具体过程就是:

99和它的父亲节点,也就是下标为4的结点进行比较,如果比父亲节点大,那就交换,此时就跟上述图片的第二步一样,再去用99的结点和它当前的父亲节点进行比较(也就是下标为1的结点),如果比父亲结点大,那就交换,重复上述操作.

问题:不需要左右孩子结点比较吗?

答案:不需要,因为在插入之前,该树就是一个大根堆.

我们用红色框框里面的内容举例:在没有插入99这个结点时,下标为1的结点比下标为3,4结点的值都要大,所以我们可以知道,即使我们的99和下标为4的元素交换,由于下标为3的结点并没有改变,所以我们下标为1的结点还是比下标为3的结点大的,所以就不需要用新的下标为4的结点和下标为3的结点进行比较了.只需要和它的父亲节点进行比较就行.

代码入下:

    //插入、
    public void offer(int val) {
    //判满
        if(isFUll()) {
          elem= Arrays.copyOf(elem,elem.length*2);
        }
        //末尾添加新元素然后UsedSize++
        //UsedSize是记录数组长度的
        elem[UsedSize++]=val;
        //进行向上调整,传入最后一个叶子结点
        shiftUp(UsedSize-1);
    }
    //向上调整
    public void shiftUp(int child) {
    //找到该结点的父亲结点
        int parent = (child-1)/2;
        //边界就是孩子结点不能为0,如果孩子结点为0说明不需要调整了
        while (child != 0) {
            
            //不需要左右孩子结点进行比较,因为该孩子节点的另一个兄弟结点一定小于父亲节点
            //子节点大于父亲节点就交换
            if(elem[child] > elem[parent]) {
                swap(elem,child,parent);
            }else {
            //如果不大于那就直接退出
                break;
            }
            //不断地进行向上调整,
            //可以对照上述画的图片进行理解
            child = parent;
            parent = (parent-1)/2;
        }
    }

时间复杂度:最坏的情况即图示的情况,从叶子一路比较到根,比较的次数为完全二叉树的高度,即时间复杂度为O(logN)

建堆举例:

大家看图:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过上述的过程,我们就通过向上调整建好了一个大根堆.

代码入下:

  //建堆
   public void createHeap(int[] array){
        for (int i = 0; i < array.length ; i++) {
            offer(array[i]);
        }
    }
    //插入、
    public void offer(int val) {
    //判满
        if(isFUll()) {
          elem= Arrays.copyOf(elem,elem.length*2);
        }
        //尾插要新增的结点
        elem[UsedSize++]=val;
        //将新插入的结点进行向上调整
        shiftUp(UsedSize-1);
    }
    //向上调整
    public void shiftUp(int child) {
        int parent = (child-1)/2;
        while (child != 0) {
            //子节点大于父亲节点
            if(elem[child] > elem[parent]) {
                swap(elem,child,parent);
            }else {
                break;
            }
            //子节点 = 父节点
            child = parent;
            //父节点 = 父节点的父节点
            parent = (parent-1)/2;
        }
    }
    //交换
      private void swap(int[]array,int left,int right) {
        int tmp=array[left];
        array[left]=array[right];
        array[right]=tmp;
    }

2.3.4 堆的删除

由于删除的是堆顶元素,我们思考一下,如何删除?有人会想到:将堆顶元素删除,然后所有元素向前移一位,最后再对每个结点进行向下调整.其实这个方法是行得通的,但却不是一个好的方法,因为所有元素前移一位就会打乱堆本身的顺序,再对堆中的每个元素向下调整,那么时间复杂度最坏可以达到O(N),那有没有什么好一些的方法吗?

答案是:有的.

注意:堆的删除一定删除的是堆顶元素。具体如下:

  1. 将堆顶元素对堆中最后一个元素交换
  2. 将堆中有效数据个数减少一个
  3. 对堆顶元素进行向下调整

我们看图:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
代码入下:

    /**
     * 删除
     * @return
     */
    public int pop() {
        if(isEmpty()) {
            return -1;
        }
        int tmp = elem[0];
        //交换
        swap(elem,0,UsedSize-1);
        //元素个数-1
        UsedSize--;
        //调用向下调整,从0开始
        shiftDown(0,UsedSize);
        return tmp;
    }

时间复杂度 : O(logN) 即树的高度

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

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

相关文章

字符串匹配 - 模式预处理:朴素算法(Naive)(暴力破解)

朴素的字符串匹配算法又称为暴力匹配算法&#xff08;Brute Force Algorithm&#xff09;&#xff0c;最为简单的字符串匹配算法。算法简介朴素的字符串匹配算法又称为暴力匹配算法&#xff08;Brute Force Algorithm&#xff09;&#xff0c;它的主要特点是&#xff1a;没有预…

功率放大器科普知识(晶体管功率放大器的注意事项)

虽然功率放大器是电子实验室的常用仪器&#xff0c;但是很多人对于它却没有清晰的认识&#xff0c;下面就让安泰电子来为大家介绍功率放大器的科普内容以及使用注意事项&#xff0c;希望大家可以对功率放大器有清晰的认识。功率放大器可以把输入信号的功率放大&#xff0c;以满…

NFT Insider #86:A16z 领投,YGG 获得 1380 万美元融资,The Sandbox与《北斗神拳》合作

引言&#xff1a;NFT Insider由NFT收藏组织WHALE Members、BeepCrypto联合出品&#xff0c;浓缩每周NFT新闻&#xff0c;为大家带来关于NFT最全面、最新鲜、最有价值的讯息。每期周报将从NFT市场数据&#xff0c;艺术新闻类&#xff0c;游戏新闻类&#xff0c;虚拟世界类&#…

智能小车红外循迹原理

循迹电路循迹电路由收发一体的红外收发管P1&#xff0c;P2&#xff1b;电位器R18&#xff0c;R29&#xff1b;发光二极管D6&#xff0c;D7和芯片LM324等组成。一共有两路&#xff0c;对应的红外电位器用于调节灵敏度。LM234用于信号的比较&#xff0c;并产生比较结果输出给单片…

MySQL8.0 optimizer_switch变化

Optimizer_switch变量是支持对优化器行为的控制。是一组值标志&#xff0c;每个标志都有一个on或off的值&#xff0c;以指示是否启用或禁用相应的行为。 MySQL8.0里除了熟悉的hash join重大变化之外&#xff0c;其他方面也有优化。 mysql> SHOW VARIABLES LIKE OPTIMIZER_…

14 基数排序(桶排序)

文章目录1 基数排序基本思想2 基数排序的代码实现2.1 java2.2 scala3 基数排序总结1 基数排序基本思想 1) 基数排序&#xff08;radix sort&#xff09;属于“分配式排序”&#xff08;distribution sort&#xff09;&#xff0c;又称“桶子法”&#xff08;bucket sort&#…

【Python】循环语句(while,for)、运算符、字符串格式化

一、while循环Python 编程中 while 语句用于循环执行程序&#xff0c;即在某条件下&#xff0c;循环执行某段程序&#xff0c;以处理需要重复处理的相同任务。其基本形式为&#xff1a;while 判断条件(condition)&#xff1a;执行语句(statements)执行语句可以是单个语句或语句…

Git、小乌龟、Gitee的概述与安装应用超详细(组长与组员多人开发版本)

目录 一、概述 1.什么是Git&#xff1f; 2.Git历史来源 3.Git的优点? 4.什么是版本控制&#xff1f; 5.版本控制工具种类&#xff1f; 6.Git工作机制 7.Git、小乌龟、Gitee、凭据管理器的简单介绍 二、Git下载安装 下载Git 安装Git 安装完成后查看版本 三、下载小…

防水蓝牙耳机评测,值得入手的四款蓝牙耳机分享

提到蓝牙耳机&#xff0c;大家第一反应是音质跟佩戴舒适度要好&#xff0c;其实除了这两个功能&#xff0c;还有就是防水性能不能少&#xff0c;而且防水等级越高&#xff0c;耳机寿命也就越长&#xff0c;那么&#xff0c;我们该如何 选购一款好用的蓝牙耳机呢&#xff1f;下面…

Echarts 配置横轴竖轴指示线,更换颜色、线型和大小

第018个点击查看专栏目录本示例是描述如何在Echarts上配置横轴竖轴指示线&#xff0c;更换颜色、线型和大小。方法很简单&#xff0c;参考示例源代码。 文章目录示例效果示例源代码&#xff08;共85行&#xff09;相关资料参考专栏介绍示例效果 示例源代码&#xff08;共85行&a…

数据的TCP分段和IP分片

本文简述下TCP分段和IP分片的区别与联系。 我们知道&#xff0c;用户空间的数据拷贝到内核空间的TCP发送缓冲区&#xff08;这个是一个结构体&#xff0c;叫sk_buffer&#xff0c;简称skb&#xff09;后就由内核网络协议栈做后续的封装和发送处理了&#xff0c;用户无需考虑下…

【Node.js】开发自己的包!

造包开发自己的包&#xff01;初始化包的基本结构页面使用根据需要也可以将模块化拆分编写包的说明文档发布包把包发布在npm上删除已发布的包模块的加载机制内置模块的加载机制自定义模块的加载机制第三方模块的加载机制当目录作为模块时的加载机制开发自己的包&#xff01; 初…

3|射频识别技术|第二讲:RFID系统的组成与工作原理|批注·上

https://blog.csdn.net/m0_57656758/article/details/128153964?spm1001.2014.3001.5501我国用无线射频识别技术实现药品管理的市场还是空白其运用具有较大的市场空间。药品运输及存储环境监控药品有效期监控提升用药安全策略血液制剂监控特殊、违禁药品监控商品价格监控药品生…

【Flutter】入门Dart语言:简单易懂的变量指南

文章目录一、概述二、详解1. 变量的声明2. 常量变量3.late 延迟初始化变量4. 变量的命名规则三、总结一、概述 “不抱有希望的人生是毫无意义的。” —— 阿卜杜勒阿齐兹 Dart中的变量是存储值的容器。它们可以是数字、字符串、布尔值或其他数据类型。变量在定义时必须指定类型…

网络原理 (1)

网络原理 文章目录1. 前言&#xff1a; 2. 应用层2.1 XML2.2 json2.3 protobuffer3. 传输层3.1 UDP3.1 TCP4. TCP 内部的工作机制 &#xff08;重点&#xff09;1. 确认应答 2.超时重传3. 连接管理3.1 建立联系 &#xff1a;三次握手3.2 断开连接 : 四次挥手4. 滑动窗口5. 流量…

长按power键,点击重启按钮,系统重启流程一

1.有可能会涉及到如下文件 2.文件流程

Spring基础总结(上)

Spring基础总结(上) 1. Spring 如何创建一个 Bean 对象 通过调用对象的无参构造方法创建一个新对象&#xff0c;然后通过依赖注入得到bean对象(默认单例)依赖注入这一步对新对象中添加了 Autowired 或者Resource 等注解的属性赋值&#xff0c;得到 Bean 对象&#xff0c;如下…

openOffice pdf.js spring boot 微信在线预览office pdf文件

下载openoffice 并安装//pdf.js 案例 https://mozilla.github.io/pdf.js/examples/index.html#interactive-examples//openoffice 连接不上 进入安装目录 cmd 运行以下命令 soffice -headless -accept"socket,host127.0.0.1,port8100;urp;" -nofirststartwizard<!…

技术管理之产品管理

一、产品相关概念 1.1 产品的定义 作为商品提供给市场&#xff0c;被人们使用和消费&#xff0c;并能满足人们某种需求的任何东西&#xff0c;包括有形的物品和无形的服务、组织、观念或者它们的组合&#xff1b;简单点产品就是解决某一类问题的东西。 1.2 产品思维 产品思…

安全研发人员能力模型窥探

能力 是一个比较抽象的概念&#xff0c;不同的行业、管理者、研发人员对能力的认知都会有差异。另外&#xff0c;作为研发团队的相应的职级定级、绩效考核的基础&#xff0c;一个“大家普遍认可”的能力的模型是非常重要的。这是比职级模型更高层的一个基本模型&#xff0c;所谓…