【排序算法】堆排序

news2024/12/27 13:41:42

堆与一维数组

建立堆与一维数组的联系

堆排序并不是直接对堆节点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.coloradmin.cn/o/707741.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C#】委托、匿名方法、Lambda表达式和事件

【C#】委托、匿名方法、Lambda表达式和事件 委托 什么是委托&#xff1f; 委托和类一样&#xff0c;是用户自定义类型&#xff0c;是方法&#xff08;函数&#xff09;的抽象。通俗讲&#xff0c;委托就是 自定义类型的方法&#xff08;函数&#xff09;的代表。 声明委托 …

HTML+CSS+JavaScript华为主页

样式&#xff1a; HTMLCSSJavaScript仿华为首页 HTML: <!DOCTYPE html> <html><head><meta charset"utf-8"><link rel"stylesheet" type"text/css" href"Homepage.css"/><script type"text/ja…

NextJs下浅尝Prisma+Sqlite+逆向生成数据模型

1.安装prisma npm install prisma/client 2.创建schema.prisma npx prisma init 执行完命令后创建文件目录如下&#xff1a; 3.配置数据库连接 generator client {provider "prisma-client-js" }datasource db {provider "sqlite" //数据库类型 这…

libevent实践07:监听服务器并管理客户端

简介 函数bufferevent_new struct bufferevent * bufferevent_new(evutil_socket_t fd,bufferevent_data_cb readcb, bufferevent_data_cb writecb,bufferevent_event_cb eventcb, void *cbarg) 参数说明&#xff1a; fd:新客户端的文件描述符 readcb&#xff1a;一个函数指…

【Redis的优化】

目录 一、Redis 高可用二、 Redis 持久化2.1、Redis 提供两种方式进行持久化2.2、RDB 持久化1. 触发条件&#xff08;1&#xff09;手动触发&#xff08;2&#xff09;自动触发 2. 执行流程3. 启动时加载 2.3、AOF 持久化1. 开启AOF2. 执行流程(1&#xff09;命令追加(append)(…

深入理解 Linux 物理内存分配全链路实现

目录 内核物理内存分配接口 物理内存分配内核源码实现 内存分配的心脏 __alloc_pages prepare_alloc_pages 内存慢速分配入口 alloc_pages_slowpath 总结 内核物理内存分配接口 在物理内存分配成功的情况下&#xff0c; alloc_pages&#xff0c;alloc_page 函数返回的都是指…

2022最常用密码公布,你的账户安全吗?

密码管理工具 NordPass 公布了 2022 年最常用密码列表&#xff0c;以及破解密码所需的时间。该研究基于对来自 30 个不同国家 / 地区的 3TB 数据库的分析。研究人员将数据分为不同的垂直领域&#xff0c;使得其能够根据国家和性别进行统计分析。今年的研究主要聚焦于文化如何影…

工业软件对于现代制造业的生产效率和质量有何影响?

工业软件在提高现代制造业的生产力和质量方面发挥着至关重要的作用。比如&#xff1a; 流程自动化&#xff1a;工业软件可以实现各种制造流程的自动化&#xff0c;消除手动任务并减少人为错误。自动化通过简化操作、缩短周期时间和提高整体效率来提高生产力。它还可以最大限度地…

vue3和element plus踩坑

1.有说vue版本有两个&#xff0c;但检查之后发现只有一个&#xff0c;且为vue3的版本 2.也有说是因为命名的问题&#xff0c;组件名和页面名一致 最后发现是因为 在main.js里面引入element plus 使用这种use方式会报错&#xff0c;虽然也不知道为什么 import { createApp } …

《计算机系统与网络安全》第十一章 入侵检测与防御技术

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

Dell-Precision5520 电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网&#xff0c;转载需注明出处。&#xff08;下载请直接百度黑果魏叔&#xff09; 硬件配置 硬件型号驱动情况 主板Dell-Precision5520 处理器Intel Core i7-7820HQ已驱动 内存Micron 2400MHz DDR4 16GB x2已驱动 硬盘Samsung 970EVO 512GB已驱动 显…

Java中volatile的作用和原理

用法 volatile 是 Java 中的关键字&#xff0c;直接修饰成员变量&#xff0c;不能和 final 关键字同时使用。 private volatile boolean flag false;作用 当一个变量被声明为volatile时&#xff0c;它可以确保以下两点&#xff1a; 保证可见性&#xff1a;当一个线程修改了…

三维天地助力高校实验室数字化智能决策分析

近年来&#xff0c;随着检验检测行业技术的不断发展&#xff0c;高校实验室管理的复杂程度也在不断提高。由于传统的检测实验室日常工作任务繁重、费时费力&#xff0c;存在数据或信息的手动录入、人工计算&#xff0c;纸质文档资料的长期保存&#xff0c;数据快速汇总困难等诸…

大数据面试题:Kafka的Message包括哪些信息

面试题来源&#xff1a; 《大数据面试题 V4.0》 大数据面试题V3.0&#xff0c;523道题&#xff0c;679页&#xff0c;46w字 参考答案&#xff1a; 一个 Kafka 的 Message 由一个固定长度的 header 和一个变长的消息体 body 组成&#xff0c;header 部分由一个字节的 magic&…

Android 12 LED 定制灯效开发小结

文章目录 背景&#xff1a;Android 10 的设备上测试正常Android 12 中目前出现无法闪烁的问题电量变化广播监听总结参考 背景&#xff1a; 在定制的Android 10系统中&#xff0c;通过修改 Framwork 层的代码后&#xff0c;调用标准的接口后&#xff0c;能实现 LED 灯的闪烁灯效…

抖音旋转验证码分析

旋转验证码类型challenge_code为99996&#xff0c; 拿到的旋转验证码通常都是如下&#xff1a; 待旋转的图片&#xff1a; 旋转的背景图&#xff1a; 加密分析过程 可以参考&#xff1a;https://blog.csdn.net/weixin_38819889/article/details/129727564 旋转的难点在于如何…

英国 Tortoise Media发布2023年全球AI指数排名;美团宣布完成收购光年之外

&#x1f989; AI新闻 &#x1f680; 美团宣布完成收购光年之外&#xff0c;加强人工智能竞争力 摘要&#xff1a;美团在公告中宣布于2023年6月29日盘后收购光年之外的全部权益&#xff0c;以加强其在快速增长的人工智能行业中的竞争力。光年之外是中国领先的通用人工智能创新…

【ISO26262】汽车功能安全第一部分:术语

【tommi_wei@163.com】 故障响应时间 fault reaction time 从故障(2.42) 探测到进入安全状态(2.102) 的时间间隔。 故障容错时间间隔 fault tolerant time interval 在危害事件(2.59) 发生前, 系统(2.129) 中一个或多个故障(2.42) 可存在的时间间隔。 功能安全 functio…

C语言之网络高级编程笔记

基于Webserver的工业数据采集项目 html cgi Modbus协议 (应用层) 工具&#xff1a;Modus Slave/Poll wireshark Postman 一、Modbus起源 1.起源&#xff1a; Modbus由Modicon公司于1979年开发&#xff0c;是一种工业现场总线协议标准。 Modbus通信协议具有多个变种&#xf…

【Matlab】神经网络遗传算法函数极值寻优——非线性函数求极值

目前关于神经网络遗传算法函数极值寻优——非线性函数求极值的博客资源已经不少了&#xff0c;我看了下来源&#xff0c;最初的应该是来自于Matlab中文论坛&#xff0c;论坛出版的《MATLAB神经网络30个案例分析》第4章就是《神经网络遗传算法函数极值寻优——非线性函数极值寻优…