热门标签 | 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
推荐阅读
  • C++实现经典排序算法
    本文详细介绍了七种经典的排序算法及其性能分析。每种算法的平均、最坏和最好情况的时间复杂度、辅助空间需求以及稳定性都被列出,帮助读者全面了解这些排序方法的特点。 ... [详细]
  • 作者:守望者1028链接:https:www.nowcoder.comdiscuss55353来源:牛客网面试高频题:校招过程中参考过牛客诸位大佬的面经,但是具体哪一块是参考谁的我 ... [详细]
  • 使用Numpy实现无外部库依赖的双线性插值图像缩放
    本文介绍如何仅使用Numpy库,通过双线性插值方法实现图像的高效缩放,避免了对OpenCV等图像处理库的依赖。文中详细解释了算法原理,并提供了完整的代码示例。 ... [详细]
  • LeetCode 540:有序数组中的唯一元素
    来源:力扣(LeetCode),链接:https://leetcode-cn.com/problems/single-element-in-a-sorted-array。题目要求在仅包含整数的有序数组中,找到唯一出现一次的元素,并确保算法的时间复杂度为 O(log n) 和空间复杂度为 O(1)。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 本文详细介绍了如何在Linux系统上安装和配置Smokeping,以实现对网络链路质量的实时监控。通过详细的步骤和必要的依赖包安装,确保用户能够顺利完成部署并优化其网络性能监控。 ... [详细]
  • 深入理解Cookie与Session会话管理
    本文详细介绍了如何通过HTTP响应和请求处理浏览器的Cookie信息,以及如何创建、设置和管理Cookie。同时探讨了会话跟踪技术中的Session机制,解释其原理及应用场景。 ... [详细]
  • 深入理解 SQL 视图、存储过程与事务
    本文详细介绍了SQL中的视图、存储过程和事务的概念及应用。视图为用户提供了一种灵活的数据查询方式,存储过程则封装了复杂的SQL逻辑,而事务确保了数据库操作的完整性和一致性。 ... [详细]
  • 本文详细介绍了 Dockerfile 的编写方法及其在网络配置中的应用,涵盖基础指令、镜像构建与发布流程,并深入探讨了 Docker 的默认网络、容器互联及自定义网络的实现。 ... [详细]
  • Yii 实现阿里云短信发送 ... [详细]
  • 汇编语言等号伪指令解析:探究其陡峭的学习曲线
    汇编语言以其独特的特性和复杂的语法结构,一直被认为是编程领域中学习难度较高的语言之一。本文将探讨汇编语言中的等号伪指令及其对初学者带来的挑战,并结合社区反馈分析其学习曲线。 ... [详细]
  • 使用GDI的一些AIP函数我们可以轻易的绘制出简 ... [详细]
  • 本文详细介绍如何在VSCode中配置自定义代码片段,使其具备与IDEA相似的代码生成快捷键功能。通过具体的Java和HTML代码片段示例,展示配置步骤及效果。 ... [详细]
  • Kotlin基础教程:集合详解
    本文深入探讨了Kotlin中的集合类型,包括可变和不可变集合,并详细介绍了List、Map和Set的使用方法及其增删改查操作。 ... [详细]
  • 深入理解C++中的KMP算法:高效字符串匹配的利器
    本文详细介绍C++中实现KMP算法的方法,探讨其在字符串匹配问题上的优势。通过对比暴力匹配(BF)算法,展示KMP算法如何利用前缀表优化匹配过程,显著提升效率。 ... [详细]
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社区 版权所有