热门标签 | 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
推荐阅读
  • 作者:守望者1028链接:https:www.nowcoder.comdiscuss55353来源:牛客网面试高频题:校招过程中参考过牛客诸位大佬的面经,但是具体哪一块是参考谁的我 ... [详细]
  • 本文详细介绍了如何在Ubuntu系统中下载适用于Intel处理器的64位版本,涵盖了不同Linux发行版对64位架构的不同命名方式,并提供了具体的下载链接和步骤。 ... [详细]
  • 根据最新发布的《互联网人才趋势报告》,尽管大量IT从业者已转向Python开发,但随着人工智能和大数据领域的迅猛发展,仍存在巨大的人才缺口。本文将详细介绍如何使用Python编写一个简单的爬虫程序,并提供完整的代码示例。 ... [详细]
  • 汇编语言等号伪指令解析:探究其陡峭的学习曲线
    汇编语言以其独特的特性和复杂的语法结构,一直被认为是编程领域中学习难度较高的语言之一。本文将探讨汇编语言中的等号伪指令及其对初学者带来的挑战,并结合社区反馈分析其学习曲线。 ... [详细]
  • 使用GDI的一些AIP函数我们可以轻易的绘制出简 ... [详细]
  • 本文详细介绍如何在VSCode中配置自定义代码片段,使其具备与IDEA相似的代码生成快捷键功能。通过具体的Java和HTML代码片段示例,展示配置步骤及效果。 ... [详细]
  • Kotlin基础教程:集合详解
    本文深入探讨了Kotlin中的集合类型,包括可变和不可变集合,并详细介绍了List、Map和Set的使用方法及其增删改查操作。 ... [详细]
  • C# LiNQ 查询 join连接
    C# LiNQ 查询 join连接 ... [详细]
  • 堆是一种常见的数据结构,广泛应用于计算机科学领域。它通常表示为一棵完全二叉树,并可通过数组实现。堆的主要特性是每个节点的值与其父节点的值之间存在特定的关系,这使得堆在优先队列和排序算法中非常有用。 ... [详细]
  • 利用存储过程构建年度日历表的详细指南
    本文将介绍如何使用SQL存储过程创建一个完整的年度日历表。通过实例演示,帮助读者掌握存储过程的应用技巧,并提供详细的代码解析和执行步骤。 ... [详细]
  • PHP数组平均值计算方法详解
    本文详细介绍了如何在PHP中计算数组的平均值,涵盖基本概念、具体实现步骤及示例代码。通过本篇文章,您将掌握使用PHP函数array_sum()和count()来求解数组元素的平均值。 ... [详细]
  • 本文深入探讨了Python中的高阶函数和Lambda表达式的使用方法,结合实际案例解析其应用场景,帮助开发者更好地理解和运用这些强大的工具。 ... [详细]
  • 数组元素逆序排列的实现
    本文介绍了一种简单有效的方法,用于将整数数组中的元素进行逆序排列。通过折半交换对应位置的元素,可以高效地完成这一任务。 ... [详细]
  • 本文介绍如何使用 Python 的 xlrd 库读取 Excel 文件,并将其数据处理后存储到数据库中。通过实际案例,详细讲解了文件路径、合并单元格处理等常见问题。 ... [详细]
  • Java 数组及其常用操作
    本文详细介绍了 Java 中的数组类型、定义方法以及常见操作,帮助开发者更好地理解和使用 Java 数组。 ... [详细]
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社区 版权所有