【数据结构与算法】常见的排序算法

news2025/1/13 7:42:46

文章目录

  • 排序的概念
  • 冒泡排序(Bubble Sort)
  • 插入排序(Insert Sort)
  • 选择排序(Select Sort)
  • 希尔排序(Shell Sort)
    • 写法一
    • 写法二
  • 快速排序(Quick Sort)
    • hoare版本(左右指针法)
    • 挖坑法
      • 递归法
      • 非递归法
    • 前后指针法
  • 堆排序
  • 归并排序


排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

冒泡排序(Bubble Sort)

优点:简单,易实现,稳定且不需要额外空间
缺点:效率低
升序思路:
两两比较,相邻的两个元素比较,第一个元素比第二个大就交换。此时最大的元素在最右边,再循环这个动作(最后一个元素不进入循环),直到循环截止,该数组为有序。

动图演示如下:
在这里插入图片描述
核心代码如下:

Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n; j++)
	{
		for (int i = 0; i < n - j-1; i++)
		{
			if (a[i + 1] < a[i])
			{
				Swap(&a[i + 1], &a[i]);
			}
		}
	}
}

冒泡排序的特性总结:

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:最坏情况是O(N^2), 最好情况是O(N)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

插入排序(Insert Sort)

优点:效率略高于冒泡和选择排序,稳定
缺点:效率不高

升序思路:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列
步骤:
1.从第一个元素开始(默认第一个元素为有序)
2.取下一个元素,从已有的有序元素中从左往右扫描
3.如果该元素大于新插进来的元素,则将该元素移动到下一个位置
4.重复步骤3,直到有序元素中找到小于或等于新元素的元素
5.将新元素插入到该元素后面
6.若有序元素全都大于新元素,则将新元素插入到第一位,即下标为0的位置

动图演示如下:
在这里插入图片描述
核心代码:

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 (tmp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			//比该元素小就退出循环
			else
			{
				break;
			}
		}
		//把tmp元素插入到end后面一个(即插入到小的数后面一个)
		a[end + 1] = tmp;
	}
}

直接插入排序的特性总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1),它是一种稳定的排序算法
  4. 稳定性:稳定

选择排序(Select Sort)

优点:表现最稳定,时间复杂度永远O(N^2),不需要额外空间
缺点:稳定上讲,不稳定,效率低

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

动图演示如下:
在这里插入图片描述
但我觉得这样稍微有一丢丢慢,可以优化一下,我们可以一次选两个值,一个最小值,一个最大值,分别将其放到序列开头和末尾处。

步骤:
1.首先在序列(未排序)中找到最小的值,放到序列起始位置
2.再找序列中最大的值,放到序列末尾处
3.重复以上动作,直到所有元素有序(即begin和end相遇停止)

核心代码:

void SelectSort(int* a, int n)
{
	//记录第一个和最后一个下标
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		//保存最大值,最小值下标
		int mini = begin;
		int maxi = begin;
		//找出最大值和最小值的下标
		for (int i = begin; i <= end; i++)
		{
			//找到最小值,将mini最小值下标更新为i位置
			if (a[i] < a[mini])
			{
				mini = i;
			}
			//找到最大值,将maxi最大值下标更新为i位置
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		//此时最小值为序列开头处
		Swap(&a[mini], &a[begin]);
		//考虑到万一最大值在begin位置被mini换走,所以增加判断
		if (begin == maxi)
		{
			maxi = mini;
		}
		//此时最大值在序列末尾处
		Swap(&a[maxi], &a[end]);
		//begin往前后,不断更新
		++begin;
		//end往前走,不断更新
		--end;
	}
}

直接选择排序的特性总结:

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度:最好情况和最坏情况都是O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

希尔排序(Shell Sort)

优点:效率高于三种简单排序,不需要额外空间
缺点:不稳定

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。

步骤:
1.选择一个增量序列,通常为初始增量为数组长度的一半,然后逐步减小增量直至为1。
2.根据选定的增量对数组进行分组,每个分组包含间隔为增量的元素。
3.对每个分组进行插入排序,即将每个元素插入到正确的位置上,使得每个分组内的元素有序。
4.逐步减小增量,重复上述步骤,直到增量为1时完成最后一次插入排序。
5.最终得到一个有序数组。

动图演示如下:
在这里插入图片描述

写法一

思路图:
gap > 1 时是预排序,目的是让它接近有序
gap == 1 时是直接插入排序,目的是为了让它有序

在这里插入图片描述
核心代码:

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
	    //循环躺数
		gap = gap / 3 + 1;
		for (int j = 0; j < gap; j++)
		{
			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;
				}
			}
		}
	}
}

写法二

多组并排
核心代码:

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;
		}
	}
}

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就 会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定

时间复杂度:
在这里插入图片描述
空间复杂度:O(1)

快速排序(Quick Sort)

hoare版本(左右指针法)

步骤:
1.选出一个key,一般选最左边或者最右边的。(男左女右,这里我就选最左边的了)。
2.定义一个begin和一个end,begin从左往右走,end从右往左走。
注:若你选的key在左边,那右边的end就要先走,反之右边,则左边的begin需要先
3.假设end先走,往左走,在行走过程中,若end遇到小于key的值就停下来,这时候begin走,往右走,当begin遇到大于key的值也停下来,然后将begin和end内容交换,然后再end走…反复这样走直到begin和end相遇,将他们相遇的下标内容与key进行交换。
4.这时候key的左边都是小于key的,而key的右边都是大于key的。
5.以key为划分点,将它的左序列和右序列分别进行上面这种排序,直到左右序列都只剩一个数值,或者不存在数值,就停止。
在这里插入图片描述
核心代码:

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	int left = begin;
	int right = end;
	int keyi = begin;
	while (left < right)
	{
	    //选的key为左边,所以右边先走,右边找比key小的,&&前面的判断是为了防止left和right越界,或者说互相错过
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}
		//右边走完左边走,左边找大,找比key大的值,和上面的循环一样
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}
		//交换,目的是要把比key大的值都放key的左边,比key小的值都放key的右边
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	keyi = left;
	//区间
	//[begin,keyi-1] keyi [keyi+1,end]
	
	//递归
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

挖坑法

递归法

步骤:
1.选出一个数据,一般选最左边或者最右边的,思想与hoare版本类似,选出的数存在变量key中,此时,原来的位置就会形成一个空位坑位,所以叫做挖坑法。
2. 依旧是定义两个变量,一个往左走,一个往右走,和上面方法一样,若你的坑位在左边,右边就先走,否则左边先走。
…思路与hoare一样的
在这里插入图片描述

int PartSort2(int* a, int begin, int end)
{

	int keyi = a[begin];
	int hole = begin;
	while (begin < end)
	{
		//右边找小,填到左边的坑
		while (begin < end && a[end] >= keyi)
		{
			--end;
		}
		a[hole] = a[end];
		hole = end;
		//左边找到,填到右边的坑
		while (begin < end && a[begin] <= keyi)
		{
			++begin;
		}
		a[hole] = a[begin];
		hole = begin;
	}
	a[hole] = keyi;
	return keyi;
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;

	int keyi = PartSort2(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

非递归法

核心代码:

void QuickSortNonR(int* a, int begin, int end)
{
	ST s;
	STInit(&s);
	STPush(&s, end);
	STPush(&s, begin);

	while (!STEmpty(&s))
	{
		int left = STTop(&s);
		STPop(&s);
		int right = STTop(&s);
		STPop(&s);

		int keyi = PartSort3(a, left, right);
		// [left, keyi-1] keyi [keyi+1, right]
		if (left < keyi - 1)
		{
			STPush(&s, keyi - 1);
			STPush(&s, left);
		}

		if (keyi + 1 < right)
		{
			STPush(&s, right);
			STPush(&s, keyi + 1);
		}
	}
	STDestroy(&s);
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;

	int keyi = PartSort2(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

前后指针法

步骤:
还是一样的
1.选出一个key,选最左边的或者最右边的
2.首先定义prev指针在序列起始位置,然后定义指针cur在prev的下一个,即prev+1
3.cur遇到比key大的值,++cur
4.cur遇到比key小的值,++prev,交换prev和cur位置的值,再++cur
在这里插入图片描述
核心代码:

int PartSort3(int* a, int begin, int end)
{
	int prev = begin;
	int cur = prev + 1;
	int keyi = begin;
	while (cur <= end)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}
	Swap(&a[keyi], &a[prev]);
	keyi = prev;
	return prev;
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;

	int keyi = PartSort3(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

快速排序的特性总结:

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定

堆排序

这个排序我们之前讲过的,这里我就不再复习啦~
需要的老铁请点这里----->堆、堆排序

堆排序的特性总结:

  1. 堆排序使用堆来选数,效率就高了很多。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

步骤:
1.分解:将待排序的序列分解成若干个子序列,每个子序列包含1个元素。
2.合并:将相邻的子序列两两合并,形成新的有序子序列,直到无法再合并。
3.重复合并步骤直到得到最终的排序结果。

在这里插入图片描述
动态演示如下:

在这里插入图片描述
核心代码:

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;
	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

	//[begin,mid] [mid+1,end]归并
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin+1));
}
//归并排序
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail");
		return;
	}
	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
}

归并排序的特性总结:

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定

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

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

相关文章

### 【数据结构】线性表--顺序表(二)

文章目录 1、什么是线性表2、线性表的基本操作3、顺序表3.1、顺序表的定义3.2、顺序表的实现方式&#xff1a;静态分配3.3、顺序表的实现方式&#xff1a;动态分配3.4、顺序表的特点3.5、顺序表的初始化与插入操作3.6、顺序表的删除与查询 1、什么是线性表 ​ 线性表是具有相同…

如何自定义Markdown中插入图片的位置

工作中常常需要在VsCode下写Markdown笔记&#xff0c;在写笔记的过程中不免需要插入图片。  Markdown中插入笔记的操作往往是比较繁琐的&#xff0c;比如&#xff1a;在文档中引用本地某个文件夹下的图片&#xff0c;首先需要你先保存图片到本地路径&#xff0c;然后需要你在文…

OpenNJet,够轻更强云原生应用引擎

前言&#xff1a; 在正式介绍OpenNJet之前&#xff0c;我们先来看看它的技术架构&#xff0c;如下图所示&#xff0c;OpenNJet正是NGINX的Pro版&#xff0c;在100%兼容NGINX基础上&#xff0c;新增了动态配置加载、主动式健康检测、集群高可用、声明式API等多种强大功能。 NGIN…

如何在您的WordPress网站上安装和设置W3 Total Cache

本周有一个客户&#xff0c;购买Hostease的虚拟主机&#xff0c;询问我们的在线客服&#xff0c;如何在您的WordPress网站上安装和设置W3 Total Cache&#xff1f;我们为用户提供相关教程&#xff0c;用户很快解决了遇到的问题。在此&#xff0c;我们分享这个操作教程&#xff…

React19学习-初体验

升级react19版本 安装 npm install reactbeta react-dombeta如果使用ts则需要在package.json中添加。等正式版发布直接可以使用types/react了 "overrides": {"types/react": "npm:types-reactbeta","types/react-dom": "npm:ty…

翻工第二次 Ant Design Pro 下载,发现问题,电脑网络配置有误,魔法了

一、相关网址链接 鱼皮的用户中心项目 &#xff08;前端Ant Design Pro构建&#xff09; 语雀 ## 没有选择umi版本这一步 Issue #11144 ant-design/ant-design-pro GitHub 关于umi ui图标未显示问题_umi ui不出现-CSDN博客 二、存在问题 导致下载速度慢 本人镜像代码写…

vue + element-plus 开发中遇到的问题

1.问题之路由守卫 初写路由守卫&#xff0c;对于next()的理解不是很透彻&#xff0c;就想着都放行&#xff0c;不然看不到效果&#xff0c;结果控制台出现了警告&#xff0c;想着报黄的问题就不是问题&#xff0c;但仔细一看发现他说&#xff0c;如果再生产阶段就会失败&#x…

数据可视化训练第二天(对比Python与numpy中的ndarray的效率并且可视化表示)

绪论 千里之行始于足下&#xff1b;继续坚持 1.对比Python和numpy的性能 使用魔法指令%timeit进行对比 需求&#xff1a; 实现两个数组的加法数组 A 是 0 到 N-1 数字的平方数组 B 是 0 到 N-1 数字的立方 import numpy as np def numpy_sum(text_num):"""…

【软件测试】3.开发模型

目录 1.常见的开发模型 1.1瀑布模型 1.2螺旋模型 1.3增量模型和迭代模型 1.4敏捷模型 1.4.1特点&#xff1a; 1.5Scrum模型&#xff08;三个角色和五个重要会议&#xff09; 1.5.1三个角色&#xff1a; 1.5.2Scrum工作流程&#xff08;五个会议&#xff09; 1.6测试模…

PCIE学习(2)PCIE配置空间详解

文章目录 前言一、配置空间header二、Base Address register&#xff08;BAR&#xff09;2.1、BAR是干什么的2.2、具体实现过程BAR示例1——32bit内存地址空间请求BAR示例2——64bit内存地址空间请求 前言 图片来自&#xff1a;https://zhuanlan.zhihu.com/p/463518877 一、…

传感器—超声波雷达

声波技术 在讲述超声波雷达之前&#xff0c;先了解一下声波的概念以及超声波和声波之间的关系 什么是声波&#xff1f; 声波是物体机械振动状态&#xff08;或能量&#xff09;的传播形式。所谓振动是指物质的质点在其平衡位置附近进行的往返运动形式&#xff0c;这种振动状…

设计模式1——初步认识篇

设计模式1——初步认识篇 一、先让我们浅聊一下面向对象和设计模式。 说起设计模式&#xff0c;我第一次听到它&#xff0c;是在学习面向对象的时候。那么什么是面向对象&#xff0c;什么是设计模式&#xff0c;而且设计模式和面向对象又有什么关系呢&#xff1f; 1、什么是面…

智能创作时代:AI引领下的内容生产革命与效率提升

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

OpenHarmony 实战开发——移植通信子系统

通信子系统目前涉及Wi-Fi和蓝牙适配&#xff0c;厂商应当根据芯片自身情况进行适配。 移植指导 Wi-Fi编译文件内容如下&#xff1a; 路径&#xff1a;“foundation/communication/wifi_lite/BUILD.gn” group("wifi") {deps [ "$ohos_board_adapter_dir/ha…

【start和run的区别(面试题)及创建线程的五种写法】

线程 1.start和run的区别2.创建线程的五种写法1.继承Thread,重写run2.实现runnable&#xff0c;重写run3.继承Thread,重写run,使用匿名内部类4.实现Runnable,重写run,使用匿名内部类5.使用lambda表达式 1.start和run的区别 1.start方法内部&#xff0c;是会调用到系统api&…

用于视频大型多模态模型(Video-LMMs)的复杂视频推理和鲁棒性评估套件

1 引言 最近,大型语言模型(LLMs)在同时处理广泛的NLP任务的同时展示了令人印象深刻的推理和规划能力。因此,将它们与视觉模态集成,特别是用于视频理解任务,催生了视频大型多模态模型(Video-LMMs)。这些模型充当视觉聊天机器人,接受文本和视频作为输入,并处理各种任务,包括视频…

《2024网络安全报告》中文版

Check Point发布了《2024 年网络安全报告》&#xff0c;Check Point Research 对网络攻击数据&#xff08;包括所有地区和全球的统计数据&#xff09;进行了全面分析&#xff0c;揭示了不断变化的网络威胁形势。 ● 去年&#xff0c;全球 1/10 的机构遭遇勒索软件攻击尝试&a…

浅析扩散模型与图像生成【应用篇】(二十三)——Imagic

23. Imagic: Text-Based Real Image Editing with Diffusion Models 该文提出一种基于文本的真实图像编辑方法&#xff0c;能够根据纯文本提示&#xff0c;实现复杂的图像编辑任务&#xff0c;如改变一个或多个物体的位姿和组成&#xff0c;并且保持其他特征不变。相比于其他文…

C++基础中的存储类别

存储的类别是变量的属性之一&#xff0c;C语言定义了4种变量的存储类别&#xff0c;分别是auto变量、static变量、register变量和extern变量。以下重点介绍这几种类型。 一、auto变量 auto变量是C默认的存储类型。函数内未加存储类型说明的变量均被称为自动变量&#xff0c;即…

UBOOT介绍

一、UBOOT简介 U-boot全称 Universal Boot Loader&#xff0c;是遵循GPL条款的开放源码项目&#xff0c;uboot 是一个裸机代码&#xff0c;可以看作是一个裸机综合例程&#xff0c;执行启动内核的功能。 补充&#xff1a;GPL条款&#xff08;GNU General Public License&…