排序篇:直接插入、希尔、直接选择和堆排序(C语言)

news2024/12/24 2:24:54

目录

前言:

一:插入排序

(1)直接插入排序

基础思路(有个印象就行,主要看单趟):

单趟排序:

完整排序:

时间复杂度分析:

(2)希尔排序

基础思路(有个印象就行,主要看单趟):

单趟排序:

完整排序:

时间复杂度分析:

二:选择排序

(1)直接选择排序

基础思路:

单趟排序:

完整排序:

时间复杂度分析:

(2)堆排序

三:性能测试


前言:

 排序的重要性:

1.数据存储和检索:对于大规模数据的存储和检索,排序可以提高检索速度。例如,当我们需要在数据库中搜索某个区间内的数据时,如果该区间已被排过序,我们可以使用二分查找算法来快速搜索,而不是一个一个元素地遍历。

2.数据分析:排序也是许多统计分析算法、机器学习算法和数据处理算法的基础。例如,在统计分析中,我们需要对大量数据进行排序后才能找到中位数或计算分位数。

3.算法优化:排序算法优化一直是计算机科学领域的研究热点,一个高效的排序算法可以缩短程序运行的时间,提升计算机系统的性能。

4.实用性:排序算法在许多现实生活中有着广泛的应用,如图书馆对书籍的分类、邮件系统对邮件的分类、商场对商品的重排等。

本文的排序都是升序,降序反过来就行

一:插入排序

(1)直接插入排序

基础思路(有个印象就行,主要看单趟):

1.将待排序的序列分为已排序区间和未排序区间,一开始已排序区间只包含第一个元素,即该序列的第一个元素。

2.对未排序的元素,用顺序依次与已排序区间的元素进行比较,找到其在已排序区间中的插入位置,并将该元素插入到已排序区间中对应的位置。

3.重复步骤 2 直到所有元素都遍历完为止。

单趟排序:

我们看下面这个有序数组a:

如果我们在后面添加一个数字5,应该如何调整为有序数组呢?

 通过观察我们知道5应该插入在4后面

①我们可以设计一个变量end记录原数组尾部下标的位置

②如果a[end]大于5,我们用变量x记录5,然后10向后覆盖5的位置,end减1

重复上一个步骤一直到找到比5小的数字或者end走到下标-1位置(这个时候整个数组后移一位),结束循环

把5插入到下标为end+1的位置(无论是找到了跳出还是未找到跳出,都是这个位置)

图解:

代码(单趟):

    //单趟只是代表了核心思想
    int end;
	int x = a[end + 1];
	//如果end < 0就代表这个数据是最小的
	while (end >= 0)
	{
		//如果大于就向后覆盖,用x保存
		if (a[end] > x)
		{
			a[end + 1] = a[end];
			end--;
		}
		//如果小于就确定了插入位置
		else
		{
			break;
		}
	}
	a[end + 1] = x;

完整排序:

完整排序的思路就是记录数组下标的end从0开始

先让前两个数字有序,然后end+1

然后让前三个数字有序,end+1

重复上述步骤一直到整个数组完成排序

图解:

代码:

//直接插入排序
void InsertSort(int* a, int n)
{
	//断言,不能传空指针
	assert(a);

	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int x = a[end + 1];
		//如果end < 0就代表这个数据是最小的
		while (end >= 0)
		{
			//如果大于就向后覆盖,用x保存
			if (a[end] > x)
			{
				a[end + 1] = a[end];
				end--;
			}
			//如果小于就确定了插入位置
			else
			{
				break;
			}
		}
		a[end + 1] = x;
	}
}

时间复杂度分析:

①直接插入排序的最好情况是,输入的序列已经是有序的,此时时间复杂度为 O(N)。最坏情况是,输入的序列是倒序的,此时时间复杂度为 O(N^2)。平均情况下的时间复杂度也为O(N^2)

我们可以通过数学方法对其时间复杂度进行分析。假设待排序的序列长度为N,则外层循环会执行N-1次,内层循环会从第二项开始逐项向前比较,直到找到该元素的插入位置或者找到第一项为止,内层循环平均需要比较一半的元素,即需要比较N/2次。因此总的时间复杂度为:T(N) = (N-1)*(N/2) = (N^2-N)/2 = O(N^2)

③可见,直接插入排序的时间复杂度是O(N^2)的,空间复杂度为O(1),它简单易懂,实现方便,尤其适合对小数列进行排序。但是,对于大规模的数据集,插入排序的性能就有些不尽如人意了,需要采用更高效的排序算法。

(2)希尔排序

希尔排序是对直接插入排序的优化

基础思路(有个印象就行,主要看单趟):

选定一个整数gap,把待排序数组中所有数据分成gap组
所有距离为gap的数据分在同一组内,并对每一组内的数据进行排序(这个过程称作预排序,可以让数组接近有序)。
然后,取不同gap重复上述分组和排序的工作。当到达gap=1时,再进行排序就完成整个数组的排序(gap为1时进行排序相当于进行了直接插入排序)。

单趟排序:

我们第一次取gap为3,对下面这个数组就行分组

这里对每一组进行排序的思路和前面的直接插入排序基本一致

只是end的变化量由1变成了gap,待插入的数据从a[end+1]变成了a[end+gap],待插入的下标位置由end+1变成了end+gap

这里一共有两种实现方式(本质是一样的)

①我们外层多加一层循环,一组一组进行排序(组数为gap)

图解:

排红色组

排绿色组(蓝色组本来就有序,只判断)

 

代码:

​
    //单趟排序只是核心思路
	gap = 3;
	//最外层——分成几组预排序
	for (i = 0; i < gap; i++)
	{
		//进行一组的预排序
		for (j = 0; j < n - gap; j += gap)
		{
			int end = j;
			int x = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > x)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
				a[end + gap] = x;
			}
		}
	}

​

②不需要分组,直接从从左向右一个个排序就行

代码:

    gap = 3;
	//进行预排序
	for (i = 0; i < n - gap; i++)
	{
		int end = i;
		int x = a[end + gap];
		while (end >= 0)
		{
			if (a[end] > x)
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			else
			{
				break;
			}
			a[end + gap] = x;
		}
	}

完整排序:

完整排序的要点主要在于控制gap的大小进行多次预排序(要确保最后一次循环gap为1)

①gap = n(n为数组元素个数),gap = gap/2

②gap = n(n为数组元素个数),gap = gap/3 + 1

(预排少,也可保证gap最后一次循环为1)

代码(本文用②):

//希尔排序
void ShellSort(int* a, int n)
{
	int gap = n;

	第一种写法,循环层数多一层
	用来控制循环
	//int i = 0;
	//int j = 0;
	//
	//while (gap > 1)
	//{
	//	gap = gap / 3 + 1;
	//	//最外层——分成几组预排序
	//	for (i = 0; i < gap; i++)
	//	{
	//		//进行一组的预排序
	//		for (j = 0; j < n - gap; j += gap)
	//		{
	//			int end = j;
	//			int x = a[end + gap];
	//			while (end >= 0)
	//			{
	//				if (a[end] > x)
	//				{
	//					a[end + gap] = a[end];
	//					end -= gap;
	//				}
	//				else
	//				{
	//					break;
	//				}
	//				a[end + gap] = x;
	//			}
	//		}
	//	}
	//}

	//第二种写法,一锅炖,比较简洁
	//用来控制循环
	int i = 0;
	//最外层——分成gap组预排序
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		//进行预排序
		for (i = 0; i < n - gap; i++)
		{
			int end = i;
			int x = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > x)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
				a[end + gap] = x;
			}
		}
	}
}

时间复杂度分析:

希尔排序的时间复杂度与增量序列(gap)的选取有很大关系。

目前没有一种增量序列的选取方法可以使希尔排序的时间复杂度一定优于O(N*logN),但是实验表明,在大多数情况下,增量序列选取为Knuth序列时(gap = gap/3 +1),希尔排序的时间复杂度约为O(N^1.3)。这个时间复杂度优于插入排序的O(N^2),但是还不如快速排序和归并排序。

理论上,希尔排序最坏情况下的时间复杂度为O(N^2),与插入排序相同。最好情况下的时间复杂度与增量序列的选取有关。

二:选择排序

(1)直接选择排序

基础思路:

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

单趟排序:

单趟排序的思路比较简单,我们可以稍微优化一下,一次遍历同时找出最大和最小,可以提高一倍的效率。

图解:

然后这里交换的时候我们会发现有一个问题,那就是end和mini(最小值下标)重叠了

最小值和最大值位置交换了,这个时候应该把mini调整为maxi


代码:

 //后续都会用到交换,封装成函数
void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}   

    int begin = 0;
    int end = n - 1;
	int maxi = begin;
	int mini = begin;

	for (int i = begin; i <= end; i++)
	{
		if (a[mini] > a[i])
		{
			mini = i;
		}
		if (a[maxi] < a[i])
		{
			maxi = i;
		}
	}

	swap(&a[maxi], &a[end]);
	// 如果end跟mini重叠,需要修正一下mini的位置
	if (mini == end)
		mini = maxi;
	swap(&a[mini], &a[begin]);

	end--;
	begin++;

完整排序:

完整排序的要点是一次遍历完成后end和begin的范围应该收缩(end-1,begin+1)

当end和begin相同时排序完成

图解:

代码:

//后续都会用到交换,封装成函数
void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

//选择排序
void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (end > begin)
	{
		int maxi = begin;
		int mini = begin;

		for (int i = begin; i <= end; i++)
		{
			if (a[mini] > a[i])
			{
				mini = i;
			}
			if (a[maxi] < a[i])
			{
				maxi = i;
			}
		}

		swap(&a[maxi], &a[end]);
		// 如果end跟mini重叠,需要修正一下mini的位置
		if (mini == end)
			mini = maxi;
		swap(&a[mini], &a[begin]);

		end--;
		begin++;
	}
}

时间复杂度分析:

直接选择排序的时间复杂度为O(N^2),其中N是待排序数组的长度。

在直接选择排序中,每个元素都需要和剩余未排序的元素进行比较

所以比较次数为 N+(N−1)+(N−2)+⋯+1

即第一轮需要进行 N-1次比较,第二轮需要进行N-2次比较

以此类推,最后一轮需要进行 1 次比较。这个等差数列的和为 N*(N-1)/2​,所以总的比较次数为 O(N^2)。

另外,直接选择排序需要进行N次交换操作,因为每次找到最小元素后都需要将其与当前位置进行交换。但是交换操作的时间复杂度相对于比较操作来说比较低,为O(N),因此不影响总的时间复杂度。

总的来说,直接选择排序虽然比较简单,但是时间复杂度不够优秀,对于大规模的数据集合,效率会比较低下。

(2)堆排序

堆排序的实现以及时间复杂度分析之前一期讲的已经比较清楚了

之前一期链接:https://blog.csdn.net/2301_76269963/article/details/130157994?spm=1001.2014.3001.5501

这里简单讲讲实现思路:

1.构建堆:从待排序的数组中构建堆,可以使用二叉堆的构建算法,从最后一个有子节点的元素开始向下调整,使其满足堆的性质。

2.查找堆顶元素:堆的堆顶元素即为最大元素(或最小元素),将它取出并放到已排序的数组的末尾。

3.重新构建最大堆:将剩余未排序的元素重新构建成最大堆(或最小堆),重复上述过程,直到所有元素都排序完毕。

代码:

//后续都会用到交换,封装成函数
void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

//向下调整
void AdjustDwon(int* a, int n, int parent)
{
	//左孩子
	int child = parent*2 + 1;

	while (child < n)
	{
		//小堆只需要改变符号就行
		//选出左右孩子中最大的一方
		//要考虑右孩子不存在的情况
		if (child+1 < n && a[child+1] > a[child])
		{
			child++;
		}

		if (a[child] > a[parent])
		{
			//交换
			swap(&a[child], &a[parent]);
			//迭代
			parent = child;
			child = parent * 2 + 1;
		}
		//如果孩子没有大于父亲,结束循环
		else
		{
			break;
		}
	}
}

//堆排序
void HeapSort(int* a, int n)
{
	int parent = (n-1-1)/2;
	//建堆
	while (parent >= 0)
	{
		AdjustDwon(a, n, parent);
		parent--;
	}
	
	//排序
	int end = n - 1;
	while (end > 0)
	{
		swap(&a[end], &a[0]);
		AdjustDwon(a, end, 0);
		end--;
	}
}

三:性能测试

代码:

// 测试排序的性能对比
void TestOP()
{
	srand(time(0));
	//随机生成十万个数
	const int N = 100000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	//5和6是给快速排序和归并排序的
	/*int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);*/

	if (a1==NULL || a2==NULL )
	{
		printf("malloc error\n");
		exit(-1);
	}
	if (a3 == NULL || a4 == NULL)
	{
		printf("malloc error\n");
		exit(-1);
	}
	/*if (a5 == NULL || a6 == NULL)
	{
		printf("malloc error\n");
		exit(-1);
	}*/

	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
		/*a5[i] = a1[i];
		a6[i] = a1[i];*/
	}

	//clock函数可以获取当前程序时间
	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();

	int begin3 = clock();
	SelectSort(a3, N);
	int end3 = clock();

	int begin4 = clock();
	HeapSort(a4, N);
	int end4 = clock();

	/*int begin5 = clock();
	QuickSort(a5, 0, N - 1);
	int end5 = clock();*/

	/*int begin6 = clock();
	MergeSort(a6, N);
	int end6 = clock();*/
	
	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("SelectSort:%d\n", end3 - begin3);
	printf("HeapSort:%d\n", end4 - begin4);
	//printf("QuickSort:%d\n", end5 - begin5);
	//printf("MergeSort:%d\n", end6 - begin6);

	free(a1);
	free(a2);
	free(a3);
	free(a4);
	//free(a5);
	//free(a6);
}


int main()
{
    //测试效率
	TestOP();
}




效率对比:

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

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

相关文章

【2023 年第十三届 MathorCup 高校数学建模挑战赛】D 题 航空安全风险分析和飞行技术评估问题 27页论文及代码

【2023 年第十三届 MathorCup 高校数学建模挑战赛】D 题 航空安全风险分析和飞行技术评估问题 27页论文及代码 1 题目 D 题 航空安全风险分析和飞行技术评估问题 飞行安全是民航运输业赖以生存和发展的基础。随着我国民航业的快速发展&#xff0c;针对飞行安全问题的研究显得…

网络编程 总结一

一、网络基础&#xff1a; 概念&#xff1a;1> 网络编程的本质就是进程间的通信&#xff0c;只不过进程分布在不同的主机上 2>在跨主机传输过程中&#xff0c;需要确定通信协议后&#xff0c;才可以通信 1. OSI体系结构&#xff08;重点&#xff09; 定义7层模型&…

针对近日ChatGPT账号大批量封禁的理性分析

文 / 高扬 这两天不太平。 3月31号&#xff0c;不少技术圈的朋友和我闲聊说&#xff0c;ChatGPT账号不能注册了。 我不以为然&#xff0c;自己有一个号足够了&#xff0c;并不关注账号注册的事情。 后面又有不少朋友和我说ChatGPT账号全部不能注册了&#xff0c;因为老美要封锁…

Java笔记_15(不可变集合、Stream流、方法引用)

Java笔记_15 一、创建不可变集合1.1、创建不可变集合的应用场景1.2、创建不可变集合的书写格式 二、Stream流2.1、体验Stream流2.2、Stream流的思想和获取Stream流2.3、Stream流的中间方法2.4、Stream流的终结方法2.5、收集方法collect2.6、练习-数字过滤2.7、练习-字符串过滤并…

Python词云图的制作与案例分享(包含 wordcloud 和 jieba库)

一、基本知识 Python 有很多可用于制作词云图的库&#xff0c;其中比较常用的有 wordcloud 和 jieba。 wordcloud 是一个用于生成词云图的 Python 库&#xff0c;其使用了 Python 的 PIL 库和 numpy 库。您可以使用 pip 命令来安装 wordcloud 库&#xff1a; pip install wo…

第12章 项目沟通管理和干系人管理

文章目录 12.1.2 沟通的方式 404沟通管理计划的编制过程12.2.2 制订沟通管理计划的工具 4114、沟通方法 12.3.2 管理沟通的工具 41312.4.2 控制沟通的技术和方法 4163、会议 12.5.1 项目干系人管理所涉及的过程 420项目干系人管理的具体内容&#xff1a;&#xff08;1&#xff…

从“青铜”到“王者”,制造企业的数字化闯关记

打过游戏的朋友可能有一个常识&#xff0c;越是精彩纷呈、奖励丰厚的副本&#xff0c;越是需要召集队友一同组团闯关。很多实体企业在数字化转型中&#xff0c;也不会单打独斗&#xff0c;一把手会先找咨询公司对企业内外情况进行调研、梳理、规划&#xff0c;提出一个顶层规划…

科学计算库—numpy随笔【五一创作】

文章目录 8.1、numpy8.1.1、为什么用 numpy&#xff1f;8.1.2、numpy 数据类型推理8.1.3、numpy 指定长度数组快速创建8.1.4、numpy 哪个是行、列&#xff1f;8.1.5、numpy 如何进行数据类型转换&#xff1f;8.1.6、numpy 有几种乘法&#xff1f;8.1.7、numpy 索引和切片操作8.…

2023年前端面试题汇总-代码输出篇

1. 异步 & 事件循环 1. 代码输出结果 const promise new Promise((resolve, reject) > {console.log(1);console.log(2); }); promise.then(() > {console.log(3); }); console.log(4); 输出结果如下&#xff1a; 1 2 4 promise.then 是微任务&#xff0c;它…

【今日重磅—国产大模型首批内测机会来了】什么是讯飞星火,如何获得内测和使用方法

♥️作者&#xff1a;白日参商 &#x1f935;‍♂️个人主页&#xff1a;白日参商主页 ♥️坚持分析平时学习到的项目以及学习到的软件开发知识&#xff0c;和大家一起努力呀&#xff01;&#xff01;&#xff01; &#x1f388;&#x1f388;加油&#xff01; 加油&#xff01…

数电实验:Quartus II 软件使用 (八进制计数器和全加器)

一、实验目的&#xff1a; 1.熟悉可编程逻辑器件的设计工具Quartus II 软件的使用。 2.熟悉FPGA开发实验系统的软件环境&#xff0c;掌握各个菜单和图标的作用和功能。 二、实验内容 &#xff08;1&#xff09;以74160实现八进制计数器为例&#xff0c;学Quartus II 软件的…

【手撕代码系列】JS手写实现Promise.all

Promise.all() 方法接收一个 Promise 对象数组作为参数&#xff0c;返回一个新的 Promise 对象。该 Promise 对象在所有的 Promise 对象都成功时才会成功&#xff0c;其中一个 Promise 对象失败时&#xff0c;则该 Promise 对象立即失败。 本篇博客将手写实现 Promise.all() 方…

Peforce(Helix) 使用快速介绍

虽然Git应该是当下使用最多的版本控管工具&#xff0c; 但曾经作为版本控管巨头的Perforce还是在持续的发展和更新中&#xff0c; 在某些企业中&#xff0c;还是作为软件的版本控管工具之一。 Helix 截止2023, Perforce 的最新版本的名称是Helix ,这个词翻译的意思是螺旋&…

【手撕代码系列】JS手写实现Promise.race

公众号&#xff1a;Code程序人生&#xff0c;分享前端所见所闻。 Promise.race() 是一个常见的 JavaScript Promise 方法&#xff0c;它接受一个 Promise 数组作为参数&#xff0c;并返回一个新的 Promise 对象。这个新的 Promise 对象在传入的 Promise 数组中&#xff0c;任意…

[架构之路-158]-《软考-系统分析师》-13-系统设计 - 高内聚低耦合详解、图解以及技术手段

目录 第1章 什么是高内聚低耦合 1.1 概念 1.2 目的 1.3 什么时候需要进行高内聚低耦合 1.4 什么系统需要关注高内聚、低耦合 第2章 分类 2.1 内聚的分类 2.2 耦合的分类 第3章 增加高内聚降低耦合度的方法 3.1 增加高内聚 3.2 降低耦合度 第1章 什么是高内聚低耦…

seurat -- 关于DE gene的讨论

实例 # 加载演示数据集 library(Seurat) library(SeuratData) pbmc <- LoadData("pbmc3k", type "pbmc3k.final")# list options for groups to perform differential expression on levels(pbmc)## [1] "Naive CD4 T" "Memory CD4 T…

Orangepi Zero2 全志H616(DHT11温湿度检测)

最近在学习Linux应用和安卓开发过程中&#xff0c;打算把Linux实现的温湿度显示安卓app上&#xff0c;于是在此之前先基于Orangepi Zero2 全志H616下的wiringPi库对DHT11进行开发&#xff0c;本文主要记录开发过程的一些问题和细节&#xff0c;主要简单通过开启线程来接收温湿度…

LeetCode1376. 通知所有员工所需的时间

【LetMeFly】1376.通知所有员工所需的时间 力扣题目链接&#xff1a;https://leetcode.cn/problems/time-needed-to-inform-all-employees/ 公司里有 n 名员工&#xff0c;每个员工的 ID 都是独一无二的&#xff0c;编号从 0 到 n - 1。公司的总负责人通过 headID 进行标识。…

QML动画分组(Grouped Animations)

通常使用的动画比一个属性的动画更加复杂。例如你想同时运行几个动画并把他们连接起来&#xff0c;或者在一个一个的运行&#xff0c;或者在两个动画之间执行一个脚本。动画分组提供了很好的帮助&#xff0c;作为命名建议可以叫做一组动画。有两种方法来分组&#xff1a;平行与…