【排序算法】堆排序

堆与一维数组

建立堆与一维数组的联系

堆排序并不是直接对堆节点Node类型排序,而是通过建立索引之间的关系,对一维数组排序。
称之为堆排序,是因为节点索引值之间的关系与完全二叉树的非常类似,而树又称堆。
设根节点为ii0开始记,则:

  • 左孩子:2i+1
  • 右孩子:2i+2

也就是说,如果要交换根节点和左孩子的数据,就是将int[i]int[2i+1]交换(当数据为int型时)。
这一关系是由“堆是一个完全二叉树”决定的。
堆排序的思路就是,在堆的根节点与左右孩子之间排序,然后递归地分别对左右孩子对应的子树实行相同的排序。
与冒泡、选择、插入排序的区别在于:

  • 冒泡、选择、插入都是相邻项间进行比较,n个元素有n-1个空位,会比较n-1次。
  • 堆排序的根节点和右孩子之间的差值为i+2,并且间隔随i增大而增大,可以显著减少比较次数。


在排序的规则上,有大顶堆和小顶堆两种:

  • 大顶堆:将最大值放到堆顶
  • 小顶堆:将最小值放到堆顶。

在冒泡、选择、插入排序中,我们无法通过一轮比较保证全局有序,是因为每轮比较的对象只有相邻两项,只能保证局部有序。
在堆排序中,同样无法通过一轮比较保证全局有序,单次只能在子树的根节点和左右孩子三个元素之间局部有序。
堆排序的步骤为(以大顶堆为例):

  1. 递归地选出局部最大值,当整个数组局部排序完成后,根节点下标为[0]的根节点元素即为全局最大值。
  2. 将全局最大值与数组最后一个元素交换位置。
  3. 忽略数组最后一个元素,重复第1步,将新的全局最大值与倒数第二个元素交换位置。
  4. 如此反复,得到有序序列。

对比冒泡排序,可以发现,堆排序与冒泡排序非常类似,都是选出全局最大值,然后将全局最大值加入到有序序列。
不同之处在于冒泡排序是相邻元素间两两比较,而堆排序有较大的空隙,减少了比较次数,从而优化时间复杂度。

局部最大值

假设有一个待排序的数组:int nums[] = { 2,5,4,1,3,7,6 };
我们可以拆成如下几个堆:

  • [2,5,4]
  • [5,1,3]
  • [4,7,6]

首先定义堆内的排序方法,这一步会被反复调用,我们可以单独拿出来,封装成一个函数。

//传入数组、根节点索引、无序序列长度
void adjust(int num[], int index, int n) {//j默认指向左孩子int j = 2 * index;if (j < n && num[j - 1] < num[j]) {//j指向左右孩子中的较大值j++;}//将较大值移到根节点位置if (num[index - 1] < num[j - 1]) {int tmp = num[index - 1];num[index - 1] = num[j - 1];num[j - 1] = tmp;}
}

这一方法只能在三个元素间选出较大值,子树的叶子节点的值可能比根节点还要大。

如果我们对红框内的子树排序完成之后,再对紫框进行排序,那么根节点4的左右孩子都将比根节点要大,我们还需要回头对红框进行一次排序。那么第一趟对红框的排序就显得很多余。
因此,我们实际的排序方式是自下而上。

建堆

掌握了局部最大值,我们就可以对一个线性数组进行堆排序了。
需要注意的是,堆排序仍然是对线性序列的排序,我们称这一算法为堆排序,是因为这一过程中,元素索引值之间的关系与完全二叉树非常类似。
前面已经说过,下标从0开始的排序需要回头再排一趟,因此我们选择自下而上建堆。
对于索引为i的元素,它的右孩子索引为2*i+1,我们需要保证它小于等于元素个数n,则有2*i+1<=n
化简得到:i<=(n-1)/2,由于结果为整型,1/2会丢失,所以i<=n/2,此即为计数器的初始值。
由于这里我们控制的是索引,从1开始,所以计数器控制条件为i>=0

朴实无华的建堆

int main() {int num[] = { 2,4,3,6,7,5,1 };int length = sizeof(num) / sizeof(num[0]);//建树for (int i = length/2; i >= 1; i--) {adjust(num, i, length);}//检验结果for (int i = 0; i < length; i++) {printf("%d ", num[i]);}return 0;
}

观察输出,我们会发现,一趟下来的结果并不是完全有序的。
与冒泡排序类似,我们每趟比较只能在三个元素间确定最大值,与兄弟节点向双亲节点传递最大值。
这样会导致部分元素虽然比较大,但因为不是当前子堆中最大的,而没有被向上传递。
这个问题在冒泡排序中也存在,冒泡排序的做法是多来几趟循环。在堆排序中,我们可以改进局部最大值的adjust()方法,递归地使得子树有序。

void adjust(int num[], int index, int n) {//向上覆盖会导致数据丢失,需要临时变量存储int tmp = num[index - 1];//j默认指向左孩子int j = 2 * index;while (j <= n) {if (j < n && num[j - 1] < num[j]) {//指向左右孩子中的较大值j++;}if (tmp >= num[j - 1]) {//当前的根节点已是最大元素,不需要交换break;}//将较大值向上覆盖//左右孩子的较大值赋值给双亲结点num[j / 2 - 1] = num[j - 1];//调整指针位置,指向该孩子的左孩子j = 2 * j;}//将num中第index个元素放到最终调整的位置上。num[j / 2 - 1] = tmp;
}

交换-调整

adjust()方法中,我们并没有对左右子树的位置进行调整,只能保证根节点大于左右孩子。
在建堆的过程中,我们也无法保证最后的结果是有序的,只能保证根节点大于它的左右孩子,而无法保证左右孩子间有序。

  • 交换指的是将目前排出的最大值与数组最后一个元素交换位置。
  • 调整指的是将交换后的剩余元素从上而下调整为一个新的大顶堆。
for (int i = length - 1; i >= 0; i--) {//交换int tmp = num[i];num[i] = num[0];num[0] = tmp;//调整adjust(num, 1, i);
}

在这一步的调整中,我们仍然调用了adjust()方法,并且在方法参数中,修改的值只有数组的右边界n
而在建堆过程中,我们是自下而上,进行了多次调整。
原因是adjust()方法的实现中,之能在一条线上调整,本质还是数组的移动。与在数组中插入元素后,普通的移动数据不同:

  • 普通的数组移动是相邻元素向后覆盖。
  • 堆排序的元素移动是满足完全二叉树下标索引关系的元素间向上覆盖。


也就是说,只能保证一条线上局部有序。
并且观察adjust()方法,可以发现它只对左右孩子中的较大值进行调整。如果自上而下建堆,那么无论循环多少次,较小值对应的子树都无法有效排序。因此在建堆的过程中,我们选择自下而上的方法,保证每一个根节点一定大于它的左右孩子。
在交换调整的过程中,原数组的末尾元素被调整到整个堆的根节点位置,它一定小于左右孩子,将在adjust()方法内移动到一个合适的位置。在移动的过程中,会将较大值向上移动,使得新树仍然有序。

总结概括

堆排序是对线性序列的排序,而不是真的对一个完全二叉树进行排序,用完全二叉树的形式解释堆排序的过程是出于直观的需要。

#include <stdio.h>
//传入数组、根节点索引、无序序列长度
void adjust(int num[], int index, int n);
int main() {int num[] = { 2,4,3,6,7,5,1 };int length = sizeof(num) / sizeof(num[0]);//建树for (int i = length / 2; i >= 1; i--) {adjust(num, i, length);}for (int i = length - 1; i >= 0; i--) {//交换int tmp = num[i];num[i] = num[0];num[0] = tmp;//调整adjust(num, 1, i);}//检验结果for (int i = 0; i < length; i++) {printf("%d ", num[i]);}return 0;
}void adjust(int num[], int index, int n) {//向上覆盖会导致数据丢失,需要临时变量存储int tmp = num[index - 1];//j默认指向左孩子int j = 2 * index;while (j <= n) {if (j < n && num[j - 1] < num[j]) {//指向左右孩子中的较大值j++;}if (tmp >= num[j - 1]) {//当前的根节点已是最大元素,不需要交换break;}//将较大值向上覆盖//左右孩子的较大值赋值给双亲结点num[j / 2 - 1] = num[j - 1];//调整指针位置,指向该孩子的左孩子j = 2 * j;}//将num中第index个元素放到最终调整的位置上。num[j / 2 - 1] = tmp;
}

堆排序的第一步是将原始序列初始化为一个堆。for循环的计数器从后开始,自下而上初始化。
然后自上而下进行一系列“交换调整”,将全局最大值移到序列后面。调整后的序列仍然是一个大顶堆。
大顶堆的排序结果是从小到大排列,小顶堆的堆排序结果是从大到小排列。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023.7.2-【for语言】:输入一个整数,并输入该整数对应个数的整数,求他们的和与平均值

程序&#xff1a; int a;int b0;int c;int sum0;double ave;printf("请输入待求整数的个数&#xff1a;");scanf("%d",&a);for (b 1; b<a; b){printf("整数%d&#xff1a;", b);scanf("%d", &c);sum c;}printf("以上…

【ChatGpt】解决视频框交换中的平滑过渡的问题

【ChatGpt】解决视频框交换中的平滑过渡的问题 问题抽象chatgpt 看看直接给参考代码 解决效果 问题 在视频的播放中&#xff0c;我们想调换下容器的位置 &#xff0c;在互调的过程中&#xff0c;如果需要重新进行数据的初始化&#xff0c;获取与加载&#xff0c;就会很慢&…

Java 多线程

java多线程 第一种方法&#xff1a; 1.自定义线程类继承Thread类 2.重写run()方法 3.创建线程对象&#xff0c;调用start()方法启动线程 第二种方法&#xff1a; 1.实现Runnable()接口 2.重写run()方法 3.创建线程对象&#xff0c;调用start()启动线程

机器学习李宏毅学习笔记34

文章目录 前言一、Knowledge distillation二、Parameter quantization三、Architecture design四、Dynamic computation总结 前言 神经网络压缩&#xff08;二&#xff09;其他方法 一、Knowledge distillation 先train一个大的network叫做teacher network&#xff0c;小的ne…

selenium-wire简介

目录 前言&#xff1a; 一.简介 二.用法 1.安装selenium-wire库 2.获取请求信息 3.获取响应信息 4.实例 前言&#xff1a; Selenium Wire 是一个 Python 库&#xff0c;它扩展了 Selenium WebDriver 的功能&#xff0c;使其能够轻松地拦截、修改和检查浏览器与服务器之…

【Vue3】学习笔记-watch函数

与Vue2.x中watch配置功能一致 两个小“坑”&#xff1a; 监视reactive定义的响应式数据时&#xff1a;oldValue无法正确获取、强制开启了深度监视&#xff08;deep配置失效&#xff09;。监视reactive定义的响应式数据中某个属性时&#xff1a;deep配置有效。 <template&…

再述时序约束

再述时序约束 一、为什么要加时序约束&#xff1f;二、时序分析是什么&#xff1f;三、时序分析的一些基本概念三、 时序分析的一些基本公式 一、为什么要加时序约束&#xff1f; 一次笔者在调试HDMI输出彩条&#xff0c;出现彩条时有时无现象&#xff0c;笔者视频输出芯片的驱…

MATLAB 之 非线性方程数值求解、最优化问题求解和常微分方程初值问题的数值求解

这里写目录标题 一、非线性方程数值求解1. 单变量非线性方程求解2. 非线性方程组的求解 二、最优化问题求解1. 无约束最优化问题求解2. 有约束最优化问题求解3. 线性规划问题求解 三、常微分方程初值问题的数值求解1. 龙格—库塔法简介2. 龙格—库塔法的实现 一、非线性方程数值…

React hooks文档笔记(四) useRef

useRef 当想让组件记住一些信息&#xff0c;又不想触发新的渲染&#xff0c;可以使用ref&#xff1a;总是返回同一个对象。 1. state 和 refs 的比较 refs&#xff08;普通的 JavaScript 对象&#xff09; state 更改时不触发重新渲染 更改时触发重新渲染 可变的——修改/…

华为OD机试真题 Python 实现【核酸检测人员安排】【2023Q1 100分】,附详细解题思路

目录 一、题目描述二、输入描述三、输出描述四、补充说明五、Python算法源码六、效果展示1、输入2、输出 一、题目描述 在系统、网络均正常的情况下组织核酸采样员和志愿者对人群进行核酸检测筛查。 每名采样员的效率不同&#xff0c;采样效率为N人/小时。 由于外界变化&…

记录一次oracle报错IMP-00058: 遇到 ORACLE 错误 904 ORA-00904: “xxx“: 标识符无效

错误原因&#xff1a;导入的表中缺少DATASRC字段 解决方法&#xff1a;在要导入的表中加上DATASRC字段

TCP协议的十大核心特性总结(全面)

目录 一.TCP本身特性 二.报文格式 TCP十大核心特性 一.确认应答 二.超时重传 三.连接管理(三次握手,四次挥手) 三次握手 四次挥手 四.滑动窗口 情况一:接收方的ACK丢失 情况二:发送方的数据包丢失 五.流量控制 六.拥塞控制 七.延迟应答 八.捎带应答 九.粘包问题 …