【数据结构与算法篇】手撕八大排序算法之交换排序

news2025/1/9 16:31:56

在这里插入图片描述

​👻内容专栏: 《数据结构与算法篇》
🐨本文概括:常见交换排序包括冒泡排序与快速排序,本篇讲述冒泡排序与快速排序的思想及实现、复杂度分析。
🐼本文作者: 花 蝶
🐸发布时间:2023.8.27

一、冒泡排序

基本思想

冒泡排序(Bubble Sort)是一种简单的排序算法,其基本思想是通过两两交换相邻元素的位置,使得较大(或较小)的元素逐步“冒泡”到数组的一端(顶部或底部),重复“冒泡”的过程,直到序列没有要交换的元素为止,从而实现排序。

​算法步骤

1、 从数组的起始位置开始,依次比较相邻的两个元素。如果相邻元素的顺序错误(前者大于后者,或者前者小于后者,取决于是升序还是降序排序),则交换这两个元素的位置;
2、继续进行相邻元素的比较和交换,直到遍历到数组的末尾。这样一轮下来,最大(或最小)的元素就会“冒泡”到数组的一端。
3、重复上述步骤,但不再考虑已经排序好的末尾部分。每一轮操作都会将一个最大(或最小)的元素移到正确的位置。
4、 经过多轮的比较和交换,最终整个数组会变得有序。如果在某一轮没有进行任何交换操作时,说明数组已经有序,可以直接跳出循环。

动图演示

在这里插入图片描述

代码实现

//冒泡排序
//时间复杂度:O(N^2)
void BubbleSort(int* a, int n)
{
	//控制冒泡排序的趟数
	for(int i = 0; i < n - 1; i++)
	{
		//假设数组有序
		int flag = 1;
		//控制每趟的过程
		for (int j = 0; j < n - 1 - i; j++)
		{
			if (a[j] > a[j + 1])
			{
				 Swap(&a[j], &a[j + 1]);
				 flag = 0;
			}
		}
		//说明没有发生交换,数组已经有序,跳出循环即可
		if (flag == 1) break;
	}
}

冒泡排序的特性

冒泡排序的核心思想就是通过反复比较相邻元素并交换它们的位置,从而逐步将最大(或最小)的元素移动到合适的位置。尽管冒泡排序的时间复杂度较高(平均和最坏情况下都是 O(N^2),可以看作是一个等差数列的求和),但它的实现相对简单,适用于小规模的数据集,因此具有较强的教学意义,想必大家在刚开始学习编程时学的一个排序就是冒泡排序吧哈哈。

二、快速排序(递归版本)

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

//快排(递归版本)
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end) return;

	int keyi = Partition(a, begin, end); 

	//前序遍历
	//递归基准值左边的区间
	QuickSort(a, begin, keyi - 1);
	//递归基准值右边的区间
	QuickSort(a, keyi + 1, end);
}

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,大家在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后续只需分析如何按照基准值来对区间中数据进行划分的方式即可。
下面用三个版本实现:hoare版本、挖坑法、前后指针法

1.hoare版本

1、选择基准元素key,通常是数组中的第一个元素。
2、使用两个指针LR,一个指向数组的开头(较小值的区域),一个指向数组的末尾(较大值的区域)。
3、交换的目标是L找到比基准大的元素,R找到比基准小的元素。不断交换指针所指的元素,直到两个指针相遇。
4、当两个指针相遇时,交换基准元素与当前相遇位置的元素,此时数组被划分为左右两个子数组。
5、对左右两个子数组递归地应用相同的分区步骤,直到所有子数组都有序

算法分析

👇①:原始序列
在这里插入图片描述
👇②:right向前移动,找比key小的位置,left往后移动找比key大的位置,然后交换。
在这里插入图片描述
👇③:继续往后寻找,直到两个指针相遇。
在这里插入图片描述
👇④:交换基准元素与当前相遇位置的元素。
在这里插入图片描述
👇⑤:这样子我们发现基准值左边的区间里面的元素都比基准值小,右边的区间里面的元素都比基准值大。我们可以按照前序遍历的思想对左边区间进行递归操作,右边区间也进行递归操作。

代码实现

//hoare版本
int Partition1(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		//注意: 要加上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[left], &a[keyi]);

	return left;
}

//快排(递归版本)
void QuickSort(int* a, int begin, int end)
{
	//当区间不存在或者只剩一个元素,就回溯
	if (begin >= end) return;

	int keyi = Partition1(a, begin, end);

	//前序遍历
	//递归基准值左边的区间
	QuickSort(a, begin, keyi - 1);
	//递归基准值右边的区间
	QuickSort(a, keyi + 1, end);
}

2.挖坑法

挖坑法的基本思想是:

  • 1、首先,选择一个基准元素(通常是数组中的第一个元素)作为“坑”(hole)。前提需要将这个基准元素用一个临时变量key存起来。
  • 2、将基准元素挖出,形成一个空位(坑)。
  • 3、从数组的另一端开始,从右向左遍历,找到一个比基准元素小的元素,然后将这个元素填入之前的坑中。
  • 4、继续从左向右遍历,找到一个比基准元素大的元素,然后将这个元素填入上一步的坑中。
  • 5、重复执行步骤 3 和步骤 4,直到左右指针相遇。
  • 6、此时,基准元素的位置就是这个相遇的位置,将基准元素填入这个坑中。

算法分析

👇①:原始序列
在这里插入图片描述
👇②:将基准值存放到临时变量key中,形成一个坑位。
在这里插入图片描述
👇③:从右向左遍历,R找到比基准值小的元素,将这个元素填入到之前的坑中,此时当前位置就形成了新坑。
在这里插入图片描述
👇④:紧接着,从左往右遍历,L寻找比基准值大的元素,将这个元素填入到上一步的坑中,此时当前位置形成了新坑。
在这里插入图片描述
👇⑤:重复以上步骤后,直到左右指针相遇,最后会形成一个坑,将临时变量放入坑中。
在这里插入图片描述
👇⑥:这样子我们发现基准值左边的区间里面的元素都比基准值小,右边的区间里面的元素都比基准值大。我们可以按照前序遍历的思想对左边区间进行递归操作,右边区间也进行递归操作。

代码实现

//挖坑法
int Partition2(int* a, int left, int 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;
	return hole;
}
//快排(递归版本)
void QuickSort(int* a, int begin, int end)
{
	//当区间不存在或者只剩一个元素,就回溯
	if (begin >= end) return;
	
	int keyi = Partition2(a, begin, end);

	//前序遍历
	//递归基准值左边的区间
	QuickSort(a, begin, keyi - 1);
	//递归基准值右边的区间
	QuickSort(a, keyi + 1, end);
}

3.前后指针法

基本思想:在快速排序的划分过程中,使用前后指针法来确定基准元素的位置,最后将数组划分成两部分,一部分小于基准元素,另一部分大于基准元素。

  • 1、首先,选择一个基准元素key(通常是数组中的第一个元素)。
  • 2、使用两个指针,prev指向数组的开头,cur指向prev的后一个位置。
  • 3、判断cur指向的数据是否小于key。若小于,则prev后移一位,cur指向的内容与prev指向的内容交换,然后cur++
  • 4、若cur指向的数据大于key,则cur++
  • 重复步骤3和步骤4,直到cur指针走完(最后一个元素的下一个位置)
  • 最后,将keyprev指向的元素交换。

动图演示

在这里插入图片描述

//前后指针法
int Partition3(int* a, int left, int right)
{
	int prev = left;
	int cur = prev + 1;
	int keyi = left;
	//cur小于key,交换++prev与cur的位置
	//将大的位置翻滚时往后挪动,小的位置移动前面
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	
	//将key与prev指向的元素交换。
	Swap(&a[prev], &a[keyi]);

	return prev;
}

//快排(递归版本)
void QuickSort(int* a, int begin, int end)
{
	//当区间不存在或者只剩一个元素,就回溯
	if (begin >= end) return;
	
	int keyi = Partition3(a, begin, end);

	//前序遍历
	//递归基准值左边的区间
	QuickSort(a, begin, keyi - 1);
	//递归基准值右边的区间
	QuickSort(a, keyi + 1, end);
}

快速排序(递归版本)的特性

时间复杂度

选择基准元素影响时间复杂度:若基准元素key偏向于所有元素中的中位数,则时间复杂度处于较好的情况,若基准元素key是所有元素中最小的,或者接近最小值,那么时间复杂度就处于较坏的情况。

  • 平均情况下的时间复杂度: 在平均情况下,快速排序的时间复杂度为 O(n log n)。这是因为在每次分区过程中,数组会被划分成大致相等的两部分。在每次递归中,都会将问题的规模减半,递归的展开图可以看作是一颗满二叉树,所以递归的深度为 O(log n),每层递归的分区操作都需要 O(n) 的时间,因此总的时间复杂度为 O(n*log n)
  • 最坏情况下的时间复杂度: 在最坏情况下,快速排序的时间复杂度为 O(n^2)。这种情况发生在每次划分后,一个子数组为空,另一个子数组包含所有的元素。这样会导致递归树变得很不平衡,每次递归的问题规模只减少一个元素。虽然快速排序通常能够避免这种情况,但在某些情况下(例如已经有序的数组),最坏情况可能出现。

在这里插入图片描述

解决方案

  • 随机数选择基准元素: 在每次划分时,随机选择一个基准元素,这可以减少最坏情况的发生概率。
  • 三数取中法: 在选择基准元素时,不仅考虑第一个和最后一个元素,还考虑数组中间位置的元素,选择其中值大小居中的元素作为基准。

👇这里提供一种三数取中的方法,以hoare版本为例:

//三数取中,返回下标
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) >> 1;

	if (a[left] < a[mid])
	{
		if (a[mid] < a[right]) return mid;
		else if (a[left] > a[right]) return left;
		else return mid;
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right]) return mid;
		else if (a[right] > a[left]) return left;
		else return right;
	}
}
//hoare版本
int Partition1(int* a, int left, int right)
{
	//将中位数与数组的第一个元素(基准值)进行交换
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	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[left], &a[keyi]);

	return left;
}

后面会更新快排的非递归版本……可到数据结构专栏查看🥰
更多数据结构与算法系列文章👉😉==> 【传送门】

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

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

相关文章

表和Json的相互操作

目录 一、表转Json 1.使用 for json path 2.如何返回单个Json 3.如何给返回的Json增加一个根节点呢 4.如何给返回的Json增加上一个节点 二、对Json基本操作 1.判断给的字符串是否是Json格式 2.从 JSON 字符串中提取标量值 3. 从 JSON 字符串中提取对象或数组 4. 更…

计网-All

路由器的功能与路由表的查看_路由器路由表_傻傻小猪哈哈的博客-CSDN博客路由基础-直连路由、静态路由与动态路由的概念_MikeVane-bb的博客-CSDN博客路由器的功能与路由表的查看_路由器路由表_傻傻小猪哈哈的博客-CSDN博客 直连路由就是路由器直接连了一个网段&#xff0c;他就…

在线考试系统java线上答题问答jsp源代码MySQL

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 在线考试系统 系统有2权限&#xff1a;管理员、学生 …

ChatGPT 与前端技术实现制作大屏可视化

像这样的综合案例实分析,我们可以提供案例,维度与指标数据,让ChatGPT与AIGC 帮写出完整代码,并进行一个2行2列的布局设置。 数据与指令如下: 商品名称 销量 目标 完成率 可乐 479 600 79.83% 雪碧 324 600 54.00% 红茶 379 600 63.…

NoSQL数据库介绍+Redis部署

目录 一、NoSQL概述 1、数据的高并发读写 2、海量数据的高效率存储和访问 3、数据库的高扩展和高可用 二、NoSQL的类别 1、键值存储数据库 2、列存储数据库 3、文档型数据库 4、图形化数据库 三、分布式数据库中的CAP原理 1、传统的ACID 1&#xff09;、A--原子性 …

webassembly001 webassembly简述

WebAssembly 官方地址:https://webassembly.org/相关历史 https://en.wikipedia.org/wiki/WebAssembly https://brendaneich.com/2015/06/from-asm-js-to-webassembly/WebAssembly&#xff08;缩写为Wasm&#xff09;是一种基于堆栈的虚拟机的二进制指令格式。Wasm 被设计为编…

【C语言练习】牛客题库第(一)期

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

攻防演练期间一次对某企业的渗透测试

免责声明 由于传播、利用本文章说黑客所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;文章作者说黑客不为此承担任何责任&#xff0c;一旦造成后果请自行承担&#xff01; 前言 某次攻防演练中&#xff0c;主办方只提供了目标…

软考:中级软件设计师:HTML

软考&#xff1a;中级软件设计师:HTML 提示&#xff1a;系列被面试官问的问题&#xff0c;我自己当时不会&#xff0c;所以下来自己复盘一下&#xff0c;认真学习和总结&#xff0c;以应对未来更多的可能性 关于互联网大厂的笔试面试&#xff0c;都是需要细心准备的 &#xff…

SM3(国密)在线加密--获取摘要

SM3&#xff08;国密&#xff09;在线加密--获取摘要

官方项目《内容示例》中Common UI部分笔记: 1.1 Activatable Widgets

本文主要面向UMG以及Common UI的初学者 文章目录 效果展示概要Activate和Deactivate可见性绑定UI动画设置Common Activatable Widget的默认焦点 效果展示 概要 这个例子非常简单&#xff0c;定义了13个Common Activatable Widget CommonUI_ActivatableWidgets相当于一个容器包…

【校招VIP】java语言考点之双亲委派模型

考点介绍&#xff1a; 双亲委派是校招面试中的高频考点之一。 双亲委派机制定义:当一个类加载器收到了类加载的请求的时候&#xff0c;他不会直接去加载指定的类&#xff0c;而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候&#xff0c;才会由当前…

系统集成项目管理工程师【中级】考证学习资料知识点整理分享——第二章《信息系统集成及服务管理》,持续更新中........

系统集成项目管理工程师(中级)考证学习资料整理分享,持续更新中........ 目 录 第二章《信息系统集成及服务管理》 一、信息系统集成及服务管理 (一)信息系统集成及服务管理的内容 (二)信息系统集成及服务管理的推进 1.实施信息系统集成及服务资质管理制度 1)…

Pico如何使用C/C++选择使用哪个I2C控制器,以及SDA和SCL针脚

本文一开始讲述了解决方案&#xff0c;后面是我做的笔记&#xff0c;用来讲述我的发现流程和探究的 Pico I2C 代码结构。 前提知识 首先要说明一点&#xff1a;Pico 有两个 I2C&#xff0c;也就是两套 SDA 和 SCL。这点你可以在针脚图中名字看出&#xff0c;比如下图的 Pin 4…

AI绘图(11)stable diffusion 如何写好prompt 四

在最开始我写了三篇关于prompt的&#xff0c;具体的大家可以跳转来去看&#xff0c;以下给出来链接&#xff1a; AI绘图&#xff08;3&#xff09;stable diffusion如何写好prompt 一_牧子川的博客-CSDN博客 AI绘图&#xff08;4&#xff09;stable diffusion如何写好prompt …

nodejs里面的event loop

1. event loop 1.1 什么是event-loop js的标准文档定义如下 https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#event_loop https://javascript.info/event-loop html的标准定义 https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-proc…

CNN 02(CNN原理)

一、卷积神经网络(CNN)原理 1.1 卷积神经网络的组成 定义 卷积神经网络由一个或多个卷积层、池化层以及全连接层等组成。与其他深度学习结构相比&#xff0c;卷积神经网络在图像等方面能够给出更好的结果。这一模型也可以使用反向传播算法进行训练。相比较其他浅层或深度神经…

Dreamweaver软件安装包分享(附安装教程)

目录 一、软件简介 二、软件下载 一、软件简介 Dreamweaver软件是一款专业的网页开发工具&#xff0c;由Adobe公司开发并广泛应用于Web开发领域。它提供了一站式的网页开发解决方案&#xff0c;包括网页设计、网页编程、网站管理和移动应用开发等功能。 Dreamweaver软件具有…

云计算服务体系-架构真题(十四)

云计算服务体系结构SaaS、PaaS、IaaS相对应分别&#xff08;&#xff09;。 答案。应用层、平台层、基础设施层 (2022)给定关系模式R(U,F)&#xff0c;其中U为属性集&#xff0c;F是U的一组函数依赖&#xff0c;那么函数依赖的公理系统(Armstrong)中分解规则是指&#xff08;&…

API管理测试 - 最佳实践和关键要素

什么是API管理测试&#xff1f; API管理测试是在软件开发和集成功能中对应用程序接口&#xff08;API&#xff09;进行测试和验证的过程。它涵盖了测试API的功能、性能、安全性以及与其他系统的交互。API管理测试对于确保API的正确运行和稳定性非常重要。 ​ 为什么API管理测…