【排序】快速排序(递归和非递归)

快速排序

    • 前言
    • 图解
    • 大致思路
      • 对于hoare版本
      • 对于挖坑法
      • 对于前后指针法
    • 实现方法
      • 递归
      • 非递归
    • 快排的优化(基于递归的优化)
      • 三数取中法
      • 小区间优化
    • 时间复杂度和空间复杂度

在这里插入图片描述

前言

快速排序,听名字就比较霸道,效率根名字一样,非常的快,但也还是O(N * logN)级别的。

我所学到的快排有三个版本:

  1. 原始版本hoare版本,也就是hoare这个人发明的
  2. 基于hoare版本改进的版本,挖坑法(还有别的叫法,我这里就说成挖坑法了)。
  3. 是跟上面两种方法不一样的方法,前后指针法。

这里讲之前跟我前面的博客一样,先给图解。

图解

首先hoare版本的:
在这里插入图片描述

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

前后指针法
在这里插入图片描述

大致思路

首先,快排讲的是一个分治的思想,什么叫分治呢,根二叉树的前序遍历一样,先处理根,再处理左树和右树。那么快排也就是这样,先处理当前的(定关键字key的位置),然后再处理左半边的,后右半边的。

那这里的关键字key是什么呢?其实就是每趟排序的时候,首先选出来的一个数(一般选择最左端或者最右端),这个数决定了你是从左往右排还是从右往左排,什么意思呢?

对于hoare版本

当你的key选取在最左端时,就先让R(right)先走,R找小,找到了之后再让L(L找大)走。

当你的key选取在最右端时,就先让L(left)先走,L找大,找到了之后再让R(R找小)走。

这样能够保证L能和R相遇

下面以key在最左端为例
此时会出现以下情况:

当R找到小的了,L也找到大的了,就让两个位置上的数交换。再让R走,找小,找到了停,L找大,重复上述步骤。

当R和L相遇时,就将key位置上的数与相遇位置的数交换位置。本趟排序结束。

然后就开始递归,递归排序相遇位置的左边和相遇位置的右边。

对于挖坑法

当你的key选取在最左端时,就先让R(right)先走,R找小

当你的key选取在最右端时,就先让L(left)先走,L找大

此时key位置就是坑的位置
这样能够保证L能和R相遇,根hoare版本一样

下面以key在最左端为例
此时会出现以下情况:

当R找到小的了,就直接把R的值放到原坑中,R位置作为新的坑,再让L先走,找大,找到大的,将L的值放到原坑中,L位置作为新坑,不断重复上述步骤。

当R和L相遇,相遇位置的数放到原坑中,相遇位置作为新坑,将key的值放到新坑中。本趟排序结束

然后就开始递归,递归排序相遇位置的左边和相遇位置的右边。根hoare相似。

对于前后指针法

当你的key选取在最左端时,就让cur从左往右走

当你的key选取在最右端时,就让cur从右往左走

下面以key在最左端为例
此时会出现以下情况:

当cur位置上的数小于key时,prev++,交换cur和prev上的数,cur++

当cur位置上的数大于等于key时,prev什么也不做,cur++
直至cur越界,再交换key和prev位置上的数。本趟排序结束。

记下cur越界时prev的位置。
然后就开始递归,递归排序该位置的左和该位置的右。

实现方法

递归

hoare版本的单趟

//hoare版本
int PartSort1(int* a, int left, int right)
{
	int keyi = left;

	while (left < right)
	{
		//right找小				等于的时候也得走,不然程序会崩掉,这里是个易错点
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}

		//left找大				等于的时候也得走,不然程序会崩掉,这里是个易错点
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}

		//找到了就交换,或者二者相遇,同一个位置交换不交换都是一样的
		swap(&a[left], &a[right]);
	}

	//相遇
	swap(&a[left], &a[keyi]);
	keyi = left;

	//返回相遇的位置,keyi、left和right都可以
	return keyi;
}

挖坑法的单趟

//挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];

	while (left < right)
	{
		//right找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		//找到了把数放到原坑,right做新坑,相遇的时候也不影响
		a[left] = a[right];

		//left找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		//找到了把数放到原坑,left做新坑,相遇的时候也不影响
		a[right] = a[left];
	}

	//相遇的位置放key
	a[left] = key;

	//返回相遇的位置
	return left;
}

前后指针法的单趟

//前后指针法
int PartSort3(int* a, int left, int right)
{
	int keyi = left;
	int previ = left;
	int curi = previ + 1;

	while (curi <= right)
	{
		//小了并且previ+1不等于curi再交换
		if (a[curi] < a[keyi] && ++prev != curi))
			swap(&a[previ], &a[curi]);
		
		curi++;
	}

	swap(&a[previ], &a[keyi]);

	return previ;
}

全趟排序
上面三个的代码只是单趟的排序,还要放到整体的排序中:

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

 	//hoare
	//int keyi = PartSort1(a, begin, end);
	
	//挖坑法
	//int keyi = PartSort2(a, begin, end);
	
	//前后指针法
	int keyi = PartSort3(a, begin, end);


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

上面的就是三种方法的代码了。

非递归

非递归的话,得要用到栈。

图解:
在这里插入图片描述

代码实现:

//快排非递归
//快排非递归
void QuickSortNonR(int* a, int begin, int end)
{
	Stack st;
	StackInit(&st);

	//初始情况下就入左右两端
	//先入右边再入左边
	StackPush(&st, end);
	StackPush(&st, begin);

	//栈不为空才继续循环
	while (!StackEmpty(&st))
	{
		//取左
		int lefti = StackTop(&st);
		StackPop(&st);

		//取右
		int righti = StackTop(&st);
		StackPop(&st);
		
		//得关键字位置
		int keyi = PartSort3(a, lefti, righti);

		//区间存在才入栈
		if (lefti < keyi - 1)
		{
			//先入右边再入左边
			StackPush(&st, keyi - 1);
			StackPush(&st, lefti);
		}

		//区间存在才入栈
		if (righti > keyi + 1)
		{
			//先入右边再入左边
			StackPush(&st, righti);
			StackPush(&st, keyi + 1);
		}

	}

}

快排的优化(基于递归的优化)

三数取中法

上面递归的三个单趟排序其实是可以再优化一下的(以升序为例),因为当数组有序的时候每次选择key时,选出来的都是最小值,这时候key最终放的位置就是最左端,而递归的时候需要把key的左端和key的右端继续排,然而此时key处于最左端,那么递归排别的段的时候就只能排key的右端了,这个时候就相当于是排一次去处一个数,那么就变成了N,N-1,N-2,…2,1,时间复杂度就会变成O(N^2),就相当于是冒泡排序了。这样的话快排就排的不快了。
在这里插入图片描述

但我们所希望的是这样的:
在这里插入图片描述

怎么做呢?
虽然我们不能取到所有有序的数中最中间的数,但是我们可以取到所有无序的数中位于最中间的数,每次排序前把这个数和左右两端的数比较以下,求出值为三者中不大也不小的那个数,然后再把这个数放到最前面,就可以实现类似于图二的方法,虽然做不到完全二分,但是也比图一强。

三数取中

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

然后把这个函数放在每一个单趟排序函数的最前面就行。

hoare版本的优化

//hoare版本
int PartSort1(int* a, int left, int right)
{
	//三数取中优化
	int mid = GetMid(a, left, right);
	swap(&a[left], &a[mid]);

	int keyi = left;

	while (left < right)
	{
		//right找小				等于的时候也得走,不然程序会崩掉,这里是个易错点
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}

		//left找大				等于的时候也得走,不然程序会崩掉,这里是个易错点
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}

		//找到了就交换,或者二者相遇,同一个位置交换不交换都是一样的
		swap(&a[left], &a[right]);
	}

	//相遇
	swap(&a[left], &a[keyi]);
	keyi = left;

	//返回相遇的位置,keyi、left和right都可以
	return keyi;
}

挖坑法的优化

//挖坑法
int PartSort2(int* a, int left, int right)
{
	//三数取中优化
	int mid = GetMid(a, left, right);
	swap(&a[left], &a[mid]);

	int key = a[left];

	while (left < right)
	{
		//right找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		//找到了把数放到原坑,right做新坑,相遇的时候也不影响
		a[left] = a[right];

		//left找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		//找到了把数放到原坑,left做新坑,相遇的时候也不影响
		a[right] = a[left];
	}

	//相遇的位置放key
	a[left] = key;

	//返回相遇的位置
	return left;
}

前后指针法的优化

//前后指针法
int PartSort3(int* a, int left, int right)
{
	//三数取中优化
	int mid = GetMid(a, left, right);
	swap(&a[left], &a[mid]);

	int keyi = left;
	int previ = left;
	int curi = previ + 1;

	while (curi <= right)
	{
		//小了并且previ+1不等于curi再交换
		if (a[curi] < a[keyi] && ((previ + 1) != curi))
			swap(&a[previ], &a[curi]);
		
		curi++;
	}

	swap(&a[previ], &a[keyi]);

	return previ;
}

小区间优化

小区间优化的作用在于当数据量比较大时,递归可能会导致栈溢出。

而快排递归的时候是类似于二叉树的,所以最后一层递归所开辟的空间基本上要占用总空间的一半(完全二分的情况下),这个时候如果我们控制一下最后一层,不让这一层的数递归排序,就可以很好地避免栈溢出的问题。

怎么做呢。很简单,判断一下当递归某一区间段的时候(右端下标 - 左端下标 < X(X可取10~20之间的数)),这时候就可以用一下别的排序,而10~20的数据量还是很小的,所以不需要用比较复杂的排序,用一个N^2级别的排序就可以了,而N^2级别的排序中插入排序就是最好的,所以这时候替换成插入排序就可以了。

小区间优化

//快排
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	else if (end - begin + 1 <= 10)
	{
		//小区间优化
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		//int keyi = PartSort1(a, begin, end);
		//int keyi = PartSort2(a, begin, end);
		int keyi = PartSort3(a, begin, end);


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

时间复杂度和空间复杂度

我这里之说优化之后的。

时间复杂度就是O(N* logN),空间复杂度是O(logN)。

就说一下空间复杂度,因为快排是基于分治思想的,而且递归排序的过程类似于二叉树的前序遍历,所以在栈上开辟空间时就要不断的堆栈,然后当函数递归达到最大深度(二叉树的最下面一层)的时候,也就是logN,函数返回,最深层的函数系统就会自动回收空间,这时候就不会再开辟更大的空间了,而且开辟的空间是慢慢的缩小再增大,再减少,再增大。。。直到排序结束。所以空间开辟的时候最大也就开辟logN大小的空间。

到此结束。。。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

永久免费内网穿透不限制速度

市面上的免费内网穿透大都有格式各样的限制&#xff0c;什么限制流量啊&#xff0c;每个月要签到打卡啊&#xff0c;还有更改域名地址等&#xff0c;只有神卓互联内网穿透是永久免费没有限制的&#xff0c;白嫖也可以。 这篇文章分享了3个方案&#xff0c;按照性能和综合指标排…

项目驱动的编写

驱动代码直接使用nfs传输,设备树直接在开发板中修改设备树文件 1、修改好设备树&#xff0c;在内核顶层make dtbs &#xff0c;然后替代tftp目录中的设备树文件 2、使用内核源码编译生成驱动程序&#xff0c;然后传送到开发板中&#xff0c;使用insmod动态加载 LCD驱动 1、初始…

从零学习SDK(7)如何打包SDK

打包SDK的目的是为了方便将SDK提供给其他开发者或用户使用&#xff0c;以及保证SDK的兼容性和安全性。打包SDK可以有以下几个好处&#xff1a; 减少依赖&#xff1a;打包SDK可以将SDK所需的库、资源、文档等打包成一个文件或者一个目录&#xff0c;这样就不需要用户再去安装或…

ArduPilot开源飞控系统之简单介绍

ArduPilot开源飞控系统之简单介绍 1. 源由2. 了解&阅读2.1 ArduPilot历史2.2 关于GPLv32.3 ArduPilot系统组成2.4 ArduPilot代码结构 3. 后续4. 参考资料 ArduPilot是一个可信赖的自动驾驶系统&#xff0c;为人们带来便利。为此&#xff0c;提供了一套全面的工具&#xff0…

读SQL进阶教程笔记12_地址与三值逻辑

1. SQL和数据库都在极力提升数据在表现层的抽象度&#xff0c;以及对用户隐藏物理层的概念 2. 关系模型是为摆脱地址而生的 2.1. “地址”不仅包括指针操作的地址&#xff0c;还包括数组下标等 3. 一个优雅的数据结构胜过一百行杂耍般的代码 3.1. 精巧的数据结构搭配笨拙的…

Spring MVC 的调用(12)

目录 SpringMVC流程 源码分析 第一步:用户发起请求到前端控制器&#xff08;DispatcherServlet&#xff09; 第二步&#xff1a;前端控制器请求处理器映射器&#xff08;HandlerMappering&#xff09;去查找处理器&#xff08;Handle&#xff09;&#xff1a;通过xml配置或者…

高效部署Redis Sentinel模式(哨兵模式),手把手教学

Redis Sentinel模式部署 前言一、服务器部署同版本的redis1、换软件源在yum拉取包的时候启用remi源 二、修改配置文件1.修改/etc/redis.conf2.配置/etc/redis/sentinel.conf 三、启动redis服务1、启动服务2、连接redis3、检查redis 前言 这里就不过多的解释高可用的好处了&…

CRM系统是什么?它有什么作用?

CRM系统是什么&#xff1f; CRM是Customer Relationship Management&#xff08;客户关系管理&#xff09;的缩写&#xff0c;是一种通过对客户进行跟踪、分析和管理的方法&#xff0c;以增加企业与客户之间的互动和联系&#xff0c;提高企业与客户之间的互信&#xff0c;从而…

基于 VITA57.4 标准的 8 路 500MSPS/1GSPS/1.25GSPS 采样率 14 位 AD 采集 FMC 子卡模块

板卡概述 FMC148 是一款基于 VITA57.4 标准的 JESD204B 接口 FMC 子卡模块&#xff0c;该模块可以实现 8 路 14-bit、500MSPS/1GSPS/1.25GSPS ADC 采集功能。该板卡 ADC 器件采用 ADI 公司的 AD9680 芯片,全 功率-3dB 模拟输入带宽可达 2GHz。该 ADC 与 FPGA 的主机接口通 …

Revit相关问题:符号线,转转问题,生成三维视图

一、Revit符号线如何画粗一些?如何自定义符号线子类别? 1、Revit在族里面符号线的粗细、显示颜色、显示线型为符号线的子类别控制! 你可以通过&#xff0c;管理选项卡新建子类别&#xff0c;然后在画符号线的时候应用该子类别! 新建符号线对象样式 应用子类别 二、Revit三维模…

背包问题——01背包|完全背包

目录 前言&背包问题的历史 01背包 1、题目 2、暴力解01背包 Ⅰ、代码 3、动态规划解01背包 Ⅰ、二维dp数组解01背包 1&#xff09;dp数组的含义 2&#xff09;递推公式 3&#xff09;dp数组的初始化 4&#xff09;遍历顺序的讨论 5、代码 Ⅱ、一维数组解01背包 1&…

C#调用C++封装的SDK库(dll动态库)——上

C#调用C封装的SDK库(dll动态库)——上 一、C封装库 通过前几篇文章&#xff0c;我们封装了C的动态DLL库&#xff0c;有Qt版的&#xff0c;有C版的&#xff0c;当然还有介绍了Pimpl模式在SDK封装中的使用&#xff1a; Qt创建SDK VS创建SDK Pimple在SDK封装中的应用 但是&a…

RabbitMQ入门

AMQP AMQP(Advanced Message Queuing Protocol,高级消息队列协议) 是进程之间传递异步消息的网络协议。 AMQP工作过程 发布者(Publisher)发布消息(Message),经过交换机(Exchange)&#xff0c;交换机根据路由规则将收到消息分发给交换机绑定的队列(Queue)&#xff0c;最后AM…

二维数组的总结

一、时间复杂度和空间复杂度 时间复杂度和空间复杂度是衡量算法效率的两个重要指标。时间复杂度是指算法执行所需的时间&#xff0c;而空间复杂度是指算法执行所需的内存空间。 计算时间复杂度和空间复杂度需要分析算法中各个操作的执行次数和内存使用情况。具体的计算方法可以…

亚马逊、ebay、temu如何提升产品点击率?测评自养号解析

产品点击率对于店铺销售额的影响至关重要&#xff0c;尤其是在竞争越来越激烈的市场环境中&#xff0c;想要有销量和转化&#xff0c;提高产品listing点击率成为了非常关键的一环。 1. 产品主图 顾客浏览产品时&#xff0c;第一眼看到的就是主图&#xff0c;一张优质的主图更容…

CSDN博客编写教程

这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个…

UniLM模型简单介绍

目录 一、概要 二、深入扩展 2.1 预训练任务 2.2 模型精调 一、概要 如果将基于Transformer的双向语言模型&#xff08;如BERT模型中的掩码语言模型&#xff09;与单向的自回归语言模型&#xff08;如BART模型的解码器&#xff09;进行对比&#xff0c;可以发现&#xff0c…

springboot+vue职称评审管理系统(源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的职称评审管理系统。项目源码请联系风歌&#xff0c;文末附上联系信息 。 目前有各类成品java毕设&#xff0c;需要请看文末联系方式 …

[Java]监听器(Listener)

过滤器&#xff08;Filter&#xff09;https://blog.csdn.net/m0_71229255/article/details/130246404?spm1001.2014.3001.5501 一 : Listener监听器简述 监听器就是监听某个对象的的状态变化的组件 监听器的相关概念&#xff1a; 事件源&#xff1a; 被监听的对象 ----- 三…

(补)4.13每日一题

给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长子串 的长度。 题目连接&#xff1a;https://leetcode.cn/problems/longest-substring-without-repeating-characters/ 解题 开始我把这个题目想简单了&#xff0c;我想的是输入一个字符串&#xff0c;从第一…