堆排序算法的基本思想

堆排序定义

一般来说,算法就像数学公式,前人经过不断优化和验证得到有规律性的公式留给后人使用,当然也会交给后人验证的思路。那么堆排序算法就是这样,它有基本固定的定义如下:

1、将数组构建为一颗有规则的完全二叉树

2、该二叉树任意父结点值必须大于(最大堆)或小于(最小堆)孩子结点

3、该二叉树除了最底层外,其它层都是从左往右充满地

4、该二叉树任意父结点左孩子数组下标 = 父结点数组下标 * 2

5、该二叉树任意父结点右孩子数组下标 = 父结点数组下标 * 2 + 1

6、该二叉树任意孩子父结点数组下标 = 孩子结点数组下标 / 2

7、该二叉树高度(所有父结点)= 数组长度 / 2

8、最后再对构建好的二叉树进行排序

图解堆排序

堆排序算法的基本思想

下面我们通过堆排序算法来实现。

构建最大堆二叉树

将数组构建为一颗有规则的完全二叉树,该二叉树遵循任意父结点必须大于(最大堆)孩子结点。当然,构建最小堆也是可以的,具体看场景来设计,一般从小到大排序可以选择最大堆,从大到小排序可以选择最小堆。当然最大堆也可以实现从大到小排序。

最大堆二叉树定义如下:

1、左孩子结点数组索引=父结点数组索引 * 2。伪代码如下:

private int leftIndex(int i) { return 2 * i;}

2、右孩子结点数组索引=父结点数组索引 * 2 + 1。伪代码如下:

private int rightIndex(int i) { return 2 * i + 1;}

3、父结点数组索引=孩子结点数组索引 / 2

private int parentIndex(int i) { return i / 2;}

为了描述直观,数组下标从1开始,但实际编写代码在操作数组取值时候需要减去1,因为实际程序中的索引是从0开始的。

堆排序算法的基本思想

假设我们按数组顺序开始构建

堆排序算法的基本思想

上图满足了父子结点直接的数组索引计算定义,但是它不满足最大堆的定义(任意父结点必须大于孩子结点),父结点6比右子结点9要小,所以我们需要对原数组值进行交换(注意:只是将两个下标对应的值进行交换,下标不变)来满足最大堆二叉树的定义,调整后结果如下:

堆排序算法的基本思想

此时,再根据定义绘制最大堆二叉树如下:

堆排序算法的基本思想

下面我们继续按这种思路构建左孩子结点数组值为3下标为2的左右孩子结点,构建结果如下:

堆排序算法的基本思想

按照下标计算公式构建如下:

堆排序算法的基本思想

按照最大堆定义中任意父结点必须大于孩子结点交换后如下:

堆排序算法的基本思想

此时,我们发现,当我们将父结点值为3和右孩子结点值为13交换后,13、8、3这颗小二叉树是满足定义了,但是上一个9、3、6小二叉树变成了9、13、6,明细不满足父结点值(9)大于子结点(13)的要求,需要继续将9和13的值交换才是正确的。但是我们思考一下,如果那个9原始值是7呢,把7跟13交换后,第二个小二叉树7、8、3又不满足要求了。由此我们思考按顺序构建可能是有问题的,不但要往上检查,还要往下检查,反复如此,知道满足定义为止,我们思考,这肯定不是最优的方式,那么有没有更好的方式呢?

答案是有,那就是反过来自底向上构建,先构建最末尾的那个父结点,再一层层向上构建,这样好处是每次构建上层的时候前面构建好的那个下层父结点会作为上层的子结点,如果这个子结点(下层的父结点)跟上层父结点发送交换,那么证明这个子结点必定大于下面所有子结点,只需要往下检查即可,不需要往上检查,这样减少程序执行的步骤进一步优化了算法。

现在问题来了,我们怎么获取到最末尾的父结点,实现自底向上进行构建呢?

根据上面的定义:”该二叉树高度(所有父结点)= 数组长度 / 2” 我们可以很容易写出自底向上构建的代码如下:

/** * 自底向上构建最大堆 * * @param param 待排序数组参数 */private void buildMaxHeap(Integer param) { //自底向上构建最大堆, 数组长度的前面一半都是父结点 int parentIndex = param.length / 2; for (int i = parentIndex; i > 0; i–) { //构建最大堆 this.maxHeap(param, i, param.length); }}

构建最大堆maxHeap代码如下:

/** * 构建最大堆必须满足以下要求: * 1、除了根以外的所有结点i都要满足 param[parentIndex(i)] >= param * 2、反过来就是任意父结点值必须大于孩子结点,最顶根父结点值是整颗树的最大值 * * @param param 待排序数组参数 * @param parentIndex 给定任意父结点索引 * @param heapLength 堆长度(一般就是数组长度) */private void maxHeap(Integer param, int parentIndex, int heapLength) { //计算左孩子结点数组下标 int leftIndex = leftIndex(parentIndex); //计算右孩子结点数组下标 int rightIndex = rightIndex(parentIndex); //最大值数组下标,默认是父结点 int maxValIndex = parentIndex; //如果左孩子结点值比父结点的值要大,则不满足最大堆要求 //因为下标我们从1开始,所以每次取值减去1保证数组不越界 if (leftIndex <= heapLength && param[leftIndex – 1] > param[parentIndex – 1]) { //这里记录最大值数组下标为左孩子结点 maxValIndex = leftIndex; } //如果右孩子结点值比前面计算得到的最大值要大,则不满足最大堆要求 if (rightIndex <= heapLength && param[rightIndex – 1] > param[maxValIndex – 1]) { //这里记录最大值数组下标为右孩子结点 maxValIndex = rightIndex; } //如果确定父结点的确不是最大值 if (maxValIndex != parentIndex) { //需要把最大值数组下标位置的值(左或右孩子)交换到父结点下标的位置 Integer oldParentVal = param[parentIndex – 1]; param[parentIndex – 1] = param[maxValIndex – 1]; //交换后,下标为maxValIndex的结点的值是原来父结点的值 param[maxValIndex – 1] = oldParentVal; //经过交换后,以maxValIndex下标作为父结点的子树又可能违反最大堆的性质 //因此这里需要继续递归调用maxHeap maxHeap(param, maxValIndex, heapLength); }}

下面我们重新根据原始数组按照自底向上的方式构建最大堆二叉树,根据计算计算公式(8 / 2)计算得到最末尾的父结点为4:

1、构建最末尾父结点(8/2=4)

堆排序算法的基本思想

2、构建倒数第二个父结点(8/2-1=3)

堆排序算法的基本思想

实际上我们需要足够细心,值进行交换后数组已经发生变化,之前父节点下标为3构建前值为9,现在构建引起的交换后值为左孩子结点6的值16,左孩子结点下标6的值变为原来父节点下标为3的值。

除此之外,我们还需要再细心点,就是每次发生交换值后,需要向下检查,这点后面我们还会再次提到和演示。

堆排序算法的基本思想

3、构建倒数第二个父结点(8/2-2=2)

堆排序算法的基本思想

堆排序算法的基本思想

4、构建倒数第一个父结点(8/2-3=1)

堆排序算法的基本思想

堆排序算法的基本思想

5、最后一个父结点交换值后如下图,我们上面说过,每次交换值后需要向下检查,发现下标为3的父结点6小于它的左孩子结点9,需要再次交换。

堆排序算法的基本思想

6、构建好最终最大堆如下

堆排序算法的基本思想

堆排序

我们发现构建好的最大堆有一个特点,就是根一定是整棵树最大值,如果构建的是最小堆,那么就反过来,根是整棵树最小值。但是,我们还发现除此之外该根的左右孩子结点是以它们为父节点的下面所有孩子结点的最大值,下面我们就是通过该特点将堆进行排序。

1、我们先看下经过最大堆二叉树构建前后数组是这样的:

堆排序算法的基本思想

2、因为我们是从小到大排序,而构建最大堆后第一个下标一定是最大值,所以我们将第一个下标交换到数组的最后位置。

堆排序算法的基本思想

3、交换后,整个数组又违反了最大堆二叉树,咋办呢?很简单,我们根据上面的思路,每次交换值后需要基于交换后的孩子节点(孩子结点也是其它孩子结点的父节点)向下检查,我们可以调用函数:

maxHeap(Integer param, int parentIndex, int heapLength) param:当前数组传入parentIndex:下标1,因为下标1跟最后下标8交换后该父节点肯定违背了定义heapLength: 这里传入8-1=7。因为1已经排好序,剩下还有7个元素需要排序

1、 将根结点交换后,堆长度剩下7个元素,最大堆的根结点放到数组最后

2、 。下面通过图解方式帮助理解:

堆排序算法的基本思想

堆排序算法的基本思想

堆排序算法的基本思想

堆排序算法的基本思想

堆排序算法的基本思想

注意,算法讲究尽可能最优,剩下最后两个元素不必再走maxHeap构建最大堆逻辑,直接交换即可,堆排序代码如下:

private Integer heapSortAsc(Integer param) { if (param == null || param.length < 1) { return param; } //先自底向上构建最大堆 this.buildMaxHeap(param); //累计处理的数量 int handlerNum = 0; //数组最大下标 int heapLength = param.length; //最后两个下标0,1不用走构建最大堆,直接交换位置即可 int endIndex = 2; for (int i = heapLength; i > endIndex; i–) { //拿出堆最顶的根(必定是最大的)结点放到数组最后 //将堆最顶的根(必定是最大的)结点放到数组最后 this.swapElement(param, i – 1, 0); //排除处理好的数量 handlerNum++; heapLength = param.length – handlerNum; maxHeap(param, 1, heapLength); } //最后两个元素之间交换 this.swapElement(param, 1, 0); return param;} /** * 交换元素 * * @param param 数组参数 * @param leftIndex 左边索引 * @param rightIndex 右边索引 */private void swapElement(Integer param, int leftIndex, int rightIndex) { Integer oldMaxIndexVal = param; param = param; param = oldMaxIndexVal;}

验证算法

上面我们通过大量图解方式讲解了最大堆排序算法的原理,我们为了更直观的验证算法,现在把全部代码放在一起如下:

/** * <p> * 堆排序算法 * </p> * * @author laizhiyuan * @since 2019/9/20. */public class HeapSortAlgorithm { public static void main(String args) { HeapSortAlgorithm algorithm = new HeapSortAlgorithm(); Integer param = new Integer{6,3,9,8,16,13,2,1}; System.out.println(“排序前:” + JSON.toJSONString(param)); long t = System.currentTimeMillis(); algorithm.heapSortAsc(param); long t2 = System.currentTimeMillis(); System.out.println(“算法耗时:” + (t2 – t) + “ms”); System.out.println(“排序后:” + JSON.toJSONString(param)); } private Integer heapSortAsc(Integer param) { if (param == null || param.length < 1) { return param; } //先自底向上构建最大堆 this.buildMaxHeap(param); //累计处理的数量 int handlerNum = 0; //数组最大下标 int heapLength = param.length; //最后两个下标0,1不用走构建最大堆,直接交换位置即可 int endIndex = 2; for (int i = heapLength; i > endIndex; i–) { //拿出堆最顶的根(必定是最大的)结点放到数组最后 //将堆最顶的根(必定是最大的)结点放到数组最后 this.swapElement(param, i – 1, 0); //排除处理好的数量 handlerNum++; heapLength = param.length – handlerNum; maxHeap(param, 1, heapLength); } //最后两个元素之间交换 this.swapElement(param, 1, 0); return param; } /** * 交换元素 * * @param param 数组参数 * @param leftIndex 左边索引 * @param rightIndex 右边索引 */ private void swapElement(Integer param, int leftIndex, int rightIndex) { Integer oldMaxIndexVal = param; param = param; param = oldMaxIndexVal; } /** * 自底向上构建最大堆 * * @param param 待排序数组参数 */ private void buildMaxHeap(Integer param) { //自底向上构建最大堆, 数组长度的前面一半都是父结点 int parentIndex = param.length / 2; for (int i = parentIndex; i > 0; i–) { //构建最大堆 this.maxHeap(param, i, param.length); } } /** * 构建最大堆必须满足以下要求: * 1、除了根以外的所有结点i都要满足 param[parentIndex(i)] >= param * 2、反过来就是任意父节点值必须大于孩子结点,最顶根父节点值是整颗树的最大值 * * @param param 参数 * @param parentIndex 给定任意父节点索引 * @param heapLength 堆长度(一般就是数组长度) */ private void maxHeap(Integer param, int parentIndex, int heapLength) { //计算左孩子结点数组下标 int leftIndex = leftIndex(parentIndex); //计算右孩子结点数组下标 int rightIndex = rightIndex(parentIndex); //最大值数组下标,默认是父节点 int maxValIndex = parentIndex; //如果左孩子节点值比父节点的值要大,则不满足最大堆要求 //因为下标我们从1开始,所以每次取值减去1保证数组不越界 if (leftIndex <= heapLength && param[leftIndex – 1] > param[parentIndex – 1]) { //这里记录最大值数组下标为左孩子结点 maxValIndex = leftIndex; } //如果右孩子节点值比前面计算得到的最大值要大,则不满足最大堆要求 if (rightIndex <= heapLength && param[rightIndex – 1] > param[maxValIndex – 1]) { //这里记录最大值数组下标为右孩子结点 maxValIndex = rightIndex; } //如果确定父节点的确不是最大值 if (maxValIndex != parentIndex) { //则需要把最大值数组下标位置的值(左或右孩子)交换到父节点下标的位置 Integer oldParentVal = param[parentIndex – 1]; param[parentIndex – 1] = param[maxValIndex – 1]; //交换后,下标为maxValIndex的结点的值是原来父节点的值 param[maxValIndex – 1] = oldParentVal; //经过交换后,以maxValIndex下标作为父节点的子树又可能违反最大堆的性质 //因此这里需要继续递归调用maxHeap maxHeap(param, maxValIndex, heapLength); } } /** * 计算给定下标的父节点的下标 * * @param i 任意给定下标 * @return 父节点的下标 */ private int parentIndex(int i) { return i / 2; } /** * 计算给定下标的左节点的下标 * * @param i 任意给定下标 * @return 左节点的下标 */ private int leftIndex(int i) { return 2 * i; } /** * 计算给定下标的右节点的下标 * * @param i 任意给定下标 * @return 右节点的下标 */ private int rightIndex(int i) { return 2 * i + 1; } }

main方法执行输出如下:

排序前:[6,3,9,8,16,13,2,1]算法耗时:0ms排序后:[1,2,3,6,8,9,13,16]

算法时间复杂度

堆排序算法时间复杂度是O(nlgn)

关于计算算法时间复杂度后面有时间再专门写一篇文章详细说明。

算法适用场景

实际上堆排序算法并不经常被用于排序,大家都知道针对排序有很多钟算法,很多算法效率都比堆排序算法要好,比如平均情况的快排算法。堆排序一般使用场景有优先级计算。比如,作业调度优先级计算就很合适,一般优先级都是根据某个字段在一个集合中计算优先级最高(最大堆)或优先级最低(最小堆)的方式去实现,我们知道,如果仅仅只是计算最大堆或最小堆,我们只需要执行构建堆函数(maxHeap)即可,不需要进行堆排序(heapSort),构建最大堆的时间复杂度是O(lgn)比堆排序的复杂度O(nlgn)效率在给定规模下要高一个常量级别,增长速度方面低线性级别。

本文【堆排序算法的基本思想】由作者: 外键 提供,本站不拥有所有权,只提供储存服务,如有侵权,联系删除!
本文链接:https://www.cuoshuo.com/blog/4212.html

(0)
上一篇 2023-03-10 09:05:09
下一篇 2023-03-11 08:03:52

相关推荐

  • ps如何美白皮肤背景不变(ps怎样使照片皮肤变白)

    步骤: 1、找一张自己想美白提亮的照片,拖入PS; 2、按Ctrl+J复制一层图层; 3、在图层面板下面第四个黑白图标(创建新的填充或调整图层),出现一栏选项,选择色阶,除开原始图层及新建图层,会在出现一个由色阶加蒙版的图层; 4、调出色阶面板,有三个小三角形(黑、灰、白),拖动这三个,调整脸部的亮度,调成一个脸部看着舒服的亮度白; 5、在面板图层的混合模式…

    2023-03-22
    000
  • linux电子书 epub(epub什么意思)

    EPUB 文件是使用开放格式发布内容的好方法。 电子书提供了一种随时随地阅读书籍、杂志和其他内容的好方法。读者可以在长途飞行和乘坐火车时享受电子书打发时间。最流行的电子书文件格式是 EPUB 文件,它是“电子出版物electronic publication”的缩写。 EPUB 文件受到各种电子阅读器的支持,并且是当今电子书出版的有效标准。 因为 EPUB …

    2023-03-17
    400
  • jsp实现购物车功能总结

    原文: https://www.cnblogs.com/wang-meng/p/5854773.html 今天来写一下关于购物车的东西, 这里首先抛出四个问题: 1)用户没登陆用户名和密码,添加商品, 关闭浏览器再打开后 不登录用户名和密码 问:购物车商品还在吗? 2)用户登陆了用户名密码,添加商品,关闭浏览器再打开后 不登录用户名和密码 问:购物车商品还在…

    2023-03-14
    200
  • asp文件打开怎么是乱码

    相信我们在工作中,有时候,我们打开一份文件,会发现里面全是乱码。里面保存的重要信息都不能用了。既然发生了乱码,就要想着解决它,不然后续所有文件都乱码了,那就麻烦了。那要怎么解决呢?事实上,乱码文件有时并不那么难解决。只要找对原因,对症下药,就能修复好的。那么我们的文件乱码怎么恢复正常呢?下面小编就与大家分享一下解决之法。有需要的朋友可以参考一下。 文件乱码大…

    2023-03-15
    300
  • typedef用法详解C语言

    C语言 typedef C 语言提供了 typedef关键字,您可以使用它来为类型取一个新的名字。下面的实例为单字节数字定义了一个术语 BYTE: typedef unsigned char BYTE; 在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char的缩写,例如:BYTE b1, b2; 按照惯例,定义时会大写字母,以便提醒用户…

    2023-03-09
    700
  • udp协议和tcp协议在哪一层_tcp和udp协议

    你是否感觉 Http、Https、TCP、UDP这些协议很耳熟,经常听到但不知道是怎么回事;或是很了解,但让你解释又容易解释不清? 一起来看看他们之间的区别和联系吧~ 一、先有个基础的认知 HTTP和HTTPS是应用层协议,该层协议负责主机间数据传输; TCP和UDP是传输层协议,该层协议负责网络连接。 二、HTTP和HTTPS HTTPS = HTTP +…

    2023-03-12
    500
  • EAP系统可以比对图纸吗

    特斯拉在新春假期期间启动了增强版自动辅助驾驶EAP功能的老用户免费限时体验。很多朋友感兴趣EAP到底是什么功能,值不值得入手激活。今天跟大家聊聊特斯拉增强版自动辅助驾驶EAP功能的技术细节吧。 ↑特斯拉EAP传感器分布示意图 特斯拉增强版自动辅助驾驶EAP由各种传感器组成360度视角,具体包括: -8 个超声波传感器,位于前后探测四周8米范围 -2 个前视侧…

    2023-03-22
    000
  • 播放器代码错误是什么意思

    今天小米电视/小米盒子播放视频提示错误码600,1000怎么办?盒子不能播放了,提示错误代码600.1000这是什么情况?所有视频都无法进行播放,提示播放出错!错误码(600,1000)。经过一轮折腾,楼主分享3个解决方法。 播放出错!小米电视/小米盒子错误码600,1000原因分析 首先分析下小米电视/盒子播放出错!错误码(600,1000)的原因,这个是…

    2023-03-17
    200
  • 汇编语言指令由什么组成 机器语言中机器指令由什么组成

    前面我们了解了计算机底层的一些知识,比如计算机体系机构、操作系统、数据库、以及网络的基础知识,今天我们来研究一下计算机底层的语言,相信有了基础知识的铺垫,对于后期的编程学习会有莫大的帮助。 什么是计算机的底层语言 计算机底层语言是指直接在计算机硬件上运行的一类程序语言,主要有机器语言和汇编语言。 机器语言:机器语言是一种直接由计算机硬件执行的语言,它由二进制…

    2023-03-20
    000
  • 开放端口扫描怎么设置

    以下是使用端口扫描时会发现的一些常见端口: 端口 21 – FTP(文件传输协议) 端口 22 – SSH(安全外壳) 端口 23 – Telnet 端口 25 – SMTP(简单邮件传输协议) 端口 53 – DNS(域名服务器) 端口 80 – HTTP(超文本传输协议) 端口 110 – POP3(邮局协…

    2023-03-13
    400

发表回复

登录后才能评论
返回顶部
错说博客上线啦!