热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

数据结构堆和堆排序以及堆排序的优化

堆的结构,我们可以分为物理中的真实结构和脑海中的逻辑结构,换句话来说,堆其实在实

堆的结构,我们可以分为物理中的真实结构和脑海中的逻辑结构,换句话来说,堆其实在实现上是一种结构,但是我们分析的时候是另一种结构。

好了,不卖关子了。

堆在实现的时候其实是数组结构,但是我们在分析的时候在脑海中应该是完全二叉树结构。

那我们先说一下什么是完全二叉树。

完全二叉树

对于完全二叉树,就是二叉树的节点要么是满的,要么二叉树是不满的,但是它也是从左到右依次变满的状态。

举个例子来说:

  • 如果它是孤零零的一个节点,那它也是完全二叉树,因为它只有一层,并且在这一层中是满的。
  • 如果它是两个节点,那么第二层的节点处在第一层节点的左节点上,右子树为空(从左到右依次变满),这样的也叫作完全二叉树
  • 如果它是两个节点,那么第二层的节点处在第一层节点的右节点上,左子树为空,这样的就不能叫做完全二叉树结构。
    在这里插入图片描述

堆在实现的时候,并没有用二叉树的结构,这只是我们脑海中堆应该有的一个概念。

那堆到底是怎么实现的呢?我们脑海中的树是如何具体实现的呢?

堆其实可以用一个从0出发的连续数组来描述的。
在这里插入图片描述

那问题又来了,从一段连续的数组到二叉树它又有什么对应的关系呢?

  • 首先,二叉树中某个节点下标为 i ,它的左子节点下标为 2i+1 ,右子节点下标是 2i + 2 ,它的父节点下标为( i - 1 )/ 2
  • 那当然,数组默认是从下标 0 开始使用的,上面的对应关系也只是适用于从下标0 开始。
  • 如果数组从下标1 开始使用呢?二叉树中某个节点下标为 i ,它的左子节点下标就变成了 2i ,右子节点下标变成 2i +1 ,它的父节点下标变为 i / 2
  • 举个例子
    在这里插入图片描述
堆的分类
  • 堆分为大根堆和小根堆
  • 大根堆:在这个完全二叉树中,以某个节点起始的整棵树,树中的最大值就是该节点
  • 小根堆:在这个完全二叉树中,以某个节点起始的整棵树,树中的最小值就是该节点
    在这里插入图片描述

heapInsert操作(插入)和heapify操作(删除)

大根堆为例

两个思考问题

1. 当用户给了一个数组时,用户逐渐向数组中添加数字,我们该如何调整使其变成大根堆呢?

  • 逐渐向数组添加数字,当添加的数字( i 位置),小于它的父节点(( i - 1 )/ 2)时,直接添加就可以了。
  • 当添加的数字( i 位置),大于它的父节点(( i - 1 )/ 2)时,就需要进行交换了。将 i 位置的新添加的数字与它的父节点(( i - 1 )/ 2)的数字进行交换。
    在这里插入图片描述
  • 通过上述方法,就能保证在数组的成长过程中,是成长出的每一步都是大根堆。
  • 这个向数组中逐步添加数字调整形成大根堆的过程就叫做heapInsert
  • 代码如下:

public void heapInsert(int [] arr ,int index){
while(arr[index] > arr[(index-1)/2]){
swap(arr,index,(index-1)/2);//交换index和(index-1)/2
index=(index-1)/2;
}
}

2. 调整为大根堆后,进行popmax()操作(减堆操作,减去最大值)后,我们又该如何调整使其变成大根堆呢?

  • 开辟一个临时变量max,将该大根堆中的最大值赋值给max,交换最后一位与最大值的位置(将最末尾的数字写入下标为0 的位置)
  • 注意:这里最末尾的数字不一定是最小值。只是最后一个数字在这里插入图片描述
  • 代码如下:

public void heapify(int [] arr,int index ,int heapsize){
int left = index * 2 + 1;//左孩子下标
while(left<heapsize){//下方还有孩子时
//找到index节点两个孩子中较大的一个,赋值给largest
int largest = left+1 < heapsize && arr[left+1] >arr[left] ? left+1 : left;
//较大的孩子与父节点index进行比较
largest = arr[largest] > arr[index] ? largest : index;
//两个孩子均没有父节点大
if(largest==index){
break;
}
swap(arr,largest,index);
index=largest;
left=index * 2 + 1;
}

}

关于堆,关键的也就是这两个操作heapInsert和heapify。清楚堆的这两个操作,对于堆排序就很容易理解了

堆排序

堆排序,简单来说,就是重复的heapify操作。再简单点,就是popmax操作。

举个例子来说。

假如用户给了一个数组[ 3 4 0 2 7 ],怎么进行堆排序呢?

我们上面讲到,当用户一个一个向数组中添加新的数字,我们可以使用heapInsert操作将其排成大根堆

现在用户给了我们数组中全部的数字,让我们排成大根堆,怎么做呢?

其实方法是一样的。我们自己一个一个加进堆中不就可以了!

对,这就是堆排序的第一步。

1. 先将整个数组变成大根堆结构

  • 有效size = 1。先看第 0 位 - 第 0 位上是不是大根堆?[ 3 4 0 2 7 ]

    • 此时 i = 0;只有 3 自己,当然,是大根堆。
  • 有效size = 2。再看第 0 位到第 1 位呢?

    • 不是大根堆,此时 i = 1;因为4的父节点(i-1)/2 =0 ,第 0 位为3 <4
    • 小了怎么办? 我们就用 heapify 调整不就可以了
    • 经过调整之后,数组就变为了[ 4 3 0 2 7 ]
      在这里插入图片描述
  • 有效size = 3。再往下,第 0 位到第 2 位呢?

    • 是大根堆,此时 i = 2;因为 0 的父节点(i-1)/2 =0 ,第 0 位为 4 > 0
    • 此时,数组为[ 4 3 0 2 7 ]
      在这里插入图片描述
  • 有效size = 4。第 0 位到第 3 位呢?

    • 是大根堆,此时 i = 3;因为 2 的父节点(i-1)/2 =1 ,第 1 位为 3 > 2
    • 此时,数组为[ 4 3 0 2 7 ]
      在这里插入图片描述
  • 有效size = 5。第 0 位到第 4 位呢?

    • 是大根堆,此时 i = 4;因为 7 的父节点(i-1)/2 =1 ,第 1 位为 3 <7
    • 用 heapify 调整,
    • 子节点与父节点交换,交换完之后,数组为[ 4 7 0 2 3 ],此时是大根堆了吗?
    • 当然还不是,我们在进行调整,子节点与父节点交换,交换完之后,数组为[ 7 4 0 2 3 ],OK,此时调整为了大根堆
      在这里插入图片描述

2. 到这里,我们的堆排序第一步就完成了。我们的第二步就相当于做popmax操作

  1. 将最后一位与第 0 位进行交换,有效size=5-1=4 。 即切断树的最后一个分支
    交换后数组为[ 3 4 0 2 7 ],这里7并没有消失,仍在这里,只是7处于无效区中。
    当然,交换完之后仍要保持大根堆的状态,此时就需要进行调整为[ 4 3 0 2 7 ]
    在这里插入图片描述
  2. 继续让最后一位与0位置的数进行交换,有效size=4-1=3 。 即切断树的最后一个分支
    交换后数组为[ 2 3 0 4 7 ],这里4 7并没有消失,仍在这里,只是4 7处于无效区中。
    当然,交换完之后仍要保持大根堆的状态,此时就需要进行调整为[ 3 2 0 4 7 ]
    在这里插入图片描述
  3. 继续让最后一位与0位置的数进行交换,有效size=3-1=2 。 即切断树的最后一个分支
    交换后数组为[ 0 2 3 4 7 ],这里3 4 7并没有消失,仍在这里,只是3 4 7处于无效区中。
    当然,交换完之后仍要保持大根堆的状态,此时就需要进行调整[ 2 0 3 4 7 ]
    在这里插入图片描述
  4. 继续让最后一位与0位置的数进行交换,有效size=2-1=1 。 即切断树的最后一个分支
    交换后数组为[ 0 2 3 4 7 ],这里2 3 4 7并没有消失,仍在这里,只是2 3 4 7处于无效区中。
    当然,交换完之后仍要保持大根堆的状态,此时就需要进行调整[ 0 2 3 4 7 ]
    在这里插入图片描述
  5. 继续让最后一位与0位置的数进行交换,有效size=1-1=0 。此时停止,再看此时数组为[ 0 2 3 4 7 ],这就排好序了,结束。

这整个过程就是堆排序,先排成大根堆,然后popmax,当有效区为0时,顺序就排好了,这整个过程就叫做heapSort()

代码如下:

public void heapSort(){
if(arr==null||arr.length<2){
//提示为空或只有一个数
}
//把列出的数组中的数一个一个heapInsert,排成大根堆
for(int i=0;i<arr.length;i++){
heapInsert(arr,i);
}
int heapSize=arr.length;
//交换第0位和最后一位
swap(arr,0,--heapSize);
//当有效区等于0时,结束
while(heapSize>0){
heapify(arr,0,heapSize);
swap(arr,0,--heapSize);
}
}
堆排序的优化

上述代码中,我们使用heapInsert()操作,将用户所提供的数组排成大根堆。
这种依次将数加进来的方法可行,但是我们分析它的时间复杂度为O( N * logN )
因此,就有了另外一种做法。
既然用户已经给了数组中所有的数,那我为什么不直接把这个二叉树排成大根堆呢?
举个例子吧。
还是那个数组[ 3 4 0 2 7 ]
在这里插入图片描述

  • 这个时候,我先看最后一层以最后一层节点为头结点的树是不是大根堆?

    因为只有一个数,所以,是大根堆。这一步,时间复杂度为O(N/2)

  • 倒数第二层,以倒数第二层节点为头结点的树是不是大根堆?

    这个时候,最糟糕的情况,我需要看的同时进行一步调整,这一步,时间复杂度为O(N/4)*2

  • 倒数第三层,以倒数第三层节点为头结点的树是不是大根堆?

    这个时候,最糟糕的情况,我需要看的同时进行两步调整,这一步,时间复杂度为O(N/8)*3

    根据这个,我们可以判断这样的时间复杂度应该为T(N)=O(N/2)+O(N/4)*2+O(N/8)*3+。。。。
    等比数列求和T(N)=C*O(N)。 C=2

所以,当用户给出我们数组中全部的数时,我们完完全全的从底开始进行heapify操作要比一个一个的heapInsert操作要快
修改代码如下:

public void heapSort(){
if(arr==null||arr.length<2){
//提示为空或只有一个数
}
//把列出的数组中的数一个一个heapInsert,排成大根堆
for(int i=arr.length;i>=0;i--){
heapify(arr,i,arr.length);
}
int heapSize=arr.length;
//交换第0位和最后一位
swap(arr,0,--heapSize);
//当有效区等于0时,结束
while(heapSize>0){
heapify(arr,0,heapSize);
swap(arr,0,--heapSize);
}
}
综上

堆排序的整个过程为:
- 现将整个数组变成大根堆结构
- 一个一个heapInsert,时间复杂度O( N * logN )
- 整体heapify,时间复杂度O( N )
- 将堆的最后一位与第 0 位进行交换,在调整为大根堆,当堆的大小为 0 时,排序完成


版权声明:本文为m0_51343267原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/m0_51343267/article/details/122322914
推荐阅读
  • 初识java关于JDK、JRE、JVM 了解一下 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
  • PriorityQueue源码分析
     publicbooleanhasNext(){returncursor&amp;amp;lt;size||(forgetMeNot!null&amp;amp;am ... [详细]
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 给定一个二维平面上的一些点,通过计算曼哈顿距离,求连接所有点的最小总费用。只有任意两点之间有且仅有一条简单路径时,才认为所有点都已连接。给出了几个示例并给出了对应的输出。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 生产环境下JVM调优参数的设置实例
     正文前先来一波福利推荐: 福利一:百万年薪架构师视频,该视频可以学到很多东西,是本人花钱买的VIP课程,学习消化了一年,为了支持一下女朋友公众号也方便大家学习,共享给大家。福利二 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • LeetCode笔记:剑指Offer 41. 数据流中的中位数(Java、堆、优先队列、知识点)
    本文介绍了LeetCode剑指Offer 41题的解题思路和代码实现,主要涉及了Java中的优先队列和堆排序的知识点。优先队列是Queue接口的实现,可以对其中的元素进行排序,采用小顶堆的方式进行排序。本文还介绍了Java中queue的offer、poll、add、remove、element、peek等方法的区别和用法。 ... [详细]
  • 本文详细介绍了PHP中与URL处理相关的三个函数:http_build_query、parse_str和查询字符串的解析。通过示例和语法说明,讲解了这些函数的使用方法和作用,帮助读者更好地理解和应用。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
author-avatar
手机用户2502857731
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有