C/C++排序算法(二) —— 选择排序和堆排序

news2024/12/24 7:41:08

文章目录

  • 前言
  • 1. 直接选择排序
    • 🍑 基本思想
    • 🍑 具体步骤
    • 🍑 具体步骤
    • 🍑 动图演示
    • 🍑 代码实现
    • 🍑 代码升级
    • 🍑 特性总结
  • 2. 堆排序
    • 🍑 向下调整算法
    • 🍑 任意树调整为堆的思想
    • 🍑 堆排序
    • 🍑 动图演示
    • 🍑 完整代码
    • 🍑 特性总结
  • 3. 总结


前言

今天我们将学习排序算法中的 直接选择排序堆排序,它们的本质就是在选择,所以这两个可以统称为 选择排序

1. 直接选择排序

🍑 基本思想

选择排序(Selection-sort)是一种简单直观的排序算法。

它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

🍑 具体步骤

相信大家都上过体育课吧,一般第一节体育课的时候,老师都会让大家排好队然后按照个头从矮到高进行排序,假设有 5 名同学。

首先老师找到了个子最矮的 5 号同学,然后老师说:5 号同学,你是最矮的,跟 1 号交换一下位置!

在这里插入图片描述

这时候,老师又说:4 号同学,你是第二矮的,跟 2 号交换一下位置!

在这里插入图片描述

老师又说:2 号同学,你是第三矮的,你和 3 号交换一下位置!

在这里插入图片描述

最后,老师说:1 号同学,你是第四矮的,你和 3 号交换一下位置吧!

在这里插入图片描述

如此一来,每一轮选出最小者直接交换到左侧的思路,就是选择排序的思路。这种排序的最大优势就是省去了多余的元素交换。

在这里插入图片描述

🍑 具体步骤

算法实现:

(1)在元素集合 array[i]array[n-1] 中选择关键码最大(小)的数据元素。

(2)若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换。

(3)在剩余的 array[i]array[n-2]array[i+1]array[n-1])集合中,重复上述步骤,直到集合剩余 1 个元素。

🍑 动图演示

我们来看看选择排序的动图演示吧

在这里插入图片描述

🍑 代码实现

代码示例

//交换函数
void Swap(int* pa, int* pb) {
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

//选择排序(一次选一个数)
void SelectSort(int* a, int n)
{
	int i = 0;
	for (i = 0; i < n; i++)//i代表参与该趟选择排序的第一个元素的下标
	{
		int start = i;
		int min = start;//记录最小元素的下标
		while (start < n)
		{
			if (a[start] < a[min])
				min = start;//最小值的下标更新
			start++;
		}
		Swap(&a[i], &a[min]);//最小值与参与该趟选择排序的第一个元素交换位置
	}
}

🍑 代码升级

实际上,我们可以一趟选出两个值,一个最大值和一个最小值,然后将其放在序列开头和末尾,这样可以使选择排序的效率快一倍。

代码示例

//交换函数
void Swap(int* pa, int* pb) {
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

//直接选择排序(优化版本)
void SelectSort(int* a, int n) {
	int left = 0;
	int right = n - 1;
	while (left < right) {
		int mini = left;
		int maxi = left;
		//遍历区间:[left+1, right]
		//选出最小的和最大的,然后交换
		for (int i = left + 1; i <= right; ++i) {
			//选出最小的数
			if (a[i] < a[mini]) {
				mini = i;
			}
			//选出最大的数
			if (a[i] > a[maxi]) {
				maxi = i;
			}
		}
		Swap(&a[left], &a[mini]); //把最小的数放在最左边
		//如果left和maxi重叠,修正一下maxi即可
		if (left == maxi) {
			maxi = mini;
		}
		Swap(&a[right], &a[maxi]); //把最大的数放在最右边
		left++;
		right--;
	}
}

🍑 特性总结

  • 直接选择排序思考非常好理解,但是效率不是很好,实际中很少使用。

  • 时间复杂度: O ( N 2 ) O(N^2) O(N2)

  • 空间复杂度: O ( 1 ) O(1) O(1)

  • 稳定性:不稳定

2. 堆排序

堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序。

堆是具有以下性质的完全二叉树:

(1)每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;

(2)或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

如下图所示,就是两种堆的类型:

在这里插入图片描述

同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子:

在这里插入图片描述

该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

  • 大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

  • 小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

🍑 向下调整算法

既然要进行堆排序,那么就要建堆,而建堆的方式有两种:

  • 使用向上调整,插入数据的思想建堆,但是时间复杂度为: O ( N ∗ l o g N ) O(N*logN) O(NlogN)

  • 使用向下调整,插入数据的思想建堆,时间复杂度为: O ( N ) O(N) O(N)

所以我们这里推荐使用 堆的向下调整算法,那么建堆也是有 2 个前提的:

(1)如果需要从⼤到⼩排序,就要将其调整为小堆,那么根结点的左右子树必须都为小堆。

(2)如果需要从⼩到⼤排序,就要将其调整为大堆,那么根结点的左右子树必须都为大堆。

但是我们先了解一下什么叫做大堆,如下图:

在这里插入图片描述

注意:有一个概念别搞错了,调整大堆并不是把元素从大到小排列,而是每个根节点都比它的叶子节点大

向下调整建大堆算法的基本思想:

(1)从根结点处开始,选出左右孩子中值较大的孩子,让大的孩子与其父亲进行比较

(2)若大的孩子比父亲还大,则该孩子与其父亲的位置进行交换。并将原来大的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。

(3)若大的孩子比父亲小,则不需处理了,调整完成,整个树已经是大堆了。

如下图所示:

在这里插入图片描述

堆的向下调整算法代码

//向下调整大堆
void AdjustDownBig(HPDataType* a, size_t size, size_t root) {
	size_t parent = root;
	size_t child = 2 * parent + 1; //默认左孩子最大

	while (child < size)
	{
		//1.找出左右孩子中小的那个
		//如果右孩子存在,且右孩子小于size(元素个数),那么就把默认小的左孩子修改为右孩子
		if (child + 1 < size && a[child + 1] > a[child]) {
			++child;
		}

		//2.把小的孩子去和父亲比较,如果比父亲小,就交换
		if (a[child] > a[parent]) {
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else {
			break; //如果孩子大于等于父亲,那么直接跳出循环
		}
	}
}

使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为: h − 1 h - 1 h1 次( h 为树的高度)。

h = l o g 2 ( N + 1 ) h = log2(N+1) h=log2(N+1)N 为树的总结点数)。

所以堆的向下调整算法的时间复杂度为: O ( l o g N ) O(logN) O(logN)

🍑 任意树调整为堆的思想

上面说到,使用堆的向下调整算法需要满足其根结点的左右子树均为大堆或是小堆才行,那么如何才能将一个任意树调整为堆呢?

答案很简单,我们只需要从 倒数第一个非叶子结点 开始,从后往前,按下标,依次作为根去向下调整即可。

注意:倒数第一个非叶子结点,即为最后一个节点的父亲,也被叫做根。

如图所示:

在这里插入图片描述

建堆代码

//从倒数第一个非叶子节点开始(最后一个节点的父亲)
//n-1是最后一个节点的下标,(n-1-1)/2最后一个节点的父亲的下标
for (int i = (n - 1 - 1) / 2; i >= 0; --i) {
	AdjustDownBig(a, n, i);
}

那么建堆的时间复杂度又是多少呢?

当结点数无穷大时,完全二叉树与其层数相同的满二叉树相比较来说,它们相差的结点数可以忽略不计,所以计算时间复杂度的时候我们可以将完全二叉树看作与其层数相同的满二叉树来进行计算。

在这里插入图片描述
 
我们计算建堆过程中总共交换的次数: T ( n ) = 1 ∗ ( h − 1 ) + 2 ∗ ( h − 2 ) + . . . + 2 h − 3 ∗ 2 + 2 h − 2 ∗ 1 T(n)=1*(h-1)+2*(h-2)+...+2^{h-3}*2+2^{h-2}*1 T(n)=1(h1)+2(h2)+...+2h32+2h21
 
两边同时乘 2 得: 2 T ( n ) = 2 ∗ ( h − 1 ) + 2 2 ∗ ( h − 2 ) + . . . + 2 h − 2 ∗ 2 + 2 h − 1 ∗ 1 2T(n)=2*(h-1)+2^2*(h-2)+...+2^{h-2}*2+2^{h-1}*1 2T(n)=2(h1)+22(h2)+...+2h22+2h11
 
两式相减得: T ( n ) = 1 − h + 2 1 + 2 2 + . . . + 2 h − 2 + 2 h − 1 T(n)=1-h+2^1+2^2+...+2^{h-2}+2^{h-1} T(n)=1h+21+22+...+2h2+2h1
 
运用等比数列求和得: T ( n ) = 2 h − h − 1 T(n)=2^h-h-1 T(n)=2hh1
 
由二叉树的性质,有 N = 2 h − 1 N=2^h-1 N=2h1 h = l o g 2 ( N + 1 ) h=log_2(N+1) h=log2(N+1)
 
所以: T ( n ) = N − l o g 2 ( N + 1 ) T(n)=N-log_2(N+1) T(n)=Nlog2(N+1)
 
那么用大 O 的渐进表示法: T ( N ) = O ( N ) T(N)=O(N) T(N)=O(N)

总结一下:

  • 堆的向下调整算法的时间复杂度: O ( l o g N ) O(logN) O(logN)

  • 建堆的时间复杂度: O ( N ) O(N) O(N)

🍑 堆排序

堆排序的基本思想是:

  • 将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

  • 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

  • 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

堆排序代码

void HeapSort(int* a, int n) {
	//建堆:使用向下调整 --> O(N)
	//从倒数第一个非叶子节点开始(最后一个节点的父亲)
	//n-1是最后一个节点的下标,(n-1-1)/2最后一个节点的父亲的下标
	for (int i = (n - 1 - 1) / 2; i >= 0; --i) {
		AdjustDownBig(a, n, i);
	}

	//升序 --> 建大堆
	size_t end = n - 1; //最后一个元素的下标
	while (end > 0)
	{
		Swap(&a[0], &a[end]); //交换第一个元素和最后一个元素
		AdjustDownBig(a, end, 0);
		--end;
	}
}

🍑 动图演示

我们来看一个堆排序的动图过程吧

在这里插入图片描述

🍑 完整代码

代码实现

//调整算法里面的交换函数
void Swap(HPDataType* pa, HPDataType* pb) {
	HPDataType tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

//向下调整算法 --> 建大堆
void AdjustDownBig(HPDataType* a, size_t size, size_t root) {
	size_t parent = root;
	size_t child = 2 * parent + 1; //默认左孩子最大

	while (child < size)
	{
		//1.找出左右孩子中小的那个
		//如果右孩子存在,且右孩子小于size(元素个数),那么就把默认小的左孩子修改为右孩子
		if (child + 1 < size && a[child + 1] > a[child]) {
			++child;
		}

		//2.把小的孩子去和父亲比较,如果比父亲小,就交换
		if (a[child] > a[parent]) {
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else {
			break; //如果孩子大于等于父亲,那么直接跳出循环
		}
	}
}

//堆排序代码
void HeapSort(int* a, int n) {
	//建堆:使用向下调整 --> O(N)
	//从倒数第一个非叶子节点开始(最后一个节点的父亲)
	//n-1是最后一个节点的下标,(n-1-1)/2最后一个节点的父亲的下标
	for (int i = (n - 1 - 1) / 2; i >= 0; --i) {
		AdjustDownBig(a, n, i);
	}

	//升序 --> 建大堆
	size_t end = n - 1; //最后一个元素的下标
	while (end > 0)
	{
		Swap(&a[0], &a[end]); //交换第一个元素和最后一个元素
		AdjustDownBig(a, end, 0);
		--end;
	}
}

//主函数
int main()
{
	int a[] = { 4,2,7,8,5,1,0,6 };
	HeapSort2(a, sizeof(a) / sizeof(int));

	for (int i = 0; i < sizeof(a) / sizeof(int); ++i) {
		printf("%d ", a[i]);
	}
	return 0;
}

🍑 特性总结

堆排序是一种选择排序,整体主要由 构建初始堆 + 交换堆顶元素和末尾元素并重建堆 两部分组成。

其中构建初始堆经推导复杂度为 O ( n ) O(n) O(n),在交换并重建堆的过程中,需交换 n − 1 n-1 n1 次,而重建堆的过程中,根据完全二叉树的性质, [ l o g 2 ( n − 1 ) , l o g 2 ( n − 2 ) . . . 1 ] [log2(n-1),log2(n-2)...1] [log2(n1),log2(n2)...1] 逐步递减,近似为 n l o g n nlogn nlogn

所以堆排序时间复杂度一般认为就是O(nlogn)级。

  • 堆排序使用堆来选数,效率就高了很多。

  • 时间复杂度: O ( N ∗ l o g N ) O(N*logN) O(NlogN)

  • 空间复杂度: O ( 1 ) O(1) O(1)

  • 稳定性:不稳定

3. 总结

在这里插入图片描述

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

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

相关文章

复现篇--zi2zi

intro: 用GAN学习东亚语言字体。zi2zi(意思是从字符到字符)是最近流行的pix2pix模型在汉字上的应用和扩展。 article:https://kaonashi-tyc.github.io/2017/04/06/zi2zi.html code:https://github.com/kaonashi-tyc/zi2zi pytorch版本:https://github.com/EuphoriaYan/zi2…

JAVA求职(盘点我这些年曾经面试过的一些公司)

盘点我从毕业至今这些年面试过的一些公司&#xff0c;有些记不清了。想起了再补充。大家有没有撞上同一辆车的&#xff0c;或者有没有在里面上班的&#xff0c;评论说说感受。 文思海辉技术有限公司 招商银行软件中心(融博) 广东亿迅科技有限公司 广州博鳌纵横网络科技有限公司…

【Java基础】——面向对象:多态

【Java基础】——面向对象&#xff1a;多态一、多态性1、多态性的理解2、何为多态性&#xff1a;3、多态性的使用&#xff1a;虚拟方法调用4、多态性的使用前提&#xff1a;5、多态性的应用举例&#xff1a;6、多态性使用的注意点&#xff1a;二、object类的使用1、java.lang.O…

关于我和计算机的故事

前言 一直很懒&#xff0c;计划的每周更新三篇博客&#xff0c;至今未做到&#xff0c;看着博客和公众好少得可怜的访问量&#xff0c;难免感叹一番。 总想坚持做一些自己喜欢的事情&#xff0c;比如写作、跑步、看书。当放飞自我一段时间后&#xff0c;心间总产生满满罪恶感…

JAVA使用poi解析execl解决数值被转为科学计数法(如:手机号、身份证号、电话、等)解决方法

1、原由 大家都知道数字在EXCEL表格中存储时有两种表现形式。1.数字作为数值存储。当数字作为数值存储时&#xff0c;单元格中的数字可以参与数学运算。2.数字作为文本存储。当数字作为文本存储时,单元格中的数值不能够参与数学运算。 数字作为文本存储时&#xff0c;如果没有更…

LMZ31710RVQR直流转换器DRV5033AJQDBZRQ1传感器原理图

LMZ3 SIMPLE SWITCHER电源模块将DC/DC转换器、电感器和无源器件都集成在一个极小、极薄的 QFN 封装中&#xff0c;得到易于使用的负载点解决方案。只需3个外部组件&#xff0c;就能实现高性能和高密度负载点设计——这使得布局极其简单。LMZ3系列具有分立式负载点设计的灵活性和…

【java】Spring Boot启动流程

Spring Boot启动流程目录一、简述二、注解SpirngBootApplication注解三、启动方法1、创建SpringApplication实例1.1、WebApplicationType1.2、getBootstrapRegistryInitializersFromSpringFactories1.3、setInitializers && setListeners1.4、deduceMainApplicationCla…

基于RK3588的嵌入式linux系统开发(一)——开发环境的搭建(SDK解压与本地初始化)

1、拷贝rk3588的linux-sdk压缩包到工作目录&#xff0c;如下所示&#xff1a; 图1 拷贝rk3588的sdk到工作目录2、进入sdk目录进行MD5码的计算&#xff0c;并对比md5sum.txt文件内的值&#xff0c;确保压缩包未被修改。 图2 MD5码计算与匹配3、安装p7zip-full工具&#xff0c;并…

shell正则表达式

文章目录七、正则表达式7.1 什么是正则表达式7.2 为什么使用正则表达式7.3 如何学习正则表达式7.4 如何使用正则表达式7.5 基本正则表达式7.6 扩展正则表达式7.7 正则表达式案例七、正则表达式 7.1 什么是正则表达式 正则表达式是通过一些特殊字符的排列&#xff0c;用以查找…

【Linux 进程间通信】管道和共享内存

1.进程间通信的概念2.匿名管道匿名管道的5个特点管道是一个单向通信的通信信道&#xff1b;匿名管道作用与具有血缘关系的进程&#xff0c;常用于父子进程&#xff1b;管道是一个文件&#xff0c;生命周期随进程&#xff1b;管道自带同步机制、原子性&#xff1b;管道是面向字节…

二叉查找树的应用 —— K模型和KV模型

文章目录前言1. K模型2. KV模型&#x1f351; 构建KV模型的树&#x1f351; 英汉词典&#x1f351; 统计水果出现的次数3. 总结前言 在上一篇文章中&#xff0c;我们进行了二叉查找树的实现&#xff08;文章链接&#xff09;&#xff0c;那么今天主要探讨一下二叉查找树的应用…

阻塞队列、阻塞队列的实现原理、七种阻塞队列分析及源码解读、使用阻 塞队列来实现生产者-消费者模型

文章目录面试回答参考语术七种队列分析及源码解读ArrayBlockingQueue2.1.0 ArrayBlockingQueue分析2.1.1 ArrayBlockingQueue源码解读&#xff1a;LinkedBlockingQueue2.2.0 LinkedBlockingQueue分析2.2.1 LinkedBlockingQueue源码解读2.3 LinkedBlockingQueue 与 ArrayBlockin…

【浅学Redis】Spring Cache的基础使用

用SpringCache操作Redis缓存数据1. Spring Cache是什么2. Spring Cache 常用注释3. Spring Cache 的使用步骤4. 使用Spring Cache操作Redis1. Spring Cache是什么 Spring Cache是一个框架&#xff0c;实现了基于注解的缓存功能&#xff0c;只需要简单的加一个注解&#xff0c;…

计算机视觉框架OpenMMLab开源学习(六):语义分割基础

✨写在前面&#xff1a;强烈推荐给大家一个优秀的人工智能学习网站&#xff0c;内容包括人工智能基础、机器学习、深度学习神经网络等&#xff0c;详细介绍各部分概念及实战教程&#xff0c;通俗易懂&#xff0c;非常适合人工智能领域初学者及研究者学习。➡️点击跳转到网站。…

工地安全帽智能识别系统 YOLOv5

工地安全帽智能识别系统通过opencv深度学习技术&#xff0c;实现对现场人员的安全帽反光衣穿戴进行自动实时识别和检测。我们选择当下YOLO最新的卷积神经网络YOLOv5来进行识别检测。6月9日&#xff0c;Ultralytics公司开源了YOLOv5&#xff0c;离上一次YOLOv4发布不到50天。而且…

Allegro172版本线到铜皮不按照设定值避让的原因和解决办法

Allegro172版本线到铜皮不按照设定值避让的原因和解决办法 用Allegro做PCB设计的时候,有时会单独给某块铜皮附上线到铜皮额外再增加一个数值,如下图 在规则的基础上,额外再避让10mil 规则避让line到铜皮10.02mil 额外设置多避让10mil,避让的结果却是30.02mil,正确的是20.…

2023金三银四季跳槽季,啃完这软件测试面试题,跳槽不就稳稳的了

前言 2023年也到来了&#xff0c;接近我们所说的“金三银四”也正在执行了&#xff0c;时间晃眼就过去了&#xff0c;有的人为了2023跳槽早早做足了准备&#xff0c;有的人在临阵磨刀&#xff0c;想必屏幕前的你也想在2023年涨薪吧&#xff0c;那么问题来了&#xff0c;怎么才…

day4——与数组有关的练习

今天是学习java的第四天&#xff0c;主要内容有 给循环起一个标签数组的定义以及数组的初始化 给循环起一个标签 给循环起一个标签&#xff0c;简单的说就是给循环起一个名字&#xff0c;内部的循环可以控制外部的循环&#xff0c;外部的循环可以控制内部的循环&#xff0c;…

第四章:搭建Windows server AD域和树域

由于Windows简单一点&#xff0c;我就先搞Windows了。AD域&#xff1a;视频教程&#xff1a;https://www.bilibili.com/video/BV1f84y1G72x/在创建AD域时要把网卡配置好这是打开网卡界面的命令DNS要改成自己的&#xff0c;因为在创建域的同时也会自动创建DNS打开服务器管理器&a…

LANP架构搭建

安装Apache解压apache安装包&#xff08;httpd-2.4.17.tar.gz&#xff09;到 /usr/src/目录下面tar -zxvf /root/httpd-2.4.17.tar.gz -C /usr/src/安装httpd所需要的依赖包yum -y install zlib* openssl* apr* pcre-devel openssl*进入httpd目录&#xff0c;安装httpd所需要的…