手撕排序之堆排序

news2025/4/6 5:00:54

一、概念:

什么是逻辑结构、物理结构?

逻辑结构:是我们自己想象出来的,就像内存中不存在一个真正的树

物理结构(存储结构):实际上在内存中存储的形式。

堆的逻辑结构是一颗完全二叉树

堆的物理结构是一个数组

之前讲过二叉树可以用两种结构进行表示。

第一种就是链式结构,将一个一个结点进行链接。

第二种就是用数组表示。

数组表示意味着我们就是以数组为结构进行访问,但我们可以通过父子结点的下标关系将其看成树。

下面是孩子与结点的下标关系(需要记住!

思路:

我们将数组想象成一个完全二叉树——首先第一个表示树的根,接下来两个表示根的两个孩子,数组的下面4个表示树的两个孩子的下面一层,以此类推。最后一层不满,前面的都是连续的。

但是进行了上面的步骤后,还是不能将其称为堆。

堆分为两类:

  • 大顶堆(大根堆):树中所有的父亲都大于等于孩子
  • 小顶堆(小根堆):树中所有的父亲都小于等于孩子

一道经典例题(判断一串数字是否是堆)

解题方法:

将第一个数字看作二叉树的根,再往后取两个数字当作根的左右孩子,接着再取4个数,以此类推。直至还原成一个完全二叉树,接着看是不是属于堆的两种类型,如果既不是大顶堆,也不是小顶堆,那么这一串数字就不是堆。

堆的插入:

首先堆在逻辑结构上是一颗二叉树,但逻辑结构是我们自己想象出来的,本质上数据还是存储在数组中,所以我们应该对数组进行修改。

这里的插入我们选择尾插,尾插后有一下几种情况:

1、

直接尾插,不用改变任何顺序

2、

发现尾插顺序不满足大堆或者小堆,记住插入只影响自己的祖先,与其他的祖先没有关系!

所以我们只要改变孩子与祖先的关系,如何根据孩子找到父亲的下标呢?

parent = (child-1)/ 2 即可。

将这两个位置进行交换。

3、最坏的情况

最坏情况下可能会一直到根节点

因此每次交换完,都要进行孩子与父亲的比较

时间复杂度:

我们看最坏的情况,执行次数就是树的高度次,也就是O(logN).

原码:

Swap(HeapDataType* a, HeapDataType* b)
{
	HeapDataType tmp = *a;
	*a = *b;
	*b = tmp;
}

void AdujstUp(HeapDataType* a,int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			break;
	}
}


void HeapPush(HP* php,HeapDataType x)
{
	assert(php);
	//判断是否需要扩容
	if (php->capcity == php->size)
	{
		int newCapcity = php->capcity == 0 ? 4 : php->capcity * 2;
		//注意realloc的使用方法
		HeapDataType* tmp = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * newCapcity);
		if (tmp == NULL)
		{
			perror(realloc);
			exit(-1);
		}
		php->a = tmp;
		php->capcity = newCapcity;
	}
	php->a[php->size] = x;
	php->size++;
	//向上调整算法,跟祖先进行比较
	AdujstUp(php->a, php->size-1);
}

堆的删除:

首先我们需要明确针对堆的删除,我们需要删除的是堆的根节点

思路一(错误)

我们直接将数组的首元素删除,然后移动(memmove)后面的数据内容,但这样极大可能影响了大小堆的结构!

思路二:(向下调整算法)

我们直接将数组的首元素和数组的最后一个元素交换位置,然后size--,删除最后一个元素,也就是根节点。

这时候我们发现根节点的左右子树都是大堆/小堆

然后将根节点与左右节点的较小结点进行比较,如果还小,那么继续交换,直到叶节点为止

以此类推,堆顶的元素是最小的,继续pop,那么次小的元素又到堆顶……

原码:

void AjustDown(HeapDataType* a,int n,int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//确定最小孩子
		if (child + 1 < n && a[child] < a[child + 1])//防止只有左叶子,访问右叶子会越界
			child++;
		if (a[parent] < a[child])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
}

void HeapPop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	AjustDown(php->a,php->size,0);
}

小总结:(调整算法的前提)

向上调整算法的前提是:前面的数据内容都是大堆/小堆

向下调整算法的前提是:左右子树的数据内容都是大堆/小堆

TIP:

我们可以根据这个思路,可以实现一个排序算法,也就是堆排序。

时间复杂度:

跟插入一样,都是O(logN)。

堆排序:

1、建堆:

我们可以直接用向上寻找算法进行建堆。——为什么?

建堆有两种方式

第一种就是向上建堆。

利用向上调整算法,每一个插入然后进行向上调整,完成建堆

时间复杂度:O(N) = N*logN

解析:

我们采取最差情况,得到O(h)的表达式,然后再用等式将h替换成n

总体的时间复杂度是O(N) = N*logN

第二种就是向下建堆。

从倒数第一个非叶子结点开始跳(也就是最后一个结点的父亲)

时间复杂度:O(N)

解析:

先假设h是树的高度,N是结点的个数

我们先用树的高度h作为自变量便于计算。最后再用等式进行替换。

考虑最坏的情况:

每层数据的个数 * 向下移动的层数

然后是等差*等比的数列求和,我们采用错位相减法计算。

最后的计算结果是2^h - 1- h.

因为2^h - 1 = n.

所以O(N) = N - log(N+1)

为什么向下调整算法要比向上调整复杂度低呢?

因为向下调整层数越低,向下调整的次数越多,所以是低*高

而向上调整层数越高,向上调整的次数就越多,所以是高*高,并且层数越高,占比越大,最后一层的结点个数就占了50%。

最后一层结点个数多并且向上调整的次数也多

2、排序

如果我们排升序,建大堆

排降序,建小堆

原因:

 建小堆有可能关系全乱了,剩下数据,看成新的完全二叉树,不一定是堆,重新建堆代价太大!

例子:

建大堆:

堆顶跟最后一个位置交换,最大的数据排好了,然后将最后一个元素不列入排序,剩下元素除了根节点其余是堆。剩下的数据由堆顶元素进行向下调整算法,选出次大的,代价是logN。

注意向上调整算法和向下调整算法的前提都是保证数据是堆!

以下是建小堆的堆排序算法

所以一共的时间复杂度O(N) = N*logN.

原码:

void HeapSort(int* a, int n)
{
	//建堆
	//建小堆/大堆

	//向上调整建堆
	//O(N*logN)
	/*for (int i = 1; i < n; i++)
	{
		AdujstUp(a, i);
	}*/
	//向下调整建堆
	//O(N),效率比向上调整高
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)//注意这里的n是数据个数,而公式中的n是下标
	{
		AjustDown(a, n, i);
	}
	//根节点的值要么最大,要么最小,可以进行排序
	//这一部分的时间复杂度O(N) = N*logN
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AjustDown(a, end, 0);
		end--;
	}
}

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

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

相关文章

常见请求方法

请求方法的本质 请求方法是请求行中的第一个单词&#xff0c;它向服务器描述了客户端发出请求的动作类型。在 HTTP 协议中&#xff0c;不同的请求方法只是包含了不同的语义&#xff0c;但服务器和浏览器的一些约定俗成的行为造成了它们具体的区别 fetch(https://www.baidu.com…

【算法|双指针|链表】反转链表

Leetcode206 反转链表 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1]示例 2&#xff1a; 输入&#xff1a;head [1,2] 输出&#xff1a;[2,1]示例…

opencv 处理扫描件移除灰色背景图

先看对比效果: 再上代码: import cv2 import numpy as npdef remove_gray_background(input_image_path, output_image_path, threshold180):# Load the input imageimage cv2.imread(input_image_path)# Convert the image to grayscalegray cv2.cvtColor(image, cv2.COLO…

【数据可视化】动态条形图Python代码实现

使用 Python 中的 bar_chart_race_cn 库创建动态条形图 前言 数据可视化在今天的数据分析和传达信息中起着至关重要的作用。动态条形图是一种强大的数据可视化工具&#xff0c;可以帮助我们展示随时间变化的数据趋势。本文将介绍如何使用 Python 编程语言中的 bar_chart_race…

阿里云yum源和tuna源

阿里云开源镜像站地址&#xff1a;阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区阿里巴巴开源镜像站&#xff0c;免费提供Linux镜像下载服务&#xff0c;拥有Ubuntu、CentOS、Deepin、MongoDB、Apache、Maven、Composer等多种开源软件镜像源&#xff0c;此外还提供域名解析D…

背包问题学习笔记-01背包

背景 背包问题是动态规划问题中的一个大类&#xff0c;学习背包问题对于掌握动态规划十分重要。背包问题也很容易成为程序员算法面试中的一个槛&#xff0c;但其实背包问题已经被研究&#xff0c;讲解的比较成熟了&#xff0c;在这些丰富的讲解资料的基础之上&#xff0c;大家…

图注意网络(GAT)的可视化实现详解

能够可视化的查看对于理解图神经网络(gnn)越来越重要&#xff0c;所以在这篇文章中&#xff0c;我将介绍传统GNN层的实现&#xff0c;然后展示ICLR论文“图注意力网络”中对传统GNN层的改进。 假设我们有一个表示为有向无环图(DAG)的文本文档图。文档0与文档1、2和3有一条边&am…

第72步 时间序列建模实战:单步滚动预测(以决策树回归为例)

基于WIN10的64位系统演示 一、写在前面 从这一期开始&#xff0c;我们开始基于python构建各种机器学习和深度学习的时间序列预测模型&#xff0c;本质上就是调用各种模型的回归分析的属性。所以很多模型其实之前都介绍过&#xff0c;比如说决策树、SVM等等。 同样&#xff0…

【踩坑篇】代码中使用 Long 作为 Map的Key存在的问题

本周的工作结束&#xff0c;详述一些在项目代码中实际遇到的一些坑。 代码中遇到这样一个场景&#xff1a; 有个业务接口&#xff0c;接口返回的值是一个JSON格式的字符串&#xff0c;通过JSON解析的方式&#xff0c;解析为格式为&#xff1a; Map<Long, Map<String, O…

数据结构——时间复杂度与空间复杂度

目录 一.什么是空间复杂度与时间复杂度 1.1算法效率 1.2时间复杂度的概念 1.3空间复杂度的概念 二.如何计算常见算法的时间复杂度 2.1大O的渐近表示法 使用规则 三.如何计算常见算法的空间复杂度 3.1 大O渐近表示法 3.2 面试题——消失的数字 3.3 面试题——旋转数组 一…

ChatGPT是留学生的论文神器还是学术不端的罪魁祸首?

当今时代&#xff0c;ChatGPT无疑是大数据和人工智能的完美结合&#xff0c;成为了搜索技术的革命性创新。几秒钟&#xff0c;一篇逻辑清晰、观点鲜明、有充分论据支持的文章即可生成。这种革命性的创新在学术界掀起了巨大的浪潮&#xff0c;甚至让全球的学校开始思考&#xff…

【GAN入门】生成 AI的概念

一、说明 GAN是生成对抗网络&#xff08;Generative Adversarial Network&#xff09;的缩写&#xff0c;是一种无监督学习算法&#xff0c;由Goodfellow等人于2014年提出。GAN由一个生成器网络和一个判别器网络组成&#xff0c;通过二者之间的对抗来训练生成器网络生成与真实样…

深入了解Python数据类型及应用

Python提供了一组丰富的内置数据类型&#xff0c;使您能够在程序中处理不同类型的数据。核心数值类型包括整数、浮点数和复数。整数表示整数&#xff0c;对于精确的计数和计算非常有用。 浮点数表示具有小数精度的实数&#xff0c;这对科学和统计计算非常重要。复数将数字扩展到…

C++系列赋值运算符重载

赋值运算符重载 类的默认函数拷贝构造函数和赋值运算符 重载赋值运算符相关注意事项 类的默认函数 一个类至少有4个默认函数&#xff1a; 默认构造函数拷贝构造函数析构函数赋值运算符重载函数 拷贝构造函数和赋值运算符 拷贝构造函数是在创建类的时候调用的&#xff0c;之…

利用PCA科学确定各个指标的权重系数

背景参考: 1、提取主成分 对样本进行PCA分析,查看不同变量贡献率,确定主要的指标。我们可以通过下列代码获取需要的所有数据: import numpy as np from sklearn.decomposition import PCA# 创建一个数据 np.random.seed(0) data = np.random.random((100,5)) y = np.ra…

深入理解CI/CD流程:改变你的开发生命周期

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

投后管理系统的主要功能及开发

投后管理系统是一种用于跟踪和管理投资组合中的投资的工具&#xff0c;通常由私募股权、风险投资公司、资产管理公司和投资者使用。其主要功能包括以下内容&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合…

全新跑分软件GeekRUN-7问世

实测非常好用CPU跑分神器点击下载 感兴趣的可以测测你的手机跑的多少。 我的峰值是7340&#xff0c;低值是4685&#xff0c;测试时后台不能有任何APP

丙烯酸共聚聚氯乙烯树脂

声明 本文是学习GB-T 42790-2023 丙烯酸共聚聚氯乙烯树脂. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本文件规定了丙烯酸共聚聚氯乙烯树脂的外观、物化性能等技术要求&#xff0c;描述了相应的采样、试验方 法、检验规则、标志、包装、…

2023年中国场馆产业研究报告

第一章 行业综述 1.1 定义与分类 场馆&#xff0c;作为一个多元化和充满活力的行业&#xff0c;为人们提供了一个为不同目的而聚集的空间。无论是为了活动、表演、展览还是聚会&#xff0c;场馆都在为社区的社会、文化和经济建设做出了不可或缺的贡献。 场馆是一个为举办各类…