常见排序算法鉴赏(原理剖析+动图演示)

目录

一、冒泡排序(BubbleSort)

二、选择排序( SelectSort)

三、插入排序(InsertSort)

四、希尔排序(ShellSort)

五、堆排序

六、快排(QuickSort)

Hoare版本

前后指针法

优化之三数取中

七、归并排序(MergeSort)

八、计数排序(CountSoort)

九、小结


注:本文排序都是升序

一、冒泡排序(BubbleSort)

通过连续地比较与交换相邻元素实现排序,这个过程就像气泡从底部升到顶部一样, 因此得名冒泡排序。

冒泡过程可以利用元素交换操作来模拟:从数组最左端开始向右遍历,依次比较相邻元素大 小,如果“左元素>右元素”就交换二者。遍历完成后,最大的元素会被移动到数组的最右端。

代码实现:

//冒泡排序
void BubbleSort(int* arr, size_t size) {
	for (size_t i = 0; i < size - 1; i++)
	{
		bool exchange = 0;
		for (size_t j = 0; j < size - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1]) {
				Swap(&arr[j], &arr[j + 1]);
				exchange = 1;
			}
		}
		if (exchange == 0) break;
	}
}

动图演示:

二、选择排序( SelectSort)

开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾:

  1. 初始状态下,所有元素未排序,即未排序(索引)区间为[0,𝑛−1]。
  2. 选取区间[0,𝑛−1]中的最小元素,将其与索引0处的元素交换。完成后,数组前1个元素已排序。
  3. 选取区间[1,𝑛−1]中的最小元素,将其与索引1处的元素交换。完成后,数组前2个元素已排序。
  4. 以此类推。经过𝑛−1轮选择与交换后,数组前𝑛−1个元素已排序。
  5. 仅剩的一个元素必定是最大元素,无须排序,因此数组排序完成。

代码实现:


//简单选择排序
void SelectSort1(int* arr, size_t size) {
	//外层循环:[0,size-1]
	for (size_t i = 0; i < size - 1; i++)
	{
		//记录最小值的索引
		size_t mini = i;
		//内层循环:找到未排序区间[1,size-1]的最小值
		//需要找size-1个数
		for (size_t j = i + 1; j < size; j++)
		{
			if (arr[mini] > arr[j])
				mini = j;
		}
			Swap(&arr[i], &arr[mini]);
	}
}

优化版本:

从区间两边缩小,区间最左侧放最小值,区间最右侧放最大值

void SelectSort2(int* arr, size_t size) {
	size_t left = 0, right = size - 1;

	while (left <= right) {
		//假设第一个索引的值既是最大值也是最小值
		size_t mini = left, maxi = left;
		//只需要从第二个值开始遍历 找新的最大值最小值
		for (size_t i = left + 1; i <= right; i++)
		{
			if (arr[i] > arr[maxi]) 
				maxi = i;
			if (arr[i] < arr[mini]) 
				mini = i;
		}
		//交换值
		Swap(&arr[left], &arr[mini]);
		//这里需要多个判断 如果maxi == left 两数交换两次相当于没有交换
		if (maxi == left) maxi = mini;
		Swap(&arr[right], &arr[maxi]);

		left++;
		right--;
	}
}

动图演示:

三、插入排序(InsertSort)

与手动整理一副牌的过程非常相似。 具体来说,我们在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。

代码实现:


//插入排序
void InsertSort(int* arr, int size) {
	//end走到倒数第二个就停下,无需再做比较
	for (size_t i = 0; i < size - 1; i++)
	{
		//[0,end],end+1
		int end = i;
		int insertVal = arr[end + 1];
		while (end >= 0) {
			if (insertVal < arr[end]) {
				arr[end + 1] = arr[end];
				end--;
			}
			else {
				break;
			}
		}
		arr[end + 1] = insertVal;
	}
	
}

 

动图演示:

四、希尔排序(ShellSort)

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

这里可以得到一个结论:

gap越大,数据跳跃越快,越小的越容易到前面,但是越不接近有序

gap越小,数据跳跃越慢,越小的不更易到前面,但是越接近有序

代码实现:

//希尔排序
void ShellSort(int* arr, size_t size) {
	int gap = size;
	while (gap > 1) {
		gap = gap / 3 + 1;
		for (int i = 0; i < size - gap; i++)
		{
			//[0,end],end+1
			int end = i;
			int insertVal = arr[end + gap];
			while (end >= 0) {
				if (insertVal < arr[end]) {
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else {
					break;
				}
			}
			arr[end + gap] = insertVal;
		}
	}
}

五、堆排序

流程:

1)建堆 实现升序建大根堆 实现降序建小根堆

核心原理:

堆排序通过反复提取堆顶元素(极值)实现排序,堆的类型决定了提取元素的顺序:

  • 大顶堆:堆顶始终为当前最大值
  • 小顶堆:堆顶始终为当前最小值

2) 排序 利用堆删除思想排序

注:此流程图来源 hello‑algo.com

代码实现:

//向下调整算法,参数:数组 数组元素个数 开始向下调整的父节点索引
AdjustDown(HpDataType* arr, size_t size, size_t parent) {
	//找较大的子节点,假设左子节点较大
	size_t child = 2 * parent + 1;
	//child索引不断增大,但不会超过数组元素个数
	while (child < size) {
		//假设错误,右子节点更大,更新索引
		if (child + 1 < size && arr[child] < arr[child + 1]) {
			child++;
		}
		//如果父节点小于等于子节点,交换两节点的值
		if (arr[parent] <= arr[child]) {
			Swap(&arr[parent], &arr[child]);
			//更新父节点的索引,现在变成儿子了
			parent = child;
			//继续找儿子比较
			child = 2 * parent + 1;
		}
		else {
			break;
		}
	}
}

void HeapSort(HpDataType* arr, int size) {
	// 建堆 升序建大根堆 降序建小根堆
	for (int i = (size - 2) / 2; i >= 0; i--) {
		AdjustDown(arr, size, i);
	}

	// 排序
	int end = size - 1;
	while (end > 0) {
		Swap(&arr[0], &arr[end]);  
		AdjustDown(arr, end, 0);   
		end--;
	}
}

六、快排(QuickSort)

Hoare版本

快排最早是由Hoare提出的,所以,他的经典思想我们有必要学习一下

有如下步骤:

1.基准元素选择

        选择数组第一个元素作为基准,记录基准索引keyi

2.双向指针扫描

  • 设置两个指针left和right,left从数组左端(begin)开始向右扫描,right从右端(end)开始向左扫描,一定要让右指针先走,这样才能保证较大值右移、较小值左移 
  •  right指针寻找第一个小于等于基准的元素,找到之后停止不动
  • left指针寻找第一个大于等于基准的元素,找到之后停止不动
  • 当两指针相遇或交叉时停止扫描       

3.  ​​​​​​元素交换

        当left < right时,交换arr[left]和arr[right]的值。这个交换操作能使较大值右移、较小值左移

4.分区完成判断

       重复步骤2-3直到left >= right,此时j所指位置即为当前分区的中间点。此时数组被划分           为:左区间:[begin,keyi-1]右区间:[keyi+1,end]

5.递归排序

        对左右两个子数组分别递归执行上述过程,直到子数组长度为1时终止递归

第一趟详细排序:

递归完成最后排序:

动图演示:

代码实现:

//Hoare版本快排
void QuickSort2(int* arr, int left, int right) {
	if (left >= right)
		return;
	//创建变量保存区间边界
	int keyi = left;
	int begin = left, end = right;

	while (left < right) {
		//右边找小
		while (left < right && arr[keyi] <= arr[right]) {
			--right;
		}
		//左边找大
		while (left < right && arr[keyi] >= arr[left]) {
			++left;
		}
		Swap(&arr[left], &arr[right]);
	}

	Swap(&arr[keyi], &arr[left]);
	keyi = left;

	// [begin, keyi-1]keyi[keyi+1, end]
	QuickSort2(arr, begin, keyi - 1);
	QuickSort2(arr, keyi + 1, end);
	
}

前后指针法

动图演示:

核心思路:

1.选定最左侧元素为基准值并保存,将prev设置为left,cur设置为left+1

2.cur去寻找当前区间内比基准值小的元素

3.如果没找到,说明基准值右侧的元素都是大于基准值的,不需要其他操作,直接跳出循环即可

4.如果找到了先prev++,再将prev和cur位置的元素交换,然后cur++。

5.等到cur越界之后,说明遍历完序列中的所有元素了,将基准值位置的元素和prev位置的元素交换

6.最后返回prev即可

代码实现:

//前后指针法快排
void QuickSort1(int* arr, int left, int right) {
	if (left >= right)
		return;

	int prev = left, keyi = left;
	int cur = left + 1;

	while (cur <= right) {
		if (arr[cur] < arr[keyi] && prev++ != cur) {
			Swap(&arr[prev], &arr[cur]);
		}
		cur++;
	}
	Swap(&arr[prev], &arr[keyi]);
	keyi = prev;
	//[left, keyi - 1] keyi [keyi + 1, right]
	QuickSort1(arr, left, keyi - 1);
	QuickSort1(arr, keyi + 1, right);
}

优化之三数取中

如果遇到有序序列或者是接近有序序列,快排的效率就会接近O(n^2),原因是我们每次选择keyi都是区间左值,这样可能选取到极端值(比如最小或最大元素)作为基准,这样会导致分区不平衡。

三数取中,顾名思义:取三个数中第二大的数

代码实现:

//三数取中
int ThreeNumMid(int* arr, int left, int right) {
	int mid = (left + right) / 2;

	if (arr[mid] < arr[right]) {
		if (arr[mid] > arr[left]) {
			return mid;
		}
		else if(arr[left] < arr[right]) {
			return left;
		}
		else {
			return right;
		}
	}
	else {// arr[mid] > arr[right]
		if (arr[mid] < arr[left]) {
			return mid;
		}
		else if (arr[right] < arr[left]) {
			return left;
		}
		else {
			return right;
		}
	}
}

但是如果数据量过小,我们三数取中法又显得十分累赘,这种情况下,我们可以选择更高效的排序,插入排序

优化后的代码:

//Hoare版本快排
void QuickSort2(int* arr, int left, int right) {
	if (left >= right)
		return;
	// 小区间选择走插入,可以减少90%左右的递归
	if (right - left + 1 < 10)
	{
		InsertSort(arr + left, right - left + 1);
	}
	else {
		//创建变量保存区间边界
		int midi = ThreeNumMid(arr, left, right);
		Swap(&arr[left], &arr[midi]);
		int keyi = left;
		int begin = left, end = right;

		while (left < right) {
			//右边找小
			while (left < right && arr[keyi] <= arr[right]) {
				--right;
			}
			//左边找大
			while (left < right && arr[keyi] >= arr[left]) {
				++left;
			}
			Swap(&arr[left], &arr[right]);
		}

		Swap(&arr[keyi], &arr[left]);
		keyi = left;

		// [begin, keyi-1]keyi[keyi+1, end]
		QuickSort2(arr, begin, keyi - 1);
		QuickSort2(arr, keyi + 1, end);

	}
	
	//双指针法快排
void QuickSort1(int* arr, int left, int right) {
	if (left >= right)
		return;

	// 新增三数取中逻辑
	int mid = ThreeNumMid(arr, left, right);
	Swap(&arr[left], &arr[mid]);
	int keyi = left;

	int prev = left, cur = left + 1;
	while (cur <= right) {
		if (arr[cur] < arr[keyi] && prev++ != cur) {
			Swap(&arr[prev], &arr[cur]);
		}
		cur++;
	}
	Swap(&arr[prev], &arr[keyi]);
	keyi = prev;

	QuickSort1(arr, left, keyi - 1);
	QuickSort1(arr, keyi + 1, right);
}

七、归并排序(MergeSort)

归并排序利用分治的思想,分为划分区间归并排序两个阶段

1.划分区间阶段:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题

2.归并排序阶段:当子数组长度为1时终止划分,开始归并,持续地将左右两个较短的有序数组归并为一个较长的有序数组,直至结束

如下图所示:

观察发现,归并排序与二叉树后序遍历的递归顺序是一致的。 ‧ 后序遍历:先递归左子树,再递归右子树,最后处理根节点 ‧ 归并排序:先递归左子数组,再递归右子数组,最后处理合并

代码实现:


//归并排序
void _MergeSort(int* arr, int* tmp, int begin, int end) {
	//递归到最小子区间
	if (begin == end)
		return;

	int mid = (begin + end) / 2;
	//[begin,mid] [mid+1,end]
	_MergeSort(arr, tmp, begin, mid);
	_MergeSort(arr, tmp, mid+1, end);

	//归并排序 将更大的值尾插到tmp数组中
	//分出两个区间范围值
	int left1 = begin, right1 = mid;
	int left2 = mid + 1, right2 = end;
	int i = begin;
	//判断两个区间的值谁更小
	while (left1 <= right1 && left2 <= right2) {
		if (arr[left1] < arr[left2]) {
			tmp[i++] = arr[left1++];
		}
		else {
			tmp[i++] = arr[left2++];
		}
	}
	//任何一个区间结束,将剩余的直接尾插
	while (left1 <= right1) {
		tmp[i++] = arr[left1++];
	}
	while (left2 <= right2) {
		tmp[i++] = arr[left2++];
	}

	//将排序好的tmp拷贝到arr中
	memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

void MergeSort(int* arr, size_t size) {
	int* tmp = (int*)malloc(sizeof(int) * size);
	if (tmp == NULL) {
		perror("malloc() err!");
		return;
	}
	_MergeSort(arr, tmp, 0, size - 1);
	free(tmp);
	tmp = NULL;
}

动图演示:

八、计数排序(CountSoort)

核心原理:

统计每个数出现的次数,一个数出现几次,映射的位置值就加几次

这个排序算法有一定的局限性:

  • 只适用于整型排序
  • 只适用于数据集中的数据群
  • 不适用于数据较为分散的数据群

代码实现:

//计数排序
void CountSoort(int* arr, size_t size) {
	//找最大值和最小值
	int min = arr[0], max = arr[0];
	for (size_t i = 1; i < size; i++)
	{
		if (arr[i] < min) min = arr[i];
		if (arr[i] > max) max = arr[i];
	}
	//创建临时数组用于统计次数
	int range = max - min + 1;
	int* tmp = (int*)malloc(sizeof(int) * range);
	if (tmp == NULL) {
		perror("malloc() err!");
		return;
	}
	memset(tmp, 0, sizeof(int) * range);

	//统计次数
	for (size_t i = 0; i < size; i++)
	{
		tmp[arr[i] - min]++;
	}
	//排序
	int j = 0;
	for (size_t i = 0; i < range; i++)
	{
		while (tmp[i]--) {
			arr[j++] = i + min;
		}
	}

}

动图演示:

九、小结

算法的稳定性:稳定排序在完成排序后,相等元素在数组中的相对顺序不发生改变 

 看到了这里,麻烦点个赞和关注再走吧~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

易基因特异性R-loop检测整体研究方案

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。 01.技术简述 R-loop是由DNA:RNA 杂交体和被置换的单链DNA组成的三链核酸结构&#xff0c;广泛参与基因转录、表观遗传调控及DNA修复等关键生物学过程。异常的R-loop积累会导致基因组不稳…

用低代码平台集成人工智能:无需专业开发也能实现智能化

引言&#xff1a;人工智能的普及与企业需求 随着人工智能&#xff08;AI&#xff09;技术的飞速发展&#xff0c;越来越多的企业开始意识到其在提升运营效率、优化客户体验和推动业务创新方面的巨大潜力。从智能客服到自动化决策支持&#xff0c;从数据分析到个性化推荐&#x…

原生android 打包.aar到uniapp使用

1.原生安卓里面引入uniapp官方提供的包文件&#xff1a; uniapp-v8-release.aar 2.提供uniapp调用的接口&#xff0c;新建类文件继承UniModule&#xff0c; package com.dermandar.panoramal;import com.scjt.lib.certlib;import io.dcloud.feature.uniapp.annotation.UniJSM…

K8s 1.27.1 实战系列(四)验证集群及应用部署测试

一、验证集群可用性 1、检查节点 kubectl get nodes ------------------------------------------------------ NAME STATUS ROLES AGE VERSION k8s-master Ready control-plane 3h48m v1.27.1 k8s-node1 Ready <none> …

OpenCV计算摄影学(18)平滑图像中的纹理区域同时保留边缘信息函数textureFlattening()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 cv::textureFlattening 是 OpenCV 中用于图像处理的一个函数&#xff0c;旨在平滑图像中的纹理区域&#xff0c;同时保留边缘信息。该技术特别适…

基于React.js 技术栈的服务端渲染框架Next.js 实战记录

自我简介&#xff1a;4年导游&#xff0c;10年程序员&#xff0c;最近6年一直深耕低代码领域&#xff0c;分享低代码和AI领域见解。 基于React.js 技术栈的服务端渲染框架Next.js 实战记录 本着学习的态度&#xff0c;将自己运用Next.js开发服务端渲染的项目复原总结出来&…

使用 Deepseek + kimi 快速生成PPT

前言 最近看到好多文章和视频都在说&#xff0c;使用 Deepseek 和 kimi 能快速生成精美的 ppt&#xff0c;毕竟那都是别人说的&#xff0c;只有自己尝试一次才知道结果。 具体操作 第一步&#xff1a;访问 deepseek 我们访问 deepseek &#xff0c;把我们想要输入的内容告诉…

CS144 Lab Checkpoint 1: stitching substrings into a byte stream

Putting substrings in sequence TCP报文在发送方会被分成许多数据报文&#xff0c;传输中可能出现顺序的重排以及丢失和重发等现象&#xff0c;所以需要重装数据报文到原来字节流的顺序。 在本实验中&#xff0c;要实现的是重组器Reassembler&#xff0c;它接受子字符串和其…

机器学习之强化学习

引言 在人工智能的众多分支中&#xff0c;强化学习&#xff08;Reinforcement Learning, RL&#xff09; 因其独特的学习范式而备受关注。与依赖标注数据的监督学习或探索数据结构的无监督学习不同&#xff0c;强化学习的核心是智能体&#xff08;Agent&#xff09;通过与环境…

笔记:代码随想录算法训练营day37:完全背包、518. 零钱兑换 II、377. 组合总和 Ⅳ、70. 爬楼梯 (进阶)

学习资料&#xff1a;代码随想录 文中含大模型生成内容 完全背包 52. 携带研究材料&#xff08;第七期模拟笔试&#xff09; 相比于之前的一个物品只能放一次&#xff0c;这次一个物品可以放多次了 递推公式变成了dp[i][j] max(dp[i - 1][j], dp[i][j - weight[i]] valu…

C/C++中函数指针和指针函数的原理和区别是什么,分别通过用例说明。

文章目录 函数指针和指针函数的区别函数指针指针函数区别 总结 函数指针和指针函数的区别 在C/C中&#xff0c;函数指针和指针函数是两个不同的概念&#xff0c;它们的用途和定义方式也有所不同。 函数指针 定义&#xff1a; 函数指针是一个指向函数的指针&#xff0c;它存储…

2025年主流原型工具测评:墨刀、Axure、Figma、Sketch

2025年主流原型工具测评&#xff1a;墨刀、Axure、Figma、Sketch 要说2025年国内产品经理使用的主流原型设计工具&#xff0c;当然是墨刀、Axure、Figma和Sketch了&#xff0c;但是很多刚入行的产品经理不了解自己适合哪些工具&#xff0c;本文将从核心优势、局限短板、协作能…

分布式事务 面试专题

分布式事务 面试专题 分布式事务与分布式锁的区别分布式事务场景核心理论分布式事务分类2PC&#xff08;标准XA模型&#xff09;3PC&#xff08;CanCommit、PreCommit、doCommit &#xff09;通知型事务异步确保型事务最大努力通知事务MQ事务消息方案本地消息表方案 补偿型TCC&…

颠覆传统软件测试!Browser Use WebUI+DeepSeek:软件测试行业的革命性突破

前置信息 硬件配置 处理器 : Intel(R) Core(TM) i5-8265U CPU 1.60GHz (四核 / 八逻辑处理器) 主板 : 20N8002UCD 内存 : 8GB(RMSA3260ME78HAF-2666 DDR4 2667 MT/s) 显示适配器 : Lexa PRO [Radeon 540/540X/550/550X / RX 540X/550/550X]/WhiskeyLake-U GT2 [UHD Graphics…

DFT之SSN架构

SSN&#xff08;Streaming Scan Network&#xff09;架构在DFT&#xff08;设计可测试性&#xff09;中的应用是一种先进的设计测试解决方案&#xff0c;旨在应对现代大规模片上系统&#xff08;SoC&#xff09;设计中的复杂测试挑战。以下是对SSN架构在DFT中应用的详细分析&am…

Elasticsearch:“Your trial license is expired”

目录标题 问题原因解决方案 问题 原因 ES的X-pack许可证是提供免费一个月的试用&#xff0c;到期之后就会报这个错误。 解决方案 查看license GET _license 开启试用license POST _xpack/license/start_trial?acknowledgetrue修改为基础license POST _xpack/license/start_…

实训任务2.2 使用Wireshark捕获数据包并分析

目录 【实训目标】 【实训环境】 【实训内容】 【实训步骤】 1.启动WireShark 2. 使用Wireshark捕获数据包 &#xff08;1&#xff09;选择网络接口 &#xff08;2&#xff09;捕获数据包 &#xff08;1&#xff09;设置Wireshark过滤器并捕获数据包 &#xff08;2&…

PHP 矩形面积和周长的程序(Program for Area And Perimeter Of Rectangle)

矩形是平面上的平面图形。 它有四条边和四个相等的角&#xff0c;每个角都是 90 度。 矩形的四条边并不像正方形那样长度相等&#xff0c;而是彼此相对的边长度相等。 矩形的两条对角线长度相等。 例子&#xff1a; 输入&#xff1a;4 5 输出&#xff1a;面积 20 …

常见Web应用源码泄露问题

文章目录 前言一、常见的源码泄露漏洞git源码泄露SVN源码泄露DS_Store文件泄漏网站备份压缩文件泄露WEB-INF/web.xml泄露CVS泄露.hg源码泄露Bazaar/bzr泄露.swp文件泄露 前言 在Web应用方面对于安全来说&#xff0c;可能大家对SQL注入、XSS跨站脚本攻击、文件上传等一些漏洞已…

Windows11下玩转 Docker

一、前提准备 WSL2&#xff1a;Windows 提供的一种轻量级 Linux 运行环境&#xff0c;具备完整的 Linux 内核&#xff0c;并支持更好的文件系统性能和兼容性。它允许用户在 Windows 系统中运行 Linux 命令行工具和应用程序&#xff0c;而无需安装虚拟机或双系统。Ubuntu 1.1 安…