排序算法第三辑——交换排序

news2024/11/25 12:26:16

目录

​编辑

一,交换排序算法的简介

二,冒泡排序

冒泡排序代码:排升序

三,快速排序

1.霍尔大佬写的快速排序

2.挖坑法

3.前后指针法

四,以上代码的缺陷与改正方法

三数取中

三路划分:

五,随机数取中

六,快速排序的非递归


 

一,交换排序算法的简介

交换排序,顾名思义便可以知道这是一种通过交换来实现排序的算法。在这这种算法中有一种最简单最好理解的算法叫做冒泡排序。但是这种算法的效率十分低下,在现实生活中不足为用。还有一种算法叫做快速排序,这种算法的效率很高被广泛应用。并且,这种算法有许多个版本。

二,冒泡排序

虽然冒泡排序效率十分低下,但是我仍然要介绍一下这种算法。这种算法的精华一句话便可以总结出来:相邻的两两比较,遇到不相等的便交换。

冒泡排序代码:排升序

void swap(int* p, int* q)
{
	int tmp = *p;
	*p = *q;
	*q = tmp;
}
void BubbleSort(int* a, int n)
{
	int flag =1;
	for (int i = 0;i < n;i++)
	{
		for (int j = 0;j < n-i-1;j++)
		{
			if (a[j] > a[j+1])
			{
				swap(&a[j], &a[j+1]);
				flag = 0;
			}
		}
		if (flag)
		{
			break;
		}
	}
}

三,快速排序

快速排序,虽然与冒泡排序同样是交换排序算法的一种但这种排序算法却比冒泡排序的效率快的多。并且快速排序的写法也有三种:

1.霍尔大佬写的快速排序

2.挖坑法

3.指针法
 下面我就来对这三种快速排序的写法进行一一介绍。

1.霍尔大佬写的快速排序

代码:

int PartSort1(int* a, int left, int right)
{
	int keyi = left;//将数组的第一个数据作为关键值
	while (left < right)
	{
		while (right > left && a[right] >= a[keyi])//右边找小于关键值的元素
		{
			right--;
		}
		while (right > left && a[left]<= a[keyi])//左边找大于关键值的元素
		{
			left++;
		}
		swap(&a[left], &a[right]);//找到后交换左右的值让小的在右边,大的在左边
	}
	swap(&a[keyi], &a[left]);//循环结束后,left与right是相交的,所以在这个相交的位置放上关键值再在关键值原来的位置上放上原相交位置上的值
	keyi = left;
	return keyi;//返回相遇位置
}
//递归调用
void QuickSort(int* a, int begin,int end)
{
	if (begin >= end)//当范围为0或范围不存在时便返回
	{
		return;
	}

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

	QuickSort(a ,begin, keyi - 1);//调整左区间
	QuickSort(a , keyi + 1, end);//调整右区间

}

霍尔大佬写的快速排序可能就是第一代快速排序算法了。这个排序算法的主要思想就是将比a[keyi]小的值放在左边,将比a[keyi]大的值放在右边。最后将左右指针相遇的位置与a[keyi]交换。

但是这个算法有几个坑:
1.一般我们在选keyi时选择的都是最左边或者最右边的值。在这里左右两个指针走的顺序就有讲究了:

1.当我们选择了最左边的值作为keyi时,我们就要让right先走,

当我们选择了最右边的值作为keyi时,我们就要让left先走。

2.left与right指针的指向也要保证指向最左边或者最右边的值,否则就会发生错误。

2.挖坑法

因为霍尔大佬写的代码有很多需要谨慎处理的地方,所以就有人写了一个对霍尔大佬的代码进行优化的算法——挖坑法。代码如下:

代码:

int PartSort2(int* a, int left, int right)
{
	int key = a[left];//记录关键值
	int hole = left;//初始化坑位
	while (left < right)
	{   //在右边找能够填坑的值并填坑
		while (left < right && a[right] >= key)
		{
			right--;
		}
		swap(&a[right], &a[hole]);
		hole = right;
		//在右边找能够填坑的值并填坑
		while (left < right && a[left] <= key)
		{
			left++;
		}
		swap(&a[hole], &a[left]);
		hole = left;
	}
   //找到最后一个坑,将关键值填入并返回关键值的下标
	swap(&a[hole], &a[left]);
	hole = left;
	return hole;
}
void QuickSort(int* a, int begin,int end)
{
	if (begin >= end)
	{
		return;
	}

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

	QuickSort(a ,begin, keyi - 1);
	QuickSort(a , keyi + 1, end);

}

这个算法便不再需要像霍尔大佬的算法一样顾虑那么多。但是这个算法的循环还是多了点。死循环的概率还是大了点。为了减少循环,又有人对这些代码进行优化。

3.前后指针法

指针法可谓是这三种快速排序的写法中最简单的一种写法了。这个算法的大概思想如下:

1.用一个keyi记录最左边或者最右边的值的下标

2.再定义两个指针cur与prev。这两个指针的指向如下:

3.把cur当作一个探路者,cur无条件的向前冲。当cur感知到自己指向的数据比a[keyi]小的时候便停下来交换cur与++prev指向的数据。当cur大于右边的边界条件时便结束循环。

代码如下: 

代码:

int PartSort3(int* a, int left, int right)
{ 
    
    
	int keyi = left;
	int prev = left;
	int cur = left + 1;

	while (cur<=right)
	{   //这里的prev一定要先加加
		if (a[cur] <a[keyi]&&++prev!=cur)
		{
		
			swap(&a[cur], &a[prev]);
		}
		cur++;
	}

	swap(&a[keyi], &a[prev]);
	keyi = prev;
	return keyi;
}
//递归调用
void QuickSort(int* a, int begin,int end)
{
	if (begin >= end)
	{
		return;
	}

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

	QuickSort(a , begin, keyi - 1);
	QuickSort(a , keyi + 1, end);

}

这个指针法便减少了代码内的循环可以大大的减少死循环的风险!!!

四,以上代码的缺陷与改正方法

虽然上面的代码可以很快的将一些数据进行排序,但是对于一些比较极端的场景下上面的代码排序起来就会变得十分吃力。比如:

1.当排序的数据是有序的数据时,上面的代码排序起来的时间复杂度就会是O(n2)

2.当排序的数据的量十分多,并且重复的数据也十分多时,上面的快速排序就会排不出来有栈溢出的风险。

针对上面的两个问题,可以给出下面的解决方案:

1.三数取中

2.三路划分

三数取中

三数取中针对的场景是数据有序时的场景,比如:

int main()
{   //创建一个有序的数组
	int N = 10000000;
	int* a = (int*)malloc(sizeof(int) * N);
	for (int i = 0;i < N;i++)
	{
		a[i] = i;
	}
	//计算排序时间
	int begin = clock();
	QuickSort(a, 0, N-1);
	int end = clock();
	printf("QuickSort:%d ", end - begin);
	return 0;
}

要排序这串数据在我的编译器上会崩掉,崩掉的原因是栈溢出。

现在来加一段三数取中代码:

//三数取中,就是在左,中,右三个数中取中间值
int GetMid(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[mid] < a[left])
	{
		if (a[right] > a[left])
		{
			return left;
		}
		else if (a[mid] > a[right])
		{
			return mid;
		}
		else
		{
			return right;
		}
		
	}
	else
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if(a[left]>a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
//加入三数取中操作的快速排序
int PartSort1(int* a, int left, int right)
{
    //取出中间值然后与最左边的值交换让keyi指向中间元素
	int mid = GetMid(a, left, right);
	swap(&a[left], &a[mid]);
	int keyi = left;
	while (left < right)
	{
		while (right > left && a[right] >= a[keyi])
		{
			right--;
		}
		while (right > left && a[left]<= a[keyi])
		{
			left++;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[keyi], &a[left]);
	keyi = left;
	return keyi;
}

效果:

虽然排起来并不算快,但是总算是排出来了。

三路划分:

 虽然上面的代码把有序的情况给排出来了,但是还有一种极端情况三数取中这个解决办法是解决不了的。那便是存在大量的重复元素的时候。比如:

int main()
{   //创建一个数据全是2的数组
	int N = 10000000;
	int* a = (int*)malloc(sizeof(int) * N);
	for (int i = 0;i < N;i++)
	{
		a[i] = 2;
	}
	//计算排序时间
	int begin = clock();
	QuickSort(a, 0, N - 1);
	int end = clock();
	printf("QuickSort:%d\n ", end - begin);
	return 0;
}

这个数组如果用三数取中的话是不可能取到中间数的,所以三数取中没有用。如果按照原代码那样写的话也会栈溢出。所以有大佬就想到了三路划分的解决方法。

三路划分:

这个方法的思想和这个方法的名字一样:

就是要将一组数据划分成三路:左路,中路,右路。当我们再次递归时只需要递归左路与右路。这样做便可以大大减少递归的深度,防止栈溢出。

实现思想:

1.定义三个指针:cur,left,right.同时用key记录a[left]的值。

2.当a[cur]小于key时便将a[cur]与a[left]交换,同时left++,cur++。

   当a[cur]大于key时便将a[cur]与a[right]交换,right--,cur不动。

   当a[cur]与key相等时,cur++,left与right不动。

3.当cur超过right时循环停止。

4.再次排序时排序的范围是:[begin,left-1],[right+1,end]

写成代码如下:

代码:

//三路划分
void QuickSort2(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
     }

	int left = begin;
	int right = end;
	int cur = begin + 1;
    
    //三数取中
	int midi = GetMid(a,left, right);
	swap(&a[left], &a[midi]);

    //三路划分,开始甩值到左右两边
	int key = a[left];
	while (cur <= right)
	{
		if (a[cur] < key)
		{
			swap(&a[cur], &a[left]);
			left++;
			cur++;
		}
		else if (a[cur] > key)
		{
			swap(&a[cur], &a[right]);
			right--;
		}
		else
		{
			cur++;
		}
	}
    
    //只递归两边的区间
	QuickSort2(a, begin, left - 1);
	QuickSort2(a, right + 1, end);
}

 这个代码便可以跑过有大量重复数据的数组排序,也能跑过有序的大量数据。

但是当我们要跑这个oj排序数组 时出现这种情况:

 虽然测试用例都过了,但还是没有将这道题通过,因为时间限制。

五,随机数取中

 为了通过这道OJ题,这里又得来上一个随机数取中的操作。因为在这道题的用例中可能存在着一些用例非常的接近导致取中的操作效率变低。所以为了提高效率,我们便可以采用随机数取中。

代码:

int GetMid(int* a, int left, int right)
{
    //只是改了这里
    srand(time(0));
	int mid = left+rand()%(right-left);
	if (a[mid] < a[left])
	{
		if (a[right] > a[left])
		{
			return left;
		}
		else if (a[mid] > a[right])
		{
			return mid;
		}
		else
		{
			return right;
		}
		
	}
	else
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if(a[left]>a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

结果:

 过啦!!!

六,快速排序的非递归

 前面我们写的快速排序都是递归版本的。递归版本的代码虽然简单但是递归调用的栈空间却比较小,很容易就会栈溢出。所以为了避免栈溢出的风险,我们便要实现一个非递归版本的快速排序算法。这个非递归写法的快速排序要用到一个数据结构就叫作栈(这个栈是我们定义的,这是一个堆上的栈)。非递归版本代码如下:

代码:

void QuickSortNR(int* a, int begin, int end)
{   //定义栈并初始化栈
	Stack stack;
	StackInit(&stack);
    //在栈中插入begin和end这个区间,牢记栈后进先出的特点。
	StackPush(&stack, end);
	StackPush(&stack, begin);

	while (!StackEmpty(&stack))
	{
        
		int left = StackTop(&stack);
		StackPop(&stack);
		int right = StackTop(&stack);
		StackPop(&stack);
        //根据区间排序
		int keyi = PartSort1(a, left, right);
        
        //判断区间是否存在再插入区间(先右后左,因为栈的特点是后进先出)
		if (keyi + 1 < right)
		{
			StackPush(&stack, right);
			StackPush(&stack, keyi + 1);
		}
		if (keyi - 1 > left)
		{
			StackPush(&stack, keyi - 1);
			StackPush(&stack,left);
		}
	}
    //栈销毁
	StackDestroy(&stack);

}

快速排序的非递归版本需要使用到数据结构中的栈,所以为了实现非递归版本的快速排序我们就得自己定义一个栈。这个栈的作用就是用来分割排序区间的

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

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

相关文章

真的绝了,通过注释来埋点好简单!!

目录 回顾 开始 插件编写 功能一 功能二 功能三 合并功能 运行代码 总结 这篇文章主要讲如何根据注释&#xff0c;通过babel插件自动地&#xff0c;给相应函数插入埋点代码&#xff0c;在实现埋点逻辑和业务逻辑分离的基础上&#xff0c;配置更加灵活 回顾 上篇文章…

微服务系列文章之 Springboot应用在k8s集群中配置的使用

Docker部署其实也可以再docker run或者dockerfile里面&#xff0c;将配置文件目录映射到宿主机&#xff0c;然后通过宿主机配置文件修改参数。 FROM docker.io/python:3.6MAINTAINER tianye # 设置容器时间 RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime &&am…

Windows安装Oh-My-Posh美化Powershell

Windows Terminal&#xff1a;https://www.microsoft.com/store/productId/9N0DX20HK701 最新Powershell下载&#xff1a;https://github.com/PowerShell/PowerShell/releases Oh-My-Posh官网&#xff1a;https://ohmyposh.dev/ Nerd字体下载&#xff1a;https://www.nerdfonts…

Git源代码管理方案

背景 现阶段的Git源代码管理上有一些漏洞&#xff0c;导致在每次上线发布的时间长、出问题&#xff0c;对整体产品的进度有一定的影响。 作用 新的Git源代码管理方案有以下作用&#xff1a; 多功能并行开发时&#xff0c;测试人员可以根据需求任务分配测试自己的功能&#…

MyBatis-Plus条件查询问题解决

问题描述 系统中有用户注册的功能&#xff0c;但是会出现重复注册的现象&#xff0c;代码中有做过重复校验&#xff0c;但是没有生效。 问题解决 首先排查数据生成时间点不同&#xff0c;相差时间有长有短&#xff0c;不是用户同时多次点击的原因&#xff0c;应该是用户这边…

js判断两个数组是增加还是删除

JS判断两个数组的数据&#xff0c;增加的数据以及删除的数据。 // 第一个参数是新数组&#xff0c;第二个参数是旧数 const compareArrays function(arr1, arr2 ) {let remove []let add []// 旧数据循环for (let i 0; i < arr2.length; i) {let item arr2[i];if (arr…

EvilBox---One靶机复盘

EvilBox—One靶机复盘 这个靶场学会了原来id_rsa的私钥可以直接爆破&#xff0c;利用ssh2john工具提取私钥&#xff0c;然后john直接爆破就可以了。 靶场下载地址&#xff1a;https://download.vulnhub.com/evilbox/EvilBox—One.ova 这个靶场是直接给ip地址的我们就不用扫描…

Spring Boot使用httpcomponents实现http请求

基于org.apache.httpcomponents的httpclient实现&#xff0c;其它的实现方式都行。 1. pom文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/…

快速实现主从表编辑,实现多个不定长从表

目录 1 前言 2 不定长表、定长表的定义 3 根据已有的电子表格制作数据库表并导入数据 3.1 订单 3.2 订单明细 3.3 客户 4 配置主从关联关系 5 继续增加一个主从关联关系 6 测试一下运行结果 7 一段代码用于实现在panel中画出字段列表面板 1 前言 实际工作中&#xff…

扑克牌检测Y8S

采用YOLOV8训练&#xff0c;得到PT模型&#xff0c;然后直接转ONNX&#xff0c;使用OPENCV的DNN&#xff0c;不需要其他依赖&#xff0c;支持C/PYTHON 扑克牌检测Y8S

数据结构(王道)——线性表的存储结构之顺序表

线性表和顺序表的关系&#xff1a; 两种实现方式&#xff1a;静态分配、动态分配 总结&#xff1a;

Win7如何合并C盘与D盘?

电脑C盘不够用&#xff0c;需要把D盘的卷删除并合并到C盘中。 整体步骤&#xff1a; 1.右击此电脑-管理-磁盘管理。 2.假如要把D盘容量合并到C盘&#xff0c;右击D盘&#xff0c;选择删除卷&#xff0c;点击“是”。此时D盘数据会被清空&#xff0c;建议先备份数据。 3.删除…

消息队列MQ入门理解

功能特性: 物联网应用 物联网设备通过微消息队列(LMQ)连接云端,双向通信,数据传输;设备数据通过消息队列(MQ)连接计算引擎,分析数据或者源数据实时高效写入到 HiTSDB / HiStore / ODPS 等。 大规模缓存同步 在商业大促活动中,如“双11”大促,各个分会场会有琳琅…

基于输出调节的一致性编队控制

参考博客&#xff1a; https://blog.csdn.net/weixin_44346182/article/details/131747082 在输出调节的基础上&#xff0c;实现了输出一致性&#xff0c;而编队控制就是没有偏移量的状态一致性&#xff0c;恰好本题的C矩阵就是系统的第一阶的状态&#xff08;位置&#xff0…

spring boot security自定义权限检查

前言 鉴权主要分为身份认证和权限控制两部分&#xff1a; 身份认证&#xff1a;检查当前用户是否合法&#xff08;比如已登录&#xff09; 权限控制&#xff1a;检查当前用户是否有访问该资源的权限 本文主要给出一个示例&#xff0c;说明如何自定义权限控制。 因为一个完整的…

增长能力模型:最大限度地发挥增长团队的潜力

作为增长领导者&#xff0c;准确评估你的组织和团队成员是一项非常重要且耗时的工作。但是如果我们计划为未来的增长领导者创造一条职业道路&#xff0c;我们就需要一个流程来为整个团队提供客观的反馈&#xff0c;这就是增长能力模型的用武之地。 增长能力模型提供了一个框架&…

argc,argv

文章目录 argc 是argument count的缩写表示传入main函数中的参数个数&#xff0c;包括这个程序本身 argv 是 argument vector的缩写表示传入main函数中的参数列表&#xff0c;其中argv[0]:程序的名字(.exe) int main(int argc, char** argv) {std::cout << "argc:…

1、环境搭建

下载visual studio,社区版基本够用了,其他版本是收费的。 下载之后安装即可。 安装完成之后要求我们选择要使用的模块。这里我们勾选如图中圈起来的模块,并选择安装。因为我这边已经安装过,所以按钮变成了“关闭” 安装完毕后,重启。 重启完成之后,打开Visual studio。…

配置JDK_IDEA

配置JDK_IDEA 1.安装JDK配置环境变量1.1.安装JDK1.2.配置JDK环境变量 2.IDEA 1.安装JDK配置环境变量 1.1.安装JDK 链接: 下载JDK 1.2.配置JDK环境变量 2.IDEA 链接: IDEA官网 自行激活

分布式运用——ELK 企业级日志分析系统

分布式运用——ELK 企业级日志分析系统 一、ELK 简介1.ELK的主要组件2.可以添加的其它组件3.为什么要使用 ELK4.完整日志系统基本特征5.ELK 的工作原理&#xff1a; 二、ELK集群部署三、ELK Elasticsearch 集群部署&#xff08;在Node1、Node2节点上操作&#xff09;1&#xff…