交换排序(快排)

news2024/11/16 7:34:49

当当当当!终于来到了令人激动人心的环节:交换排序!在这里,我们将会学习到一个大家经常听到过的名词:快速排序,而我希望通过这篇文章的学习,大家也能够真正的学会快排!

交换排序

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

冒泡排序

冒泡排序的特性总结:

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

其实博主之前的文章中也有写过冒泡排序,如果大家有不了解的,可以直接跳转到此篇文章进行观看:https://blog.csdn.net/Rainai/article/details/132239596

博主在这里就不详细介绍冒泡排序啦~

快速排序

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

// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
 	if(right - left <= 1)
 		return;
 
 // 按照基准值对array数组的 [left, right)区间中的元素进行划分
 	int div = partion(array, left, right);
 
 // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
 // 递归排[left, div)
 	QuickSort(array, left, div);
 
 // 递归排[div+1, right)
 	QuickSort(array, div+1, right);
}

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,同学们在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后续只需分析如何按照基准值来对区间中数据进行划分的方式即可。

将区间按照基准值划分为左右两半部分的常见方式有:

  1. hoare版本
    在这里插入图片描述
    hoare版本的快排会有很多很多坑,我先一一为大家把坑都列出来,看看大家是否理解
//单趟
void QuickSort(int* a,int n){
	int left=0,right=n-1;
	int key=a[left];
	
	while(left<right){
		//右边找小
		while(a[right]>key){
			--right;
		}
		//左边找大
		while(a[left]<key){
			--left;
		}
		Swap(&a[left],&a[right]);
	}
	Swap(&a[left],&key);
}

这里的问题在于①相遇位置的判断:做不到相遇会停的条件,需要将left<right也写在内部while循环中②key是一个局部变量,我们期望的并不是和key交换,而是和key所在值的地址交换,所有我们直接把key改为下标③left从0开始与key是相等的,left动不了

那么我们更改后是这个样子的:

//单趟
void QuickSort(int* a,int n){
	int left=1,right=n-1;
	int key=0;
	
	while(left<right){
		//右边找小
		while(left<right && a[right]>a[key]){
			--right;
		}
		//左边找大
		while(left<right && a[left]<a[key]){
			--left;
		}
		Swap(&a[left],&a[right]);
	}
	Swap(&a[left],&a[key]);
}

当我们把单趟排好之后,我们的左边是小的,右边是大的,如果左边和右边也有序,那么就可以达成总体有序

这个时候,我们可以把其分为左子树,根,右子树的形式来看,就拿动图的例子来讲:此时6就相当于根节点,而左右两边就是左子树和右子树,接下来再对左右两边也按照这样的方法来操作即可
在这里插入图片描述

就拿左边的3 1 2 5 4举例,3是a[key],如果进行交换,右边找小到2,左边也找大,找不到,到2相遇,然后2和3交换,刚好成为2 1 3 5 4,这个时候需要排的就是2 1和5 4了,向左找根;然后就是2和1换,5和4换,最后只有1或者4的时候,我们就不需要再换啦,而这个范围,我们可以大致理解为这样子:[begin,key-1] key [key+1,end]
在这里插入图片描述
所以,我们发现,如果要走全趟,就需要①递归②更改函数结构

void QuickSort(int* a,int begin,int end){
	if(begin>=end)
		return;//递归判断条件
	
	int left=begin+1,right=end-1;
	int key=begin;
	
	while(left<right){
		//右边找小
		while(left<right && a[right]>a[key]){
			--right;
		}
		//左边找大
		while(left<right && a[left]<a[key]){
			--left;
		}
		Swap(&a[left],&a[right]);
	}
	Swap(&a[left],&a[key]);
	key=left;//换了位置,下标也要换咧
	
	QuickSort(a,begin,key-1);
	QuickSort(a,key+1,end);
}
	//[begin,key-1] key [key+1,end]这个区间可能不存在
	//拿之前的2 1 3 5 4举例,5和4的下标是3和4
	//begin是3,key-1是3,key是4,key+1是5,end是4
	//这种区间左边就是一个值的区间,右边就是不存在的区间

大家是不是以为这样子快排就写好了,可是事实上我们还有一些坑仍未填好,大家别着急,我们继续往下看

这个时候假如我们把数组改为这个样子:6 1 2 6 7 9 3 4 6 10 8,启动程序会发现进入了死循环,这又是为什么呢?
左边找大,右边找小,左边从1开始,右边从8开始,8找,找到6停下了;左边也是找到6停下了,然后就是6和6换,没有意义,所以我们完全可以把while的判断条件加上一个=
修改之后,我们还会发现另一个问题,1 2 3 4 6 6 7 6 8 9 10是输出的结果,为什么7会在6的前面呢?我们经过调试,会发现左边实际上是没有问题的,但在右边递归的时候,一开始是9 7 6 10 8,变成8 7 6 9 10,left和right会在下标为8的位置上相遇,在排序8 7 6的时候出现了问题,进行第一次swap的时候,成为了6 7 8 9 10,是没有问题的,但是在递归左边时,6和7直接就进行了互换,因为右边要找小,而有序的情况下就会导致直接互换
在这里插入图片描述
在这里插入图片描述
所以我们再次进行修改:

void QuickSort(int* a,int begin,int end){
	if(begin>=end)
		return;//递归判断条件
	
	int left=begin,right=end;
	int key=begin;
	
	while(left<right){
		//右边找小
		while(left<right && a[right]>=a[key]){
			--right;
		}
		//左边找大
		while(left<right && a[left]<=a[key]){
			--left;
		}
		Swap(&a[left],&a[right]);
	}
	Swap(&a[left],&a[key]);
	key=left;
	
	QuickSort(a,begin,key-1);
	QuickSort(a,key+1,end);
}

如果想随时更换使用方法,那么就可以这么写,当然,我们得先把hoare版本的方法作为函数提取出来

//hoare版本
int PartSort1(int* a, int begin, int end)
{
	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);

	int left = begin, right = end;
	int keyi = begin;

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

而下面这个就是快排大概的一个模板写法:

void QuickSort(int* a, int begin, int end) {
	if (begin >= end)
		return;

	int key = partSort(a, begin, end);
	QuickSort(a, begin, key - 1);
	QuickSort(a, key + 1, end);
}

只要更改partSort的方法,而这个模板并不需要改变就可以测试效果,使用起来肯定比每次都要写一大堆快捷方便

虽然此刻快速排序的功能已经发展齐全,但是其中还有一些需要控制的问题。比如当我们测试代码的运行效率时,虽然正常的时候,其量级都是NlogN,尤其是当排序的数据出现二分的情况,但是如果代码本身有序,那么其效率就会低的离谱,成为N^2,如果要排序的数据量过大,甚至还会出现栈溢出,这个时候就需要我们手动写出一个函数,在每次排序的时候将一个不大也不小的值当作key,就不会出现效率低下的问题了

int GetMid(int* a, int begin, int end) {
	// 在begin和end之间选择一个中位数  
	int mid = (begin + end) / 2;
	if (a[begin] <= a[mid] && a[mid] <= a[end]) {
		return mid;
	}
	else if (a[begin] > a[mid]) {
		// 如果a[begin]大于a[mid],那么中位数要么在a[begin]的位置,要么在a[end]的位置  
		return (a[begin] <= a[end]) ? begin : end;
	}
	else {
		// 如果a[begin]小于a[mid],那么中位数要么在a[mid]的位置,要么在a[end]的位置  
		return (a[mid] <= a[end]) ? mid : end;
	}
}

而在这个部分的最后,我们还要讲的一个点是:为什么left和right的相遇位置总是比key要小?结论是右边先走,我们给大家分析一下:
在这里插入图片描述
大家自己画画图也会发现,情况就是这样的

以上我们讲解的方法其实就是第一种快排的方法,是hoare写出来的,所以也叫做hoare法,这个方法大家会发现有很多坑,所以实际情况下我们并不建议去写这种方法,而以下的两个方法,相信大家看了之后相比较于这个会觉得非常简单

  1. 挖坑法
    在这里插入图片描述

首先,选择数列中的一个位置作为起始点,并标记为“坑位”。然后,从数列的两端开始,分别向这个坑位移动。(比如这里就是右边先移)在移动过程中,如果左边的数字比坑位上的数字大,或者右边的数字比坑位上的数字小,就继续移动。一旦左边的数字小于等于坑位上的数字,或者右边的数字大于等于坑位上的数字,就将当前位置与坑位交换。重复这个过程,直到左边的指针和右边的指针相遇。最后,将起始位置的值填入相遇点。(坑位上的数字就是一开始取出来的key)

代码如下:

int partSort2(int* a, int begin, int end) {
	int mid = GetMid(a, begin, end);
	Swap(&a[mid], &a[begin]);

	int key = a[begin];//key值
	int hole = begin;//起始坑位
	while (begin < end) {
		//右边找小,填到左边的坑
		while (begin < end && a[end] >= key) {
			--end;
		}

		a[hole] = a[end];//找到小的了,把end位置的值填进原来的坑里
		hole = end;//现在的end位置变成坑

		//左边找大,填到右边的坑
		while (begin < end && a[begin] <= key) {
			++begin;
		}

		a[hole] = a[begin];//找到大的了,把begin位置的值填进坑里
		hole = begin;//现在的begin位置变成坑
	}
	a[hole] = key;//最后把一开始取出的坑位值塞进洞里(begin和end相交后)
	return hole;//返回坑位下标
}
  1. 前后指针法

上述的两个方法实际上的想法都是差不多的,但下面的指针法,跟上面的两种方法思想就不太一样了
在这里插入图片描述

通过上面的图,我们也可以概括以下两点:
1.cur遇到比key大的值,++cur
2.cur遇到比key小的值,++prev,交换prev和cur位置的值,++cur

int partSort3(int* a, int begin, int end) {
	int mid = GetMid(a, begin, end);
	Swap(&a[mid], &a[begin]);

	int keyi = begin;
	int prev = begin;
	int cur = begin + 1;

	while (cur <= end) {
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[prev], &a[cur]);//为什么不推荐先找小,如果都比它大,就可能会跑出去,越界了

		++cur;//因为两种情况下都涉及到++cur
	}

	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;//这样写逻辑更清晰,其实可以直接return prev
}

另外,除了以上几种方法,我们还需要了解一个优化的小tips,虽然在release版本下面并无法起到太大的作用

我们知道满二叉树最后一层占领的节点是将近所有节点一半的节点,引申到快排,我们会发现,在排序的过程中,也是最后几次排序消耗更多,我们不断分割并且对左边展开递归,比如,为了让7个数有序,我们会这样做:
在这里插入图片描述
那么7个数就递归了7次,这样子的话,消耗的栈空间会很多,为了减少栈空间的消耗,我们选择小区间优化,选择最后三层到四层进行插入排序;不过需要记住的是,虽然是优化,但有时候效果并不会特别好,因为这两种方法的效率差距就是减少的递归和增加的插入排序的性能的差距,所以用与不用全看个人

void quickSort(int* a, int begin, int end) {
	if (begin >= end) {
		return;
	}

	//小区间优化 选择最后三到四层
	if (end - begin + 1 <= 10) {
		InsertSort(a+begin, end - begin + 1);//这里不是从a开始的,而是从a+begin开始的
	}

	else {
		int mid = GetMid(a, begin, end);
		Swap(&a[begin], &a[mid]);

		int left = begin, right = end;
		int key = begin;
		while (left < right) {
			//右边找小
			while (left < right && a[right] >= a[key]) {
				--right;
			}
			//左边找大
			while (left < right && a[left] <= a[key]) {
				++left;
			}

			Swap(&a[left], &a[right]);
		}
		Swap(&a[left], &a[key]);
		key = left;

		quickSort(a, begin, key - 1);
		quickSort(a, key + 1, end);
	}
}

下面我们将要进行另外一个重要方法的讲解,非递归快排!

非递归快排(栈)

在以前,我们用非递归实现递归的时候,就需要使用到循环,比如斐波那契数列等等,但可以用循环实现的,大概率也没有必要使用递归,所以这里我们要介绍一种新方法:利用栈来仿造递归的效果

栈利用的是堆空间,比起递归使用的栈空间来说很大,完全可以使用

在这里插入图片描述
就比如我们还是拿刚才的数组来举例,我们先让0和9进栈,然后它出栈之后key=5,分割为[0,4]和[6,9],[0,4]后进的原因是因为我们这里规定的是先递归左边的,由于栈又是后进先出,所以这样安排;0,4出出来之后,其key为2,所以范围变为[0,1]和[3,4],先出0,1之后发现其后面的范围变为[0,0](单元素区间)和[2,1](不存在区间),则出栈后后面不再入栈,3和4同理。

有些同学会想需不需要把栈的元素类型改成数组,这样子一组才能塞两个元素进去,其实也没有必要,看完代码你就明白了!

//非递归快排
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 = PartSort(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);
}

运行之后,其实结果也是一样的,以上几种方式在效率方面均没有太大差距,只是实现的思路不大相同。而且,虽然有些地方思想上看的明白,但是轮到自己写时也还是会不太清晰,所以希望大家也可以自己多看多写,以达到真正的理解!

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

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

相关文章

SkyWalking+es部署与使用

第一步下载skywalking :http://skywalking.apache.org/downloads/ 第二步下载es:https://www.elastic.co/cn/downloads/elasticsearch 注&#xff1a;skywalking 和es要版本对应&#xff0c;可从下面连接查看版本对应关系&#xff0c;8.5.0为skywalking 版本号 Index of /di…

Scala入门01

Spark入门 1.入门 spark采用Scala语言开发 Spark是用来计算的 Scala掌握&#xff1a;特性&#xff0c;基本操作&#xff0c;集合操作&#xff0c;函数&#xff0c;模式匹配&#xff0c;trait&#xff0c;样例类&#xff0c;actor等内容。 2.内容讲解 2.1 Scala简介 在http…

idea报错(建一个数据库和里面的表)

//报错1 2024-01-30 11:36:50.652 ERROR 21136 --- [nio-9090-exec-5] c.e.exception.GlobalExceptionHandler : 异常信息&#xff1a; 前端没有传数据就会报这个错 //报错2 Caused by: java.sql.SQLException: Access denied for user rootlocalhost (using password: YES…

【UEFI实战】Redfish的BIOS实现——生成EDK数据

生成Redfish文件 Redfish数据的表示形式&#xff0c;最常用的是JSON。将JSON表示的数据转换成C语言可以操作的结构体&#xff0c;是必不可少的步骤。当然如果手动转换的话&#xff0c;需要浪费大量的时间&#xff0c;因此DMTF组织开发了一个工具&#xff0c;用于将JSON数据快速…

redis-4 集群

应用场景 为什么需要redis集群&#xff1f; 当主备复制场景&#xff0c;无法满足主机的单点故障时&#xff0c;需要引入集群配置。 一般数据库要处理的读请求远大于写请求 &#xff0c;针对这种情况&#xff0c;我们优化数据库可以采用读写分离的策略。我们可以部 署一台主服…

论文笔记:多任务学习模型:渐进式分层提取(PLE)含pytorch实现

整理了RecSys2020 Progressive Layered Extraction : A Novel Multi-Task Learning Model for Personalized Recommendations&#xff09;论文的阅读笔记 背景模型代码 论文地址&#xff1a;PLE 背景 多任务学习&#xff08;multi-task learning&#xff0c;MTL&#xff09;&a…

【C/C++ 01】初级排序算法

排序算法通常是针对数组或链表进行排序&#xff0c;在C语言中&#xff0c;需要手写排序算法完成对数据的排序&#xff0c;排序规则通常为升序或降序&#xff08;本文默认为升序&#xff09;&#xff0c;在C中&#xff0c;<algorithm>头文件中已经封装了基于快排算法的 st…

Leetcode3015. 按距离统计房屋对数目 I

Every day a Leetcode 题目来源&#xff1a;3015. 按距离统计房屋对数目 I 解法1&#xff1a;暴力 暴力枚举每一个房屋对 (i, j) 的 3 种路径&#xff1a; i->j&#xff1a;长度为 len1 j-i&#xff1b;i->x->y->j&#xff1a;长度为 len2 abs(i - x) 1 a…

解决Linux环境下gdal报错:ERROR 4: `/xxx.hdf‘ not recognized as a supported file format.

网上查了一堆资料&#xff0c;五花八门&#xff0c;总结了一下可能的原因&#xff1a; ① gdal不支持该格式 使用命令“gdalinfo --formats” 即可查看当前环境中的gdal所能支持的数据格式。如下图&#xff08;没截完整&#xff0c;下面还有一大串&#xff09;。 这个是很常见…

Java代码混淆加密之ClassFinal

一:介绍 ClassFinal是一款java class文件安全加密工具,支持直接加密jar包或war包,无需修改任何项目代码,兼容spring-framework;可避免源码泄漏或字节码被反编译。 二:功能特性 无需修改原项目代码,只要把编译好的jar/war包用本工具加密即可。运行加密项目时,无需求修…

iText操作pdf

最近有个任务是动态的创建pdf根据获取到的内容&#xff0c;百度到的知识点都比较零散&#xff0c;官方文档想必大家也不容易看懂。下文是我做出的汇总 public class CreatePdfUtils {public static void create(){//准备File file new File("C:\\code\\base-project-back…

STM32矩形(矩阵)按键(键盘)输入控制LED灯 ——4*4矩阵按键源码解析

本文基于标准函数库的工程实现stm32F103C8T6使用4*4的矩阵按键控制LED灯的亮灭及闪烁等功能。 程序源码&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1_MPhvMduKCTP0MPG-Gtw3A?pwd2syk 提取码&#xff1a;2syk 文章目录 一、矩形键盘介绍 1、硬件电路基本原理 …

Sketch 英文转中文:轻松搞定

Sketch版本的转换一直是每个人的关键问题。现在UI设计领域有很多UI设计软件&#xff0c;但大部分都是英文版。对于国内英语基础差的设计师来说&#xff0c;使用这样的软件无形中增加了工作量&#xff0c;往往需要在设计编辑的同时查阅翻译。中文Sketch版本替代即时设计详细介绍…

动手学RAG:汽车知识问答

原文&#xff1a;动手学RAG&#xff1a;汽车知识问答 - 知乎 Part1 内容介绍 在自然语言处理领域&#xff0c;大型语言模型&#xff08;LLM&#xff09;如GPT-3、BERT等已经取得了显著的进展&#xff0c;它们能够生成连贯、自然的文本&#xff0c;回答问题&#xff0c;并执行…

常用网址备份

阿里git下载镜像 (npmmirror.com 主页有其他资源)https://registry.npmmirror.com/binary.html?pathgit-for-windows/

【Java】Spring的APO及事务

今日目标 能够理解AOP的作用 能够完成AOP的入门案例 能够理解AOP的工作流程 能够说出AOP的五种通知类型 能够完成"测量业务层接口万次执行效率"案例 能够掌握Spring事务配置 一、AOP 1 AOP简介 问题导入 问题1&#xff1a;AOP的作用是什么&#xff1f; 问题2&am…

Phoncent博客,探索Rie Kudan的GPT创作之举

近日&#xff0c;大家都在谈论日本作家Rie Kudan&#xff0c;她凭借其小说《东京共鸣塔》&#xff08;"Tokyo-to Dojo-to"&#xff09;荣获了日本极具声望的芥川奖。这本小说引起了广泛的讨论和思考&#xff0c;因为令人惊讶的是&#xff0c;Kudan在其中直接引用了人…

Python(19)Excel表格操作Ⅰ

目录 导包 读取EXCEL文件 1、获取worksheet名称 2、设定当前工作表 3、输出目标单元格数据 4、工作表.rows&#xff08;行&#xff09; 5、工作表.columns&#xff08;列&#xff09; 小结 导包 要想使用 python 操作 Excel 文件&#xff0c;应当导入 openpyxl 包。在…

Java 面试题之 IO(二)

字符流 文章目录 字符流Reader&#xff08;字符输入流&#xff09;Writer&#xff08;字符输出流&#xff09; 文章来自Java Guide 用于学习如有侵权&#xff0c;立即删除 不管是文件读写还是网络发送接收&#xff0c;信息的最小存储单元都是字节。 那为什么 I/O 流操作要分为字…