【排序算法 下】带你手撕常见排序 (冒泡,快排,归并排序) (动图详解)

news2025/6/20 1:34:56

欢迎来到 Claffic 的博客 💞💞💞

“只要有花可开,就不允许生命与黯淡为伍。”

前言: 

承接上篇,继续带大家手撕常见排序算法,这次讲剩余的两类:交换排序和归并排序。

注:

这篇博客属于数据结构内容,建议学习完C语言的小伙伴食用~


目录

🐨Part1:交换排序

1.冒泡排序

1.1思想

1.2实现

2.快速排序

2.1Hoare经典版

2.2挖坑版

🐸Part2:归并排序

归并排序

思想

实现

🦄最后总结 


Part1:交换排序

1.冒泡排序

1.1思想

冒泡排序相比大家都很熟悉了,这是学习C语言过程当中的第一个排序算法

它的思想就是一趟一趟走,每一次都找到最大的数字,放到靠后的位置,就像水底冒泡一样,越往上气泡越大。

动图:

1.2实现

//冒泡排序
void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n; j++)
	{
		for (int i = 1; i < n-j; i++)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
			}
		}
	}
}

特征分析:

像选择排序,思想易理解,也好实现,适合教学;

时间复杂度:最好:O(N^2)  最坏:O(N^2);

空间复杂度:O(1);

稳定性:稳如老狗

2.快速排序

在这里,讲快速排序三种常见的实现方法:Hoare经典版本,挖坑版本,前后指针版本。 

2.1Hoare经典版

因为这个算法是霍尔大佬发明的,所以以他的名字命名,这是最经典的快速排序:

思想:

先找一个参考值 key,定义左右两端的下标/指针 R , L ,向内靠拢;

注意:左边做key,右边先走,可使相遇点比key小;

R 找比 key 值小的位置,L 找比 key 值大的位置;

都找到后,R L 对应位置的数字交换;

R L 相遇,相遇位置对应的数字与 key 交换。

这样就保证了数组左侧数字都小于 key,右侧数字都大于 key 。

到这里还没完呢,可不能保证左右侧数字都是有序的,所以还要对左右两侧进行相同操作,没错,可以用递归。 

有点类似与二叉树的分治。

代码实现:

//快排(Hoare)
void QuickSort1(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left, end = right;

	int keyi = left;
	while (left < right)
	{
		//右找小
		while (left < right && a[right] >= a[keyi])
			right--;

		//左找大
		while (left < right && a[left] <= a[keyi])
			left++;
		
		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[left]);
	keyi = left;

	//递归
	QuickSort1(a, begin, keyi - 1);
	QuickSort1(a, keyi + 1, end);
}

特性分析:  

快速排序的综合性能和适用场景都比较好,大多数库里的排序都是快排;

时间复杂度:最好:O(N*logN)  最坏:O(N^2)  平均取O(N*logN);

空间复杂度:O(logN)   (递归创建函数栈帧)

稳定性:不稳定

以下另外两种版本与此版本的性能相同,便不再进行此特性分析

2.2挖坑版

这个方式的本质思想是没有变化的,就是把 key 值另外保存起来,形成一个坑位,在数据交换的过程中不断更新坑位:

这种思路下,就不必让 R 优先走了,也可以让 L 优先走,因为最终都能保证相遇点比 key 小,图示默认让 R 优先走;

代码实现: 

//快排(挖坑法)
void QuickSort2(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left, end = right;
	
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		//右找小
		while (left < right && a[right] >= key)
			right--;
		a[hole] = a[right];
		hole = right;

		//左找大
		while (left < right && a[left] <= key)
			left++;
		a[hole] = a[left];
		hole = left;
	}

	a[hole] = key;

	//递归
	QuickSort2(a, begin, hole - 1);
	QuickSort2(a, hole + 1, end);
}

特征同霍尔版本  

2.3前后指针版

这个版本是最方便,最好理解的;

定义 curprev 两个下标,对应位置的值与 key 比较:

无论比较的值是大是小,cur 都要++。 

代码实现: 

//部分快排(前后指针法)
int PartSort3(int* a, int left, int right)
{
	int keyi = left;
	int key = a[left];
	int prev = left;
	int cur = left + 1;

	while (cur <= right)
	{
		if (a[cur] < key && ++prev != cur) // 保证 prev 在 cur 的左侧
			Swap(&a[cur], &a[prev]);
		cur++;
	}
	Swap(&a[keyi], &a[prev]);//与下标为keyi的交换
	keyi = prev;

	return keyi;
}

 特征同霍尔版本  

Part2:归并排序

归并排序

思想

所谓归并排序,就是采用分治法,将已有的有序子序列合并,得到完全有序的序列,以此类推,最终并得到的就是一个有序的数组。

实现

实现过程中,我们需要一块辅助空间来帮助归并,最先归并好的结果在那块辅助空间当中,最后再把结果拷贝到原数组空间中。

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;
    // 中分
	int mid = (begin + end) / 2;
	//[begin,mid] [mid+1,end]
    // 递归
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid+1, end, tmp);
	//拷贝
	int begin1 = begin,begin2 = mid + 1;
	int end1 = mid,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)
	{
		perror("malloc fail");
		return;
	}
    // 这里不能自己递归自己,否则会不断开辟空间
	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
}

特征分析:

归并排序效率不低,但需要开辟一块大小为N的空间,当数据量过大时,可以考虑在磁盘中排序。

时间复杂度:最好:O(N*logN)   最坏:O(N*logN) 

空间复杂度:O(N) 

稳定性:稳定

最后总结 

排序方法平均情况最好情况最坏情况辅助空间稳定性
冒泡排序O(N^2)O(N)O(N^2)O(1)稳定
简单选择排序O(N^2)O(N^2)O(N^2)O(1)不稳定
直接插入排序O(N^2)O(N^1.3)O(N^2)O(1)稳定
希尔排序O(N*logN)~O(N^2)O(N*logN)O(N^2)O(1)不稳定
堆排序O(N*logN)O(N*logN)O(N*logN)O(1)不稳定
归并排序O(N*logN)O(N*logN)O(N*logN)O(N)稳定
快速排序O(N*logN)O(N*logN)O(N^2)O(logN)~O(N)不稳定

各排序对比

代码已上传至 我的gitee 

拿走不谢~


总结: 

到这里,其实排序还并没有完结,我在后期会更新快速排序的优化以及快速排序和归并的非递归实现方式,有点小期待呢~

码文不易 

如果你觉得这篇文章还不错并且对你有帮助,不妨支持一波哦  💗💗💗

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

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

相关文章

C++——模板初阶与泛型编程

文章目录&#x1f490;专栏导读&#x1f490;文章导读&#x1f337;引例&#x1f337;函数模板&#x1f33a;函数模板的概念&#x1f33a;函数模板的格式&#x1f337;函数模板的原理&#x1f337;函数模板的实例化&#x1f33a;隐式实例化&#x1f33a;显式实例化&#x1f33a…

Maven安装

目录 1.Maven安装 1.1下载 1.2 安装步骤 1、解压 apache-maven-3.6.1-bin.zip&#xff08;解压即安装&#xff09; 2、配置本地仓库 3、配置阿里云私服 4、配置环境变量 1.3 安装检测 1.Maven安装 认识了Maven后&#xff0c;我们就要开始使用Maven了&#xff0c;那么首…

基于OpenCv的图像分割(分水岭算法)

文章目录图像分割distanceTransform()connectedComponents()watershed()查看图像的矩阵图像分割 图像分割对于图像处理和计算机视觉领域非常重要&#xff0c;可以用于对象识别、图像分析、图像压缩等应用。 注意&#xff1a;通常我们把前景目标的灰度值设为255&#xff0c;即白…

网络原理与网络通信

目录 网络互连原理 网络通信 IP地址和端口号 网络协议 五元组 协议分层 OSI七层模型 TCP/IP五层模型 封装和分用 网络互连原理 计算机在最开始的时候是没有网络的&#xff0c;每个计算机之间相互独立。这样处理信息就非常的麻烦&#xff0c;为了能够更高效的利用计算…

一个基于Java线程池管理的开源框架Hippo4j实践

文章目录概述定义线程池痛点功能框架概览架构部署Docker安装二进制安装运行模式依赖配置中心接入流程个性化配置线程池监控无中间件依赖接入流程服务端配置三方框架线程池适配拒绝策略自定义概述 定义 Hippo4j 官网地址 https://hippo4j.cn/ 最新版本1.5.0 Hippo4j 官网文档地…

硬件系统工程师宝典(17)-----你的PCB符合工艺要求吗?

各位同学大家好&#xff0c;欢迎继续做客电子工程学习圈&#xff0c;今天我们继续来讲这本书&#xff0c;硬件系统工程师宝典。上篇我们说到PCB设计中板子要符合EMC&#xff0c;信号的走线要平顺&#xff0c;信号回流阻抗尽量小。今天我们开始看看板子在生产制造时的工艺问题。…

【安全防御】防火墙(二)

目录 1、防火墙如何处理双通道协议 2、防火墙如何处理nat 3、防火墙支持哪些NAT&#xff0c;主要应用的场景是什么&#xff1f; 4、当内网PC通过公网域名解析访问内网服务器的时候&#xff0c;会存在什么问题&#xff0c;如何解决&#xff1f;请详细说明 5.防火墙使用VRRP…

面试题总结-JS

文章目录一、JS 系列1、原型、原型链2、闭包3、this指向4、call、 apply、 bind 的作用与区别&#xff1f;5、数组扁平化6、var、let、const 区别7、对称加密和不对称加密的区别8、js 的栈和堆9、对象的深拷贝和浅拷贝10、浏览器的事件循环机制11、宏任务和微任务12、script 标…

StringBuilder、StringBuffer、String的区别

StringBuilder与StringBuffer的append方法源码分析 #mermaid-svg-N8145OzAyMWzlewt {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-N8145OzAyMWzlewt .error-icon{fill:#552222;}#mermaid-svg-N8145OzAyMWzlewt .er…

C#基础学习--泛型

目录 C#中的泛型 泛型类 声明泛型类 创建构造函数 创建变量和实例 类型参数的约束 Where 子句 泛型方法 声明泛型方法 ​编辑 调用泛型方法 扩展方法和泛型类 泛型结构 泛型委托 泛型接口 协变 逆变 接口的协变和逆变 C#中的泛型 泛型允许我们声明 类型参数化 的代码&…

Jetpack Compose大师乘势而上,创建引人入胜和直观的UI;实用技巧和技术

简述 Jetpack Compose 是 Android 上的一种全新的 UI 工具箱&#xff0c;旨在简化 Android UI 开发流程&#xff0c;提高开发效率和应用性能&#xff0c;并且提供更直观、更灵活、更强大的 UI 定义方式。 Jetpack Compose 提供了一套新的声明式 UI 编程模型&#xff0c;采用 …

【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)

【Redis】多级缓存 文章目录【Redis】多级缓存1. 传统缓存的问题2. 多级缓存方案2.1 JVM进程缓存2.1.1 本地进程缓存2.1.2 Caffeine2.2 Nginx缓存2.2.1 准备工作2.2.2 请求参数处理2.2.3 nginx发送http请求tomcat2.2.3.1 封装http查询函数2.2.3.2 使用http函数查询数据2.2.4 ng…

Huffman 编码

1.Huffman编码 1952年提出一种编码方法&#xff0c;该方法完全依据字符出现概率来构造异字头的平均长度最短的码字&#xff0c;有时称之为最佳编码&#xff0c;一般就叫做Huffman编码(有时也称为霍夫曼编码)。 2.Huffman树 树是一种重要的非线性数据结构&#xff0c;它是数据元…

​2023年十大目标检测模型!

“目标检测是计算机视觉中最令人兴奋和具有挑战性的问题之一&#xff0c;深度学习已经成为解决该问题的强大工具。”—Dr. Liang-Chieh Chen目标检测是计算机视觉中的基础任务&#xff0c;它涉及在图像中识别和定位目标。深度学习已经革新了目标检测&#xff0c;使得在图像和视…

【CV大模型SAM(Segment-Anything)】真是太强大了,分割一切的SAM大模型使用方法:可通过不同的提示得到想要的分割目标

目录前言安装运行环境SAM模型的使用方法导入相关库并定义显示函数导入待分割图片使用不同提示方法进行目标分割方法一&#xff1a;使用单个提示点进行目标分割方法二&#xff1a;使用多个提示点进行目标分割方法三&#xff1a;用方框指定一个目标进行分割方式四&#xff1a;将点…

文件操作和IO—javaEE

文章目录1.文件1.1文件系统的结构1.2java中的文件操作&#xff08;metadata的操作&#xff09;2.io操作2.1定义2.2io划分2.3java的io流之输入流2.4java的io流之输出流1.文件 文件包含数据本身和文件的头信息&#xff08;metadata&#xff09;&#xff0c;文件的头信息包括文件…

VSCode的C/C++编译调试环境搭建(亲测有效)

文章目录前言1.安装VSCode和mingw642.配置环境变量3.配置VSCode的运行环境3.1设置CodeRunner3.2设置C/C4.调试环境配置前言 这片博客挺早前就写好了&#xff0c;一直忘记发了&#xff0c;写这篇博客之前自己配的时候也试过很多博客&#xff0c;但无一例外&#xff0c;都各种js…

SpringBoot(4)整合数据源

SpringBoot整合数据源数据层解决方案数据源技术持久化技术数据库技术NoSQL整合Redis整合MongDB整合ES数据层解决方案 MySQL数据库与MyBatisPlus框架&#xff0c;后面又用了Druid数据源的配置&#xff0c;所以现在数据层解决方案可以说是MysqlDruidMyBatisPlus。而三个技术分别…

一文彻底了解派克Parker无铁芯/有铁芯直线电机及其应用

一、什么是直线电机&#xff1f; 直线电机是一种将电能直接转换成直线运动机械能&#xff0c;而不需要任何中间转换机构的传动装置。它可以看成是一台旋转电机按径向剖开&#xff0c;并展成平面而成。 二、直线电机的特点 直线电机类似于一台旋转电机解剖摊开来进行运转。在一…

9、DRF实战总结:过滤(filter)与排序,以及第三方库django-filter的使用(附源码)

在前面的DRF系列教程中&#xff0c;以博客为例介绍了序列化器(Serializer), 并使用基于类的视图APIView和ModelViewSet开发了针对文章资源进行增删查改的完整API接口&#xff0c;并详细对权限、认证(含jwt认证)和分页进行了总结与演示。在本篇文章中将向演示如何在Django REST …