【数据结构】手撕排序

news2024/11/25 23:45:07

在这里插入图片描述
🔥博客主页 小羊失眠啦.
🎥系列专栏《C语言》 《数据结构》 《Linux》《Cpolar》
❤️感谢大家点赞👍收藏⭐评论✍️


在这里插入图片描述

文章目录

  • 一、排序的概念及其运用
    • 1.1 排序的概念
    • 1.2 常见的算法排序
  • 二、 冒泡排序
  • 三、直接插入排序
  • 四、希尔排序
  • 五、 选择排序
  • 六、各大排序算法的复杂度和稳定性

一、排序的概念及其运用

1.1 排序的概念

排序:所谓排序就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2 常见的算法排序

排序算法分为比较类排序非比较类排序,如下图所示:

在这里插入图片描述


二、 冒泡排序

  • 思想 : 在一个序列中,每次对序列中的相邻记录的键值大小进行比较,将较大(升序)或较小(降序)的记录向后移动。如此循环,大/小的记录会慢慢“”到序列的后端,整个过程就像是冒泡一样,顾称之为冒泡排序。
  • **冒泡过程:**以下是对某个序列进行冒泡排序的过程

在这里插入图片描述

可以看出,对于上面具有5个元素的无序数组,我们通过4趟的冒泡后就将其变为有序数组,每一趟冒泡后都可以使最大的数沉底。

  • **动图演示:**我们可以通过一下动图感受一下冒泡两两比较的过程:

img

  • **循环控制:**很明显,我们需要两层循环来控制冒泡排序的过程。内层循环控制当前趟的数据交换,外层循环控制冒泡排序的趟数
  • **外层循环结束条件:**由于每一趟结束后都有一个数冒到序列后端,因此对于N个数的序列来说,一共需要N-1趟(只剩一个数不需要冒泡)。
for (int i = 0; i < n - 1; i++) //外层循环,N-1趟
{
     ; 
}
  • **内层循环结束条件:**内层循环用于数据的比较。已知N个数据共需比较N-1次,由于每一趟结束后就有数据到正确的位置,下一趟需要比较的数据个数就会少1,因此每趟的比较次数随着趟数的增加呈递减趋势,初始为N-1次
for (int i = 0; i < n - 1; i++) //外层循环,N-1趟
{
	for (int j = 0; j < n - 1 - i; j++) //内层循环,次数随趟数增加而递减,初始为N-1
	{
           ;
	}
}
  • 完整代码:
void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

void BubblingSort(int* a, int n)
{

	for (int i = 0; i < n - 1; i++) //外层循环,N-1趟
	{
		for (int j = 0; j < n - 1 - i; j++) //内层循环,次数随趟数增加而递减,初始为N-1
		{
			if (a[j] > a[j + 1]) //升序排列,较大的往后移
			{
				swap(&a[j], &a[j + 1]); //交换
			}
		}
	}
}
  • **改进优化:**上面的代码还存在着改进空间,我们来看下面两个情景:

在这里插入图片描述

对于情境1,我们只需一趟冒泡即可让数组有序,而如果按照上面的代码,我们依旧要进行4趟的冒泡,即有三趟是无效的。

情境2就更夸张了,数组已经有序,我们却傻乎乎的做了4趟无效冒泡。无疑是非常浪费时间的。


考虑到这些情况,我们提出了优化方案在每趟结束后判断一下当前趟是否发生了元素交换,如果没有,则说明序列已经有序了,及时止损,反之继续。优化后的代码如下:

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

void BubblingSort(int* a, int n)
{

	for (int i = 0; i < n - 1; i++) //外层循环,N-1趟
	{
		int flag = 0;
		for (int j = 0; j < n - 1 - i; j++) //内层循环,次数随趟数增加而递减,初始为N-1
		{
			if (a[j] > a[j + 1]) //升序排列,较大的往后移
			{
				swap(&a[j], &a[j + 1]); //交换
				flag = 1;
			}
		}
		if (flag == 0)
			break;
	}
}
  • 时间/空间复杂度:结合上面的图片和代码我们可以看出,总共N-1趟,每趟N-1,N-2…次比较,共比较 (N-1) + (N-2) + (N-3) + (N-4) + (N-5) + … + 1次,时间复杂度为O(N^2);而由于没有额外的辅助空间,空间复杂度为O(1)。
  • 稳定性分析:由于我们是将较大的或较小的进行交换,当两个数相等时并不会进行交换,因而不会改变相同元素的先后次序,所以冒泡排序是稳定的排序。

三、直接插入排序

  • 基本思想

​ 插入排序的基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

在这里插入图片描述

  • 过程分析

​ 我们可以将第一个元素作为一个有序序列,从第二个元素开始插入到这个有序序列中,过程如下:

以升序为例

当插入第i个元素时(i>=1),说明前面的array[0],array[1],…,array[i-1]已经排好序,我们将array[i]位置的值从后往前与array[i-1],array[i-2],…依次进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移


为什么不从前往后开始进行比较?

如果我们从前往后进行比较,当找到插入位置时,根据顺序表的插入我们知道:必须先将后面的元素全部后移,而后移又不能从前往后移,否则会造成元素覆盖,我们必须从后往前移动。折腾了半天又要从后往前遍历,那为什么不一开始就从后往前比较,在比较的同时一并挪动元素,何乐而不为呢?

单趟插入动图:

img

全过程动图:

img

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > tmp)
				a[end + 1] = a[end];
			else
				break;
			end--;
		}
		a[end + 1] = tmp;
	}
}

复杂度/稳定性分析
复杂度:

根据上面的动图我们可以发现,当元素集合越接近有序,直接插入的时间效率越高,当元素集合已经有序时,时间复杂度便是O(N),即遍历一遍数组(最优情况)。

而时间复杂度我们看的是最坏情况,即数组元素逆序的情况。此时每次插入相当于头插,而头插的时间复杂度为O(N),因此总时间复杂度为O( 插入次数 * 单次插入时间复杂度 ) = O(N^2)。

而除了待排序数组之外只有常数级的辅助空间,空间复杂度为O(1)

稳定性:

由于我们是大于tmp才进行元素挪动,当等于tmp时直接将tmp放到后方,不会对相同元素的顺序进行改变,因此直接插入排序是稳定的排序


四、希尔排序

  • 基本思想
    希尔排序又称缩小增量法,其基本思想是:选出一个整数gap表示增量,根据gap将待排序的记录分为gap组,所有距离为gap的记录分在同一组,然后对每一组进行直接插入排序随着排序次数的增多,增量gap逐渐减少,当gap=1时,即所有记录分在同一组进行直接插入排序,排序完成后序列便有序了,算法结束。分组方法如下:

在这里插入图片描述

在这里插入图片描述

过程分析:

  1. 首先,我们需要对gap的初始值进行确定,这并没有一个确定的答案,一般是按照数据量来进行选取。一般我们选取gap = n/3 + 1或者gap = n/2作为gap的初始值。
  2. 根据算法的思想,我们观察到gap是在不断地进行缩小,最后缩小到1进行直接插入排序。因此我们gap的缩小增量可以继续使用gap = gap/3 + 1gap = n/2来表示。
  3. 对1、2点进行合并后,我们可以这样用来表示gap的迭代更新:
int gap = n; //n为序列元素个数
while(gap > 1)
{
    //gap = gap/2;
    gap = gap/3 + 1;
 
    //每组进行直接插入排序
    //...
}

上面的循环在以下两种情况下会结束:

  • n == 1:即序列只有一个元素,此时无需进行排序,不会进入循环
  • n != 1 ,gap == 1:由于gap的更新是在插入排序之前,因此当循环判断到gap == 1时,上一次进行的就是以1为gap增量的直接插入排序,此时序列已经有序,退出循环。

为什么要取gap = gap/3 + 1而不是gap = gap/3?

由于最后gap要缩小到1进行直接插入排序,而如果我们选取gap = gap/3时,假设gap初始为6,第一次更新后gap=2,第二次更新后gap=0(向下取整),循环便结束了,并不会进行gap=1时的插入排序。因此,为了避免这种情况的发生,我们让gap = gap/3 + 1保证最后一次gap一定为1


那为什么取gap = gap/2而不是gap = gap/2 + 1?

这种情况不需要处理的原因是gap不可能等于0,因为进入循环的条件是gap>1,而gap只有等于0或1时gap/2才会为0。因此,无论gap初始为多少,最后一定都会在gap=1处停下。并且,当gap=2时,使用gap = gap/2 + 1会出现死循环/font>噢

  1. 每组之间进行的就是我们上面介绍的直接插入排序,不一样的是相邻元素的距离是gap而不是1。以下是gap = 3时的单趟希尔排序的动图过程:

img

  1. 下面是希尔排序的全过程(以gap = gap/3 + 1为例):

img

  1. 通过观察,我们可以发现实际上希尔排序就是直接插入排序的优化版。随着每一趟的分组排序,序列越来越接近有序。前面我们说过,直接插入排序在序列越接近有序的情况下效率越高,希尔排序就是通过每趟的预排序来使得序列越来越接近有序,从而提高直接插入排序的整体效率。
  2. 要点提炼:当gap > 1时,对序列进行分组预排序,目的是使得序列越来越接近有序;当gap == 1时,数组接近有序,此时进行直接插入排序效率大幅提高。
//写法1:一组一组排
void ShellSort1(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		//1:gap > 1,预排序
		//2:gap == 1,直接插入排序
		gap = gap / 3 + 1; //缩小增量
		for (int j = 0; j < gap; j++) //一组一组进行插入排序,共gap组
		{
			//对当前组进行直接插入排序,组内相邻元素相距gap。
			//(其实就相当于把上面介绍的直接插入排序代码中的-1改成-gap即可)
			for (int i = j; i < n - gap; i += gap)
			{
				int end = i;
				int tmp = a[end + gap];
				while (end >= 0)
				{
					if (tmp < a[end])
					{
						a[end + gap] = a[end];
						end -= gap;
					}
					else
					{
						break;
					}
				}
				a[end + gap] = tmp;
			}
 
		}
 
	}
}

但是,上面的代码实际上还可以写得更加简洁

我们知道每个组的元素都相距gap,而组与组之间距离都为1。那么,我们实际上不用一组一组分开排,而是采用多组并排的方式,这样就可以少写一个for循环,代码如下

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

复杂度/稳定性分析

复杂度:

​ 尽管上面的代码有多个循环嵌套,但这并不意味着希尔排序的效率低下。我们根据代码来分析一下希尔排序的时间复杂度,过程如下图所示:

在这里插入图片描述

我们可以看到,在gap很大或者gap很小的情况下,每趟排序的时间复杂度为O(N),共进行log3n趟,那我们是不是可以认为希尔排序的时间复杂度为O(NlogN)?实际上并不行,因为当gap处于中间的过程时,时间复杂度的分析实际上是个很复杂的数学问题。每一趟预排序之后都对下一趟排序造成影响,这就好比叠buff的过程。

在这里插入图片描述

以下分别是两本书中对希尔排序时间复杂度的说法:

1、《数据结构(C语言版)》— 严蔚敏

在这里插入图片描述

2、《数据结构-用面相对象方法与C++描述》— 殷人昆
在这里插入图片描述

因为我们上面的gap是按照Knuth提出的方式取值的,并且Knuth进行了大量的试验统计,时间复杂度我们就按照:O(n^1.25)到O(1.6n^1.25)来进行取值。

然后就是空间复杂度,由于我们依旧只用到了常数级的辅助变量,因此空间复杂度为O(1)

稳定性:

由于希尔排序是分组进行排序,当相同的数被分到不同组时,很可能就会改变相同的数的顺序,因此,希尔排序是不稳定的排序

在这里插入图片描述


五、 选择排序

  • 思想 : 每一次从待排序的数据元素中选出最小(升序)或最大(降序)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
  • **选择过程:**以下是对某个序列进行选择排序的过程:

在这里插入图片描述

  • **动图演示:**我们一样通过动图感受一下选择排序的过程:

img

  • **循环控制:**类似的,我们需要两层循环来控制选择排序的过程。内层循环遍历序列找出最大/最小值,外层循环控制选择的次数
  • **外层循环结束条件:**每一次遍历完都可以选出一个数换到起始位置,一共N个数,故要选N-1次(最后一个数不需要选择)
for (int i = 0; i < n-1; i++) //外层循环,共要选择n-1次
{
      ;
}
  • **内层循环结束条件:**内层循环通过比较进行选数,一开始N个数需要比较N-1次,然后每趟结束后下一次选择的起始位置就往后移动一位,比较次数减1
for (int i = 0; i < n-1; i++) //外层循环,共选择n-1次
{
	for (int j = i + 1; j < n; j++) //内层循环,起始位置开始向后进行比较,选最小值
	{
           ;
	}
}
  • 完整代码:
void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}


void SelectSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++) //外层循环,共选择n-1次
	{
		int mini = i; //记录最小值的下标,初始为第一个数下标
		for (int j = i + 1; j < n; j++) //内层循环,起始位置开始向后进行比较,选最小值
		{
			if (a[mini] > a[j]) //比最小值小,交换下标
			{
				mini = j;
			}
		}
		swap(&a[mini], &a[i]); //将最小值与起始位置的数据互换
	}
}
  • **时间/空间复杂度:**一共选了N-1次,每次选择需要比较N-1,N-2,N-3…次,加起来和冒泡一样时间复杂度O(N);没有用到辅助空间,空间复杂度O(1)
  • **稳定性分析:**由于是选数交换,在交换的过程中很可能会打乱相同元素的顺序,例如下面这个例子:

在这里插入图片描述

我们发现,在第一趟交换中,黑5被交换到了红5后面,在整个排序结束后,黑5依然在红5的后方,与最开始的顺序不一致。由此我们可以得出,选择排序是不稳定的排序。


六、各大排序算法的复杂度和稳定性

排序算法时间复杂度(最好)时间复杂度(平均)时间复杂度(最坏)空间复杂度稳定性数据敏感度
冒泡排序O(N)O(N^{2})O(N^{2})O(1)稳定
选择排序O(N^{2})O(N^{2})O(N^{2})O(1)不稳定
直接插入排序O(N)O(N^{2})O(N^{2})O(1)稳定
希尔排序O(N^{1.3})O(NlogN)\sim O(N^{2})O(N^{2})O(1)不稳定
堆排序O(NlogN)O(NlogN)O(NlogN)O(1)不稳定
快速排序O(NlogN)O(NlogN)O(N^{2})O(1)不稳定
归并排序O(NlogN)O(NlogN)O(NlogN)O(N)稳定

本次的内容到这里就结束啦。希望大家阅读完可以有所收获,同时也感谢各位铁汁们的支持。文章有任何问题可以在评论区留言,小羊一定认真修改,写出更好的文章~~

在这里插入图片描述

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

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

相关文章

【C语言】程序设计加密解密

&#x1f6a9;write in front&#x1f6a9; &#x1f50e; 介绍&#xff1a;"謓泽"正在路上朝着"攻城狮"方向"前进四" &#x1f50e;&#x1f3c5; 荣誉&#xff1a;2021|2022年度博客之星物联网与嵌入式开发TOP5|TOP4、2021|2222年获评百大博…

系列学习前端之第 3 章:一文精通 css

全套学习 HTMLCSSJavaScript 代码和笔记请下载网盘的资料&#xff1a; 链接: 百度网盘 请输入提取码 提取码: 6666 一、CSS基础 1. CSS简介 CSS 的全称为&#xff1a;层叠样式表 ( Cascading Style Sheets ) 。 CSS 也是一种标记语言&#xff0c;用于给 HTML 结构设…

电脑搜不自己的手机热点,其余热点均可!

一、现象&#xff1a; 之前可正常连接&#xff0c;突然间发现收不到自己的WiFi信号&#xff0c;其余人均可收到。通过重复手机电脑关机、改变热点设置中的频段等方式均没解决&#xff0c;同事电脑和手机可搜索到我的WiFi。 二、问题&#xff1a; WiF驱动程序更新 三&#x…

js vue 输入正确手机号/邮箱后,激活“发送验证码”按钮

按钮禁止点击状态&#xff1a; 按钮能够点击状态&#xff1a; 我采用的方式是监听手机号/邮箱输入框的输入事件&#xff0c;即实判断用户输入的数据是否满足规则&#xff0c;如果满足手机号/邮箱规则&#xff0c;则激活“获取验证码”按钮。 话不多说&#xff0c;上代码 样式…

数据结构与算法-D7栈实现及应用

顺序栈 具有顺序表同样的存储结构&#xff0c;由数组定义&#xff0c;配合用数组下标表示的栈顶指针top完成操作 sqstack.h stack_creat stack_push stack_empty stack_full 1、判断栈是否为空 2、top--&#xff0c;取&#xff1a;data[top1] stack_top stack_clear stack_fre…

加密系统,您的数据安全守护者,助您远离泄露风险!

随着云计算、大数据等技术的快速发展&#xff0c;企业在技术升级&#xff0c;业务上云的同时&#xff0c;也面临着来自互联网的各类安全威胁&#xff0c;并且导致数据信息的泄露。企业数据安全问题几乎为新时代人的焦虑又添上了一把火&#xff0c;面对形形色色的网络攻击手段&a…

每日一练2023.12.7—— 情人节【PTA】

题目链接&#xff1a;L1-035 情人节 题目要求&#xff1a; 以上是朋友圈中一奇葩贴&#xff1a;“2月14情人节了&#xff0c;我决定造福大家。第2个赞和第14个赞的&#xff0c;我介绍你俩认识…………咱三吃饭…你俩请…”。现给出此贴下点赞的朋友名单&#xff0c;请你找出…

【Vue】学习路线图(VIP珍藏版)

爆肝三天,整理了下Vue的常用内容, 穿插开发过程中遇到的问题与总结, 后期不定期更新[开发遇到的问题与小结]到本文章中, 欢迎各位大佬指正~ 目录 HTML、CSS和JavaScript基础知识 HTML标记语言 HTML (HyperText Markup Language) HTML5: 最新版本的 HTML CSS样式 JavaS…

357. 统计各位数字都不同的数字个数

357. 统计各位数字都不同的数字个数https://leetcode.cn/problems/count-numbers-with-unique-digits/ 当 n 0 时&#xff0c; &#xff0c; 只有这一种选择&#xff0c; 当 n 1 时&#xff0c; , 有 种选择 当 n2 时&#xff0c; 的选择可以由两部分构成&#…

Java程序员,你掌握了多线程吗?【文末送书】

目录 摘要 01、多线程对于Java的意义 02、为什么Java工程师必须掌握多线程 03、Java多线程使用方式 04、如何学好Java多线程 参与方式&#x1f947; 推荐一个人工智能学习网站 https://www.captainbed.cn/bear 摘要 互联网的每一个角落&#xff0c;无论是大型电商平台的…

前端笔记(三)CSS 盒子模型

结构伪类选择器 基本的结构伪类选择器 可以根据元素的结构关系来查找元素 比如列标签 li&#xff0c;使用 li:first-child { background-color: green; }就可以选中第一个该标签。 <!DOCTYPE html> <html lang"en"> <head><meta charset&q…

一键搭建你的hnust请假条

hnust请假条 湖南科技大学请假条生成器 https://hnust.rick.icu/new &#xff08;直接使用&#xff09; Hnust Leave Note 去github https://github.com/rickhqh/hnust_leave_note 效果展示 界面展示效果图 v2.0 更新 vant和vue重构了整个源码同步学校新版请假条样式修复了…

Python-调试

左下角有相关的操作 断点&#xff1a;鼠标右键点击行 左下角为函数的调用栈 单步按F7

基于LSTM和N-gram序列的英文文本生成(文末送书)

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

HXDSP2441-地址空间转换

地址空间转换 地址空间本身很简单&#xff0c;但由于HXDSP2441同时有DSP核和CPU核&#xff0c;且二者寻址方式不同&#xff0c;导致编程中会有歧义。 DSP核采用字地址编址方式&#xff0c;CPU核和总线空间内都采用字节地址编址方式&#xff0c;而且文档中所涉及的所有寄存器地…

Angular 由一个bug说起之三:为什么时不时出现额外的水平/垂直滚动条?怎样能更好的防止它的出现?

目录&#xff1a; 什么是单元溢出 控制滚动条出现的属性 怎样能减少意外的滚动条出现 一、什么是单元溢出 在说到这个问题之前我们先简单阐述一下视图窗口(Viewport)和视图内容(View Content) 视图窗口简单来说就是呈现内容的视口&#xff0c;浏览器就是一个窗口&#xff…

RabbitMQ(一)概述

1 RabbitMQ 概念 RabbitMQ 是一个消息中间件&#xff1a;它接受并转发消息。你可以把它当做一个快递站点&#xff0c;当你要发送一个包裹时&#xff0c;你把你的包裹放到快递站&#xff0c;快递员最终会把你的快递送到收件人那里&#xff0c;按照这种逻辑 RabbitMQ 是一个快递站…

一文搞懂系列——你真的了解如何生成动态库了吗?

引言 动态库的编译&#xff0c;这有什么难度&#xff0c;这不是手到擒来的事情吗&#xff1f;无非不就是&#xff1a; gcc -FPIC -shared -o libxxx.so *.o *.c 我若是提出这些需求场景&#xff0c;阁下又如何应对呢&#xff1f; 动态库A依赖其他部分提供的能力。但是却不…

IT外包对中小企业的独特优势

在竞争激烈的商业环境中&#xff0c;企业的发展稍有缓慢&#xff0c;就很有可能被竞争对手快速赶超、趁机抢占市场。一些中小企业为了更好地应对市场变化和提高自身竞争力&#xff0c;越来越多地转向了IT外包服务。相较于大型企业&#xff0c;中小企业在选择IT外包时能够获得一…

SSL证书代理

众所周知&#xff0c;SSL证书已经成为当下网络安全中不可或缺的一个环节&#xff0c;对于很多开发公司来说&#xff0c;给自己的客户提供SSL证书安全服务也是最为基础的。 但是目前市面上像阿里云之类的证书服务商对于开发公司需要的证书并没有太大的一个优惠政策&#xff0c;给…