堆排序(含证明)

news2025/1/6 20:04:28

引言

前面我们讲过堆的基本操作的实现,现在给定一个int类型的数组,里面存放的数据是无序的,我们如何利用堆的思想来实现数组内数据的升序排列或降序排列呢?

通过前面讲到的堆的实现,我们可以想到,我们再开辟一块空间来模拟堆,将给定数组里的数据一一插入堆中(pushHeap)。这样做可以是可以,但如果是代码量未免有点大,而且还要额外开辟空间。如果是在平时考试时碰到这种题,那实现起来会有点麻烦。

如果能在给定数组上直接调整并减少点代码量就好了,今天我们就来讲讲如何这样实现堆排序。

堆排序主要涉及到两个步骤:1.建堆 2.利用堆的思想排序(具体怎么排序后面会讲)

接下来我们以排升序为例进行讲解。

如何建堆?

以建大堆为例:

如果是在原数组的基础上进行建大堆,通过前面的堆的学习和实现,我们可以通过在原数组进行向上调整法或向下调整法达到目的。

向上调整法建堆

我们以这个数组为例:

int a[7]={30,60,56,80,70,10,15};

db16d8a0dc2a4f67bd271283f8df41c4.png

要将它建成大堆:

514a5e2b44404f2bbccc36f313196c0e.png

从下向上遍历:从数组的第二个元素(下标为1)开始,依次向上遍历到第一个元素(下标为0)。这是因为堆的第一个元素(根节点)在构建过程中不需要进行向上调整。

向上调整:对于遍历到的每个元素(假设当前元素的下标为i),执行以下步骤:

  • 计算当前元素的父节点下标(parent = (i - 1) / 2)。
  • 比较当前元素与其父节点的值。
  • 如果当前元素的值大于其父节点的值,则交换这两个元素的位置。
  • 交换后,将当前元素的下标更新为其父节点的下标,并重复上述比较和交换步骤,直到当前元素的值不大于其父节点的值,或者当前元素成为根节点。
  • 完成建堆:当遍历完所有元素并完成所有必要的向上调整后,整个数组就形成了一个大顶堆。

附上代码:

void swap(dataType* x, dataType* y)
{
	dataType tmp = *x;
	*x = *y;
	*y = tmp;
}

void adjustUp(dataType* a, int child)
{
	assert(a);
	int parent = (child - 1) / 2;
	while (child > 0) {
		if (a[child] > a[parent]) 
		{
			swap(&a[parent], &a[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else {
			break;
		}
	}
}

void heapSort(int* a, int n) {
	for (int i = 1; i < n; i++) {
		adjustUp(a, i);
	}
}

向下调整法建堆

还是以原来的数组为例:

int a[7]={30,60,56,80,70,10,15};

将它建成大堆:

70f74c4107b6498fbc6520eee434163d.png

从最后一个非叶子节点开始,依次向上遍历每个节点(包括根节点)。对于每个节点,执行以下步骤:

  • 将当前节点视为父节点。
  • 比较其父节点与左右子节点的值(如果存在的话)。
  • 如果父节点的值小于左右子节点中的较小值,则交换父节点与较小子节点的值。
  • 将交换后的较小子节点视为新的父节点,重复上述比较和交换步骤,直到父节点的值大于或等于其所有子节点的值,或者父节点成为叶子节点。

当所有节点都按照上述步骤调整完毕后,整个数组就形成了一个大顶堆。

这里有一个问题,如何算最后一个出非叶子节点的下标呢?

在一个堆中,最后一个叶子节点的下标是n-1,那它父节点的下标就是(n-1-1)/2也就是(n-2)/2。

附上代码:

void swap(dataType* x, dataType* y)
{
	dataType tmp = *x;
	*x = *y;
	*y = tmp;
}
int getMax(dataType* a, int x, int y)
{
	if (a[x] > a[y])
	{
		return x;
	}
	else {
		return y;
	}
}
void adjustDown(dataType* a, int parent, int size)
{
	assert(a);

	while (parent < size)
	{
		int lChild = 2 * parent + 1, rChild = 2 * parent + 2, swapChild;
		if (rChild < size) {
			swapChild = getMax(a, lChild, rChild);
		}
		else {
			swapChild = lChild;
		}
		
		if (swapChild < size && a[parent] < a[swapChild]) {
			swap(&a[parent], &a[swapChild]);
			parent = swapChild;
		}
		else {
			break;
		}
	}
}
void heapSort(int* a, int n) {

	//向下调整法
	for (int i = (n - 2)/2; i >= 0; i--) {
		adjustDown(a, i, n);
	}

}

时间复杂度

向上调整法建堆

93e52c43bd69448a9efdfc7292ec5f93.jpg

 向下调整法建堆

8930ba646803479f84f7ff66f4d89109.png

总结

对比之下,向下调整法建堆的时间复杂度更低,所以我们更推荐用向下调整法建堆。

一个误区:排升序就是直接将原来无序的数组构建成小堆就可以了

说到建堆,无非就两种:要么建大顶堆,要么建小顶堆。但如果是排升序,很多人可能第一反应是直接建小顶堆就可以了。但是这样真的合适吗?我们以这个数组为例:

int a[5]={30,60,56,70,10};

e58f378a7f574d8cb603c35c7e37db64.png

 把它建成小顶堆会是这样:

099b6ba6382542d48dc376105ea8e18c.png

 对应的数组就是这样了:int a[5]={10,30,56,70,60};

我们发现这样调整后,数组里面的数据并不是完全按照升序排列的。为什么会这样呢?

我们需要知道,在小堆中,堆顶元素确实是最小的。但是,这并不意味着从堆顶到堆尾的所有元素都按升序排列。小堆的结构只保证了从根节点到每个叶子节点的路径上,元素是递增的。然而,不同路径上的元素之间并没有这样的保证。

所以,直接将原来无序的数组构建成小堆是不可行的办法。

排升序建大堆,排降序建小堆

那我们该怎么办呢?

答案是:排升序建大堆,排降序建小堆。

竟然跟我们最初的想法背道而驰,接下来我会讲解一下为什么要这么做以及具体如何实现。

我们以这个数组为例:

int a[5]={4, 10, 3, 5, 1}

具体步骤

1.我们先把将它建成大堆:

int a[5]={10,5,3,4,10}; 

6c8ad0247139419c86865056b7ebad55.png

2.交换堆顶元素与末尾元素:将堆顶元素(当前最大值)与序列末尾元素交换位置。

b7b48cdc67d74159afbe58cb02013d45.png

这一步骤将最大值“沉”到底部,同时保证了剩余元素组成的子序列仍满足堆的性质。

cfac0d24537541be837e6ec964f63d1c.png

3.调整剩余元素:将交换后的剩余元素(除去已排序的堆顶元素)重新调整为一个堆。

由于堆顶元素已被移到末尾,此时需要对剩余元素重新执行向下调整操作,使得新的堆顶元素成为剩余元素中的最大值。

39b3eafe4baa4326a0016302bdb1dfdc.png

 4.重复交换与调整:重复步骤2和步骤3,每次都将当前堆顶元素(即剩余部分的最大值)与末尾元素交换,并对剩余元素重新调整为堆。

53b9bdede498458a8ffed136970aeab9.png

         ①                          ②                         ③

 

0a79bc3a06e344dc84ef06fb4e22c601.png   ④                           ⑤                        ⑥

随着每一次交换和调整,有序序列逐渐扩大,堆的大小逐渐减小。

5.终止条件:当堆的大小减小到1时,整个序列已经有序,堆排序过程结束。

0577ca2b744048fcaf7619f587293fac.png

代码

void swap(dataType* x, dataType* y)
{
	dataType tmp = *x;
	*x = *y;
	*y = tmp;
}
int getMax(dataType* a, int x, int y)
{
	if (a[x] > a[y])
	{
		return x;
	}
	else {
		return y;
	}
}
void adjustDown(dataType* a, int parent, int size)
{
	assert(a);

	while (parent < size)
	{
		int lChild = 2 * parent + 1, rChild = 2 * parent + 2, swapChild;
		if (rChild < size) {
			swapChild = getMax(a, lChild, rChild);
		}
		else {
			swapChild = lChild;
		}
		
		if (swapChild < size && a[parent] < a[swapChild]) {
			swap(&a[parent], &a[swapChild]);
			parent = swapChild;
		}
		else {
			break;
		}
	}
}
void heapSort(int* a, int n) {

	//向下调整法
	for (int i = (n - 2)/2; i >= 0; i--) {
		adjustDown(a, i, n);
	}

    int cnt = n - 1;
    while (cnt) {
	    swap(&a[0], &a[cnt]);
	    adjustDown(a, 0, cnt);
	    --cnt;
    }

}

时间复杂度

前面我们知道,通过向下调整法构建初始堆的时间复杂度为O(n),因为需要对n个元素进行一次向下调整操作。

交换堆顶元素与末尾元素并重新调整堆的过程需要重复n-1次,每次调整的时间复杂度为O(log n)。

因此,总的时间复杂度为O(n log n)。

如果排升序建小堆

在堆排序中,如果目标是进行升序排序,通常我们会选择构建大顶堆,而不是小顶堆。这是因为大顶堆的堆顶元素是最大的,通过不断地将堆顶元素与末尾元素交换并调整堆,可以逐步将序列排序为升序。

然而,如果尝试使用小顶堆来进行升序排序,步骤和弊端如下:

具体步骤

1.建堆:给定一个无序数组int a[5]={4, 10, 3, 5, 1},通过向下调整法将其构建为小顶堆int a[5]={1, 3, 4,5,10};

        1
       /  \
     3    4
     / \
   5   10

 

2.交换与调整:将小顶堆的堆顶元素(当前最小值)与数组末尾元素交换位置。

堆顶元素(1)与数组末尾元素(10)交换:      

      10
      /  \
    3    4
    / \
  5    1
由于交换后的堆顶元素可能不再是最小值,因此需要对剩余元素重新调整为小顶堆。

对应的数组表示变为:[10, 3, 4, 5, 1],此时,1已经被放到了数组最后的位置,不再参与后续堆化。

但是注意了,剩余元素[10, 3, 4, 5]需要重新调整为小顶堆。但是,不能直接通过下沉操作将3放到堆顶,因为这里的adjustDown函数是从堆顶开始与较大的子节点交换,直到找到合适的位置。在这个情况下,堆顶是10,它是当前堆中的最大值,下沉操作会将它与孩子几点钟较大的那一个孩子节点交换,也就是4,并不能把3交换上去。

正确的重新堆化过程:实际上,我们需要从最后一个非叶子节点开始(在这个例子中是4),向上逐个节点进行堆化,确保每个子树都满足小顶堆的性质。
然后,我们再将根节点(10)与其子节点中较小的一个进行交换,直到整个堆满足小顶堆的性质。
这个过程比简单地下沉操作要复杂得多,因为它可能涉及到多次交换和比较。

 

3.重复步骤:重复步骤2,每次都将新的堆顶元素(当前最小值)与数组末尾元素交换,并重新调整剩余元素为小顶堆。

弊端(与大顶堆相比)

一、效率低下

  • 小顶堆:
  1. 每次交换到末尾的是当前最小值。
  2. 需要对整个堆进行重新调整以得到下一个最小值。
  3. 迭代过程中每次都需要进行调整,导致整体效率低下。
  • 大顶堆:
  1. 每次交换到末尾的是当前最大值。
  2. 剩余元素自然形成新的堆,只需对堆顶元素进行向下调整。
  3. 每次迭代的时间复杂度相对较低。

二、稳定性问题

  • 小顶堆升序排序时,每次交换最小值可能破坏相同元素的相对顺序,稳定性问题更突出。

三、逻辑复杂性

  • 小顶堆用于升序排序反直觉。
  • 升序排序目标为从小到大排列,大顶堆堆顶为当前最大值,更符合需求。
  • 小顶堆需每次交换后进行复杂调整操作。

 

ps:在第一次学习数据结构的时候,我并没有学过堆排序。这篇博客也是本人到目前为止写的最心累的一篇博客!其实如果掌握好了前面堆的实现的内容,堆排序理解和实现起来并不难。所以前面的内容一定要理解到位!

 

 

 

 

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

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

相关文章

前海湾地铁的腾通数码大厦背后的临时免费停车点探寻

临时免费停车点&#xff1a;前海湾地铁的腾通数码大厦背后的桂湾大街&#xff0c;目前看不仅整条桂湾大街停了​车&#xff0c;而且还有工地餐点。可能是这个区域还是半工地状态&#xff0c;故暂时还不会有​罚单的情况出现。 中建三局腾讯数码大厦项目部A栋 广东省深圳市南山…

Wend看源码-Durid

项目地址 GitHub - alibaba/druid: 阿里云计算平台DataWorks(https://help.aliyun.com/document_detail/137663.html) 团队出品&#xff0c;为监控而生的数据库连接池 简介 Druid连接池是阿里巴巴开源的数据库连接池项目&#xff0c;自2011年开源以来&#xff0c;它因其卓越的…

GY302光照传感器模块详解

目录 一、引言 二、功能特点 三、工作原理 四、引脚功能 五、应用场景 六、使用方法 七、总结 一、引言 在当今科技飞速发展的时代&#xff0c;传感器技术在各个领域都发挥着至关重要的作用。光照传感器作为一种能够感知环境光照强度的设备&#xff0c;广泛应用于农业、…

分布式事务调研

目录 需求背景&#xff1a; 本地事务 分布式基本理论 1、CAP 定理 2、BASE理论 分布式事务方案 #2PC #1. 运行过程 #1.1 准备阶段 #1.2 提交阶段 #2. 存在的问题 #2.1 同步阻塞 #2.2 单点问题 #2.3 数据不一致 #2.4 太过保守 3PC #本地消息表 TCC TCC原理 …

win10系统部署RAGFLOW+Ollama教程

本篇主要基于linux服务器部署ragflowollama&#xff0c;其他操作系统稍有差异但是大体一样。 一、先决条件 CPU ≥ 4核&#xff1b; RAM ≥ 16 GB&#xff1b; 磁盘 ≥ 50 GB&#xff1b; Docker ≥ 24.0.0 & Docker Compose ≥ v2.26.1。 如果尚未在本地计算机&#xff…

自然语言处理期末试题汇总

建议自己做&#xff0c;写完再来对答案。答案可能存在极小部分错误&#xff0c;不保证一定正确。 一、选择题 1-10、C A D B D B C D A A 11-20、A A A C A B D B B A 21-30、B C C D D A C A C B 31-40、B B B C D A B B A A 41-50、B D B C A B B B B C 51-60、A D D …

Android Studio的AI工具插件使用介绍

Android Studio的AI工具插件使用介绍 一、前言 Android Studio 的 AI 工具插件具有诸多重要作用&#xff0c;以下是一些常见的方面&#xff1a; 代码生成与自动补全 代码优化与重构 代码解读 学习与知识获取 智能搜索与资源推荐实际使用中可以添加注释&#xff0c;解读某段代…

iQOO Neo10系列携三大蓝科技亮相,性能与续航全面升级

11月29日&#xff0c;iQOO Neo10系列正式登场。作为iQOO Neo系列的最新力作&#xff0c;Neo10系列不仅延续了该系列一贯的“双芯”特色&#xff0c;更在性能、续航、屏幕、影像等多个方面实现了全面升级&#xff0c;为用户带来前所未有的使用体验。此次发布的Neo10系列共有两款…

172页PPT集团数字化转型采购供应链及财务管控业务流程指南

一、供应商管理与数字化转型 1.1供应商管理数字化的重要性与挑战 重要性&#xff1a; 效率提升&#xff1a; 数字化可以提高供应商管理的效率&#xff0c;通过自动化流程减少手动操作&#xff0c;加快决策速度。透明度增强&#xff1a; 数字化工具可以提供实时数据&#xff…

springboot338it职业生涯规划系统--论文pf(论文+源码)_kaic

毕 业 设 计&#xff08;论 文&#xff09; 题目&#xff1a;it职业生涯规划系统的设计与实现 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以…

【深度学习基础】一篇入门模型评估指标(分类篇)

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;深度学习_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 前言 2. 模…

【STM32学习】TB6612FNG驱动芯片的学习,驱动电路的学习

目录 1、TB6612电机驱动芯片 1.1如下是芯片的引脚图&#xff1a; 1.2如下图是电机的控制逻辑&#xff1a; 1.3MOS管运转逻辑 1.3典型应用电路 2、H桥驱动电路 2.1、单极模式 2.2、双极模式 2.3、高低端MOS管导通条件 2.4、H桥电路设计 2.5、自举电路 3、电气特性 3…

STM32 HAL库开发学习3.STM32启动浅析

STM32 HAL库开发学习3.STM32启动浅析 一、STM32启动模式&#xff08;也称自举模式&#xff09;1. MSP与PC指针赋值2. F1系列的启动模式&#xff1a;3. F4系列启动模式4. F7系列启动模式5. H7系列启动模式 二、STM32启动过程1. MSP 栈顶地址2. PC值3. Reset_Handler4. 启动文件内…

FCBP 认证考试要点摘要

理论知识 数据处理与分析&#xff1a;包括数据的收集、清洗、转换、存储等基础操作&#xff0c;以及数据分析方法&#xff0c;如描述性统计分析、相关性分析、数据挖掘算法等的理解和应用 。数据可视化&#xff1a;涉及图表类型的选择与应用&#xff0c;如柱状图、折线图、饼图…

xv6前置知识

fork函数 一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。 一个进程调用fork()函数后,系统先给新的进程分…

ECharts柱状图-极坐标系下的堆叠柱状图,附视频讲解与代码下载

引言&#xff1a; 在数据可视化的世界里&#xff0c;ECharts凭借其丰富的图表类型和强大的配置能力&#xff0c;成为了众多开发者的首选。今天&#xff0c;我将带大家一起实现一个柱状图图表&#xff0c;通过该图表我们可以直观地展示和分析数据。此外&#xff0c;我还将提供…

监控视频汇聚平台:Liveweb视频监控管理平台方案详细介绍

Liveweb国标视频综合管理平台是一款以视频为核心的智慧物联应用平台。它基于分布式、负载均衡等流媒体技术进行开发&#xff0c;提供广泛兼容、安全可靠、开放共享的视频综合服务。该平台具备多种功能&#xff0c;包括视频直播、录像、回放、检索、云存储、告警上报、语音对讲、…

MySQL源码编译

华子目录 下载源码包上传并解压安装cmake环境检测make编译make install安装 部署复制编译文件到别的主机上 下载源码包 下载相应源码包mysql5.7编译安装需要boost库&#xff0c;这里官网下载含boost的源码包https://downloads.mysql.com/archives/community/ 上传并解压 [roo…

请求(request)

目录 前言 request概述 request的使用 获取前端传递的数据 实例 请求转发 特点 语法 实例 实例1 实例2 【关联实例1】 域对象 组成 作用范围&#xff1a; 生命周期&#xff1a; 使用场景&#xff1a; 使用步骤 存储数据对象 获得数据对象 移除域中的键值…

推荐学习笔记:矩阵补充和矩阵分解

参考&#xff1a; 召回 fun-rec/docs/ch02/ch2.1/ch2.1.1/mf.md at master datawhalechina/fun-rec GitHub 业务 隐语义模型与矩阵分解 协同过滤算法的特点&#xff1a; 协同过滤算法的特点就是完全没有利用到物品本身或者是用户自身的属性&#xff0c; 仅仅利用了用户与…