排序(3)之交换排序

news2025/1/18 7:23:22

目录

前言

交换排序

1.冒泡排序

1.1冒泡排序的实现

1.2 特性总结

2.快速排序

2.1hoare版本

 2.2  挖坑法

 2.3 前后指针版本

3.快速排序的优化

3.1 三数取中法

3.2 小区间优化

4.快速排序的非递归实现


前言

今天小编给大家带来交换排序的内容,对于交换排序中的快速排序在理解上会相对困难一点,小编这里会配合图片给大家细细讲解。那么现在就开始我们今天的主题。


交换排序

交换排序的思想是:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动

1.冒泡排序

在介绍快速排序之前,这里先给大家介绍一个常见且理解上比较简单的排序——冒泡排序。

1.1冒泡排序的实现

我们这里以升序为例:

 

冒泡排序的排序过程是通过元素之间的不断比较如果前一个元素比后一个元素大,那么就让前一个元素和后一个元素进行交换,那么通过元素之间的不断比较交换最后我们就会发现我们最后一个元素就是我们数组的最大值,那么此时我们就可以把最后一个元素固定,然后重复操作,我们每次都可以选出一个最大数,进行固定,知道我们固定了n-1个元素后,我们也完成了排序操作。

同样我们在实现冒泡排序之前我们需要交换函数:

void swap(int* p, int* q)
{
	int temp = *p;
	*p = *q;
	*q = temp;
}

然后这里我们看冒泡排序的代码:

void BubbleSort(int* a, int n)
{
	
	for (int j = 0; j < n; j++)
	{
		bool exchange = false;
		for (int i = 1; i < n - j; i++)
		{
			if (a[i] < a[i - 1])
			{
				exchange = true;
				swap(&a[i], &a[i - 1]);
			}
		}
		if (exchange == false)
		{
			break;
		}
	}
}

这里我们第一层循环用来控制循环次数,第二层循环控制每次参与循环的元素个个数,由于每循环一次我们下一次的循环元素就会少一个,所以第二层循环的结束条件就是也就是我们每次需要参数与循环的元素个数最大值就是等于n-j。

对于冒泡排序我们还可以对其做一个优化,那么具体是什么优化以及优化在哪里呢?

对于冒泡排序的时间复杂度,我们一般定义在O(N^2)这个量级,但是对于最好的情况下来讲,也就是我们已经是有序的情况,如果我们不加以优化,那么该时间复杂度仍然是在O(N^2)这个量级,但是如果我们在该进入第一次循环时,我们就加一个条件去判断该是否出现了后面值比前面小的情况,如果没出现,那么说明我们数组已经是有序的,那么我们此时直接退出那么这里的时间复杂度也就会大大缩减在O(N)这个量级。

所以小编用一个布尔类型的变量exchange,如果出现了后面值比前面值小的情况时exchange=true就说明,我们数组现在是一个无序的状态,但是没有出现此类情况,我们的exchang=false没有变化,说明数组有序我们直接退出循环。

1.2 特性总结

1.时间复杂度:O(N^2)
2. 空间复杂度:O(1)
3. 稳定性:稳定

2.快速排序

对于快速排序,我们这里一共有三个版本的方法,但是该本质的思想都是一样的,只不过在代码的实现有着一定差异,这里我们都需要使用到递归的思想。

基本思想:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

2.1hoare版本

hoare版本的快速排序就是快速排序的开始,对于该排序细节大家可以先看图一下

 那么看完这个动态图我想大家已经对快排有了一定的理解,对于快排,首先我们就需要找到一个key值,一般我们选取的就是最左边和最右边,如果选择最左边,那么我们就需要让右边先走,反之就是最左边先走,这里我们让右边找小于key位置内容的值,找到后就停下,左边找比key位置内容大的值,找到后让左边和右边的值进行互换,那么最后左边和右边的相遇值就是key位置内容应该处于的位置,那么就需要让我们相遇位置的内容和key位置的内容进行互换。

对于这里大家可能有几个疑惑:

首先是对于key位置的选择,以及左右位置谁先走的问题,这里主要是为了让我们相遇的位置的值是小于key(左边做key),或者大于key(右边做key)(这是由于我们最后需要让左右相遇位置内容与key位置内容交换),这里小编以将key选最左边为例,给大家解释一下原因:

这里我们分类讨论一下, 对于右边一共就这两种情况相遇情况:

1.右边找到小,左边找大没有找到,左边遇到右边

2.右边找小,没有找到,直接遇到L,要么就是一个比key小的位置,或者直接到keyi

类似道理,右边做key,左边先走,相遇位置就比key要大

第二就是大家可能有疑问,为什么左右相遇的位置,就是我们key位置内容排序后应该处于的位置,这是由于排序的过程中已经让小于key位置的内容的值,放在了keyi的左边,大于的值放在了keyi值得右边,那么相遇的值就是keyi的值。

那为什么说我们快速排序需要使用到递归的思想呢?

这是由于我们每次只能固定一个值的位置,那么为了不后面的排序不影响我们已经排好序的位置,我们这里就把这个数组分为两个区间:

区间位置如下:

那么这里我们就继续把左,右区间安赵我们上面的操作进行排序,排序结束后继续分左右区间,那么这里也就类似于二叉树的前序遍历,因此我们这里使用递归思想进行区间分配排序。

那么我们这里看直接先看代码:

void PartSort1(int* a, int left, int right)
{
	if (right-left <= 0)
	{
		return;
	}
	int begin = left;
	int 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;
	PartSort1(a, begin, keyi - 1);
	PartSort1(a, keyi +1, end);
}

解释如下:

 递归返回图:

 

 2.2  挖坑法

 挖坑法的本质和hoare的版本并没有很大的不同,只是在代码实现方面有点一点点不一样,那么该具体实现过程如下:

 这里由于和hoare大佬版本基本没有很大区别这里小编就不过多说明,这里的操作还是左边找大、右边找下,然后进行坑位互换即可,我们仍然是需要区间划分,而这仅仅只需要我们以排序结束后坑位为中心划分左右区间即可,还是递归思想实现,由于这个的展开图与上面并没有什么区别,这里小编就简单的给大家讲解一下代码的一些细节

// 快速排序挖坑法
void PartSort2(int* a, int left,int right)
{
	if (left >= right)
	{
		return;
    }
	int begin = left;
	int end = right;
	int hole = left;
	int key = a[left];
	while (left < right)
	{
		while (a[right] > key)
		{
			right--;
		}
		swap(&a[left], &a[right]);
		hole = right;
		while (a[left] < key)
		{
			left++;
		}
		swap(&a[left], &a[right]);
		hole = left;
	}
	a[hole] = key;
	PartSort2(a, begin, hole - 1);
	PartSort2(a, hole + 1, end);
}

 2.3 前后指针版本

前后指针法的运行过程和之前两个版本有一点的不同,但是本质过程还是将大的值放在后面,小的值移到前面,最后来确定我们的key值所在位置。这里我们看图理解一下:

 

 这里大家可以发现该运行过程是:

1.cur找到比key小的值,++prev,cur和prev的位置的值进行交换,++cur

2.cur找到比key大的值,++cur

说明:

1.prev要么紧跟着cur(prev的下一个就是cur)

2.要么跟cur中间间隔着比key大的一段区间

过程中就是,把key大的值往右翻,比key小的值翻到左边,当cur遍历完最后一个元素,那么prev的位置就是我们key的位置,找到key的位置,我们就可以以key为标准对该的左右区间进行划分,然后继续对左右区间执行我们以上操作。对于区间的划分以及递归,这里的操作与前面并没有特别大的差别,因此这里我们主要给大家介绍一下代码的实现细节。

void PartSort3(int* a, int left,int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int keyi = left;
	int prve = left;
	int cur = prve + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prve != cur)
		{
			swap(&a[cur], &a[prve]);
		}
		cur++;
	}
	swap(&a[prve], &a[keyi]);
	keyi = prve;
	PartSort3(a, begin, keyi - 1);
	PartSort3(a, keyi + 1, end);
}

代码详解:

3.快速排序的优化

3.1 三数取中法

快速排序在最糟糕的情况下,可能会出现O(N^2)的时间复杂度的情况,就比如一下情况:

我们这里可以发现在已经排好序的情况,我们在经历完整的一次排序时,首先需要遍历N,次,然后就是N-1……,最后1次,而这个过程中递归分了N层,所以该所有次数相加就等于1+2+……+N=(1+N)*N/2那么此处的量级也就是O(N^2)。

那么实际上我们理想中的快速排序是:

 这里我们可以看到当我们类似于一棵二叉树的形式进行递归,我们的分层也就是logN层,对于没一层的排序我们是近似于N次循环,那么就让我们得时间复杂度达到了O(N*logN),那么这里需要实现的条件,也就是让我们的key值最好接近或者等于我们排序结束后的中间位置值。

那么我们如何解决此类问题呢,我们采取的优化方式就是三数取中。那么什么是三数取中算法呢?

这里指的是我们分别取,最左端序列left,最右端序列right,以及mid=(left+right)/2,这三位置的数进行大小比较,取中间大小的数放到我们的最左端做key,这样就可以在一定程度上避免O(N^2)的情况出现。

这里我们直接看代码:

int GetMid(int *a,int left, int right)
{
	int mini = (left + right) / 2;
	if (a[left] > a[mini])
	{
		if (a[mini] > a[right])
		{
			return mini;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[mini] < a[right])
		{
			return mini;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

这里我们的代码逻辑如下:

 那么我们以前后指针法,看一下优化的后代码:

// 快速排序前后指针法
void PartSort3(int* a, int left,int right)
{
	if (left >= right)
	{
		return;
	}
	int mini = GetMid(a, left, right);
	if (mini != left)
		swap(&a[left], &a[mini]);
	int begin = left;
	int end = right;
	int keyi = left;
	int prve = left;
	int cur = prve + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prve != cur)
		{
			swap(&a[cur], &a[prve]);
		}
		cur++;
	}
	swap(&a[prve], &a[keyi]);
	keyi = prve;
	PartSort3(a, begin, keyi - 1);
	PartSort3(a, keyi + 1, end);
}

3.2 小区间优化

 这里我们为什么要做这样的优化呢,这里的原因自然是由于过深的递归有着一定的局限性,那么这里我们就来一起分析一下

递归太深存在以下几个问题:

1.效率。(影响不是很大)

2.深度太深,会导致栈溢出

那么为了避免递归太深,这里我们就可以使用小区间优化的方式:

小区间优化——小区间直接使用插入排序(当我们的递归到一个区间只有十几个元素时,我们还用递归来求解,那么该效率时远远小于插入排序的,而且递归越深,递归次数越多,所以我们就可以让快速排序的区间在10多一点时,使用插入排序)

这里我们直接看代码:

void PartSort3(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	if ((right - left + 1) > 10)
	{
		int mini = GetMid(a, left, right);
		if (mini != left)
			swap(&a[left], &a[mini]);
		int begin = left;
		int end = right;
		int keyi = left;
		int prve = left;
		int cur = prve + 1;
		while (cur <= right)
		{
			if (a[cur] < a[keyi] && ++prve != cur)
			{
				swap(&a[cur], &a[prve]);
			}
			cur++;
		}
		swap(&a[prve], &a[keyi]);
		keyi = prve;
		PartSort3(a, begin, keyi - 1);
		PartSort3(a, keyi + 1, end);
	}
     //直接插入排序
	else
	{
		InsertSort(a+left,(right-left+1));
	}
	
}

这样我们几乎就消灭了最底层的三到四层递归,而对于我们这里的二路递归每增加一层就是上一层两倍,所以我们消灭底层的三四层递归,也就消灭了大部分递归,也对此起到了优化作用。

4.快速排序的非递归实现

由于递归有以上的不足之处,所以我们也要学会将递归改为非递归,我们这里一起分析一下。我们在每次递归的实现过程中,我们改变的就是一个区间的范围,所以我们这里也就是要学会去控制我们区间的范围,然后按递归的顺序去用循环实现即可。

那么为了按递归的顺序利用循环去实现,我们这里就需要去用栈这个特殊的数据结构去实现,那么对于栈不熟悉的大家可以去看看小编有关栈的介绍。

那么这里的具体过程如下:

我们发现,这样就可以合理控制区间,进行非递归实现,代码如下:

//单趟排序(前后指针法)
int PartSort(int *a,int left,int right)
{
	//三数取中
	int mini = GetMid(a, left, right);
	if (left != mini)
		Swap(&a[left], &a[mini]);
	//排序
	int cur = left+1;
	int prve = left;
	int keyi = left;
	while (cur<=right)
	{
		if (a[cur] < a[keyi] && ++prve != cur)
		{
			Swap(&a[prve], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prve], &a[keyi]);
	return prve;
}
//非递归实现快速排序
void QuickSort(int *a,int left,int right)
{
	ST  s;
	STInity(&s);
	STpush(&s, right);
	STpush(&s, left);
	while (!STEmpty(&s))
	{
		int begin = STTop(&s);
		STPop(&s);
		int end = STTop(&s);
		STPop(&s);
		int keyi=PartSort(a, begin, end);
		if (keyi+1 < end)
		{
			STpush(&s,end);
			STpush(&s, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			STpush(&s, keyi - 1);
			STpush(&s, begin);
		} 
	}

	STDestory(&s);
}

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

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

相关文章

C-关键字(下)

文章目录循环控制switch-case-break-defaultdo-while-forgetchar()break-continuegotovoidvoid*returnconstconst修饰变量const修饰数组const修饰指针指针补充const 修饰返回值volatilestruct柔型数组union联合体联合体空间开辟问题利用联合体的性质,判断机器是大端还是小端enu…

力扣javascript刷题343——动态规划之整数拆分

这几天有在开始投暑期实习的简历&#xff0c;可能确实是投的太晚了&#xff0c;好多厂都没有hc了&#xff0c;阿里简历面都没过&#xff08;感觉是kpi面试&#xff09;&#xff0c;被深深打击到了呜呜呜&#xff0c;花了两天整理情绪&#xff0c;重新出发&#xff0c;下篇文章针…

mysql 索引详解

mysql 索引索引分类1. 普通索引和唯一索引2. 单列索引和组合索引3. 全文索引4&#xff0e;空间索引操作使用索引1. 在已有表中添加索引2. 删除索引索引是一个单独存储在磁盘上的数据库结构&#xff0c;使用索引可以快速找出在某个或多个列中有一特定值的行&#xff0c;提高查询…

【C语言 -结构体 结构体声明、定义、初始化、结构体成员访问、结构体传参】

C语言 - 结构体声明、定义、初始化、结构体成员访问、结构体传参一 结构体类型的声明&#xff1a;声明格式&#xff1a;二 结构体的定义并初始化2.1用结构体创建&#xff08;定义&#xff09;结构体变量&#xff08;对象&#xff09;的两种方式&#xff1a;&#xff08;1&#…

WebRTC 系列(三、点对点通话,H5、Android、iOS)

WebRTC 系列&#xff08;二、本地 demo&#xff0c;H5、Android、iOS&#xff09; 上一篇博客中&#xff0c;我已经展示了各端的本地 demo&#xff0c;大家应该知道 WebRTC 怎么用了。在本地 demo 中是用了一个 RemotePeerConnection 来模拟远端&#xff0c;可能理解起来还有点…

HTTP协议:当下最主流的应用层协议之一,你确定不了解一下吗?

一.HTTP协议的含义http是什么&#xff1f;超文本传输协议&#xff08;Hyper Text Transfer Protocol&#xff0c;HTTP&#xff09;是一个简单的请求-响应协议&#xff0c;它通常运行在TCP之上。‘超’可以理解为除了文本之外的图片&#xff0c;音频和视频&#xff0c;和一些其他…

硬盘、文件系统相关常识

1.硬盘 以机械硬盘为例&#xff0c;下面是机械硬盘的外形结构。 结构图&#xff1a; 每个磁盘分为两个盘面&#xff0c;每个盘面中有很多磁道(Disk Track)&#xff0c;每个磁道上有很多扇区(Sector)&#xff0c;磁道上的一段一段的就是扇区。 扇区是最小的单位&#xff0c;…

Flutter开发日常练习-黑白主题

1.添加了白天黑夜模式 2.country_picker: ^2.0.20 城市信息框架 3.image_picker: ^0.8.53 photo_manager: ^2.3.0 相机和相册的调用 4.shared_preferences: ^2.0.8 sqflite: ^1.3.1 path: 数据异步持久化到磁盘 注:登录的时候记录一下登录状态isLogin,通过isLogin来标记是否…

OCR之论文笔记TrOCR

文章目录TrOCR: Transformer-based Optical Character Recognition with Pre-trained Models一. 简介二. TrOCR2.1. Encoder2.2 Decoder2.3 Model Initialiaztion2.4 Task Pipeline2.5 Pre-training2.6 Fine-tuning2.7 Data Augmentation三. 实验3.1 Data3.2 Settings3.2 Resul…

如何战胜AI?唯努力尔-- DSP算法的FPGA实现指南

如何战胜AI?唯努力尔! DSP算法的FPGA实现指南! 来一集番外。 而这 也是开坑的第一个算法&#xff01;我们先讲案例再谈实现指南 文章目录如何战胜AI?唯努力尔! DSP算法的FPGA实现指南!观前提醒实用算法原理数学原理代码模块划分与实现FIR滤波器误差计算与系数更新模块最终代…

算法 贪心2 || 122.买卖股票的最佳时机II 55. 跳跃游戏 45.跳跃游戏II

122.买卖股票的最佳时机II 如果想到其实最终利润是可以分解的&#xff0c;那么本题就很容易了&#xff01; 如何分解呢&#xff1f; 假如第0天买入&#xff0c;第3天卖出&#xff0c;那么利润为&#xff1a;prices[3] - prices[0]。 相当于(prices[3] - prices[2]) (prices[2…

HBuilderX 开发工具

介绍 uni-app 官方推荐使用 HBuilderX 来开发 uni-app 类型的项目。 主要好处&#xff1a; 模板丰富完善的智能提示一键运行 下载 HBuilderX 1、官网下载地址&#xff1a;https://www.dcloud.io/hbuilderx.html 2、下载正式版&#xff08;根据自己电脑选&#xff09; 安装…

( “树” 之 DFS) 112. 路径总和 ——【Leetcode每日一题】

112. 路径总和 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有节点值相加等于目标和 targetSum 。如果存在&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 叶子节点…

虚假评论检测可视化系统的实现

菜鸟一枚&#xff0c;大佬勿喷&#xff0c;主要是想分享&#xff0c;希望能帮到像我一样的人。 主要代码是参考&#xff1a;https://github.com/SoulDGXu/NLPVisualizationSystem/tree/master/frontend 他这个代码实现了词云、摘要生成等功能吧。因为我做的是虚假评论检测系统&…

星环科技自研技术,加速大数据从持久化、统一化、资产化、业务化到生态化

从2013年成立开始&#xff0c;星环科技就专注于大数据基础技术与企业数据业务的更好结合&#xff0c;同时面对中国更为复杂的数据应用场景&#xff0c;研发了多种更贴合国内大数据应用需求的大数据管理技术&#xff0c;在大数据技术领域有多项基础技术突破。星环科技在坚持技术…

尚硅谷大数据技术Zookeeper教程-笔记02【服务器动态上下线监听案例、ZooKeeper分布式锁案例、企业面试真题】

视频地址&#xff1a;【尚硅谷】大数据技术之Zookeeper 3.5.7版本教程_哔哩哔哩_bilibili 尚硅谷大数据技术Zookeeper教程-笔记01【Zookeeper(入门、本地安装、集群操作)】尚硅谷大数据技术Zookeeper教程-笔记02【服务器动态上下线监听案例、ZooKeeper分布式锁案例、企业面试真…

Vue3 关于setup与自定义指令

setup语法糖 最大好处就是所有声明部分皆可直接使用&#xff0c;无需return出去 注意&#xff1a;部分功能还不完善&#xff0c;如&#xff1a;name、render还需要单独加入script标签按compositionAPI方式编写 // setup 下还可以附加<script> setup语法糖独有 &…

【CocosCreator入门】CocosCreator组件 | Graphics(绘制)组件

Cocos Creator 是一款流行的游戏开发引擎&#xff0c;具有丰富的组件和工具&#xff0c;其中Graphics组件允许您在游戏中绘制2D图形和几何形状&#xff0c;并通过编写脚本来控制其外观和行为。 目录 一、组件属性 二、组件方法 三、脚本示例 一、组件属性 属性功能说明lineW…

MySQL删除数据1093错误

现象&#xff1a;在进行更新和删除操作的时候&#xff0c;条件语句里面有子查询语句&#xff0c;此时会报1093错误&#xff01; 错误日志&#xff1a;1093 - You can’t specify target table ‘t_suer_study_video’ for update in FROM clause 首先根据条件来查询 SELECT * …

动手学深度学习V2的笔记小记

自动求导 两种方式&#xff1a;正向&#xff0c;反向 内存复杂度&#xff1a;O(n) 计算复杂度&#xff1a;O(n) 线性回归 梯度下降通过不断沿着反梯度方向更新参数求解 两个重要的超参数是批量大小和学习率 小批量随机梯度下降是深度学习默认的求解算法 训练误差和泛化误差 训练…