八大排序算法之快速排序(下篇)(快排的优化+非递归快排的实现)

18 篇文章 6 订阅
订阅专栏

目录

一.前言

1.快速排序的实现:

快速排序的单趟排序(排升序)(快慢指针法实现):​

2.未经优化的快排的缺陷

二.快速排序的优化

1.三数取中优化

优化思路:

2. 小区间插入排序优化

小区间插排优化的递归快排:

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

1.快排一个难以避免的缺陷(暂不考虑三指针单趟排序优化)

2.非递归快排的实现思路

数据结构栈模拟系统栈算法思想:​

非递归快排代码实现:


一.前言

1.快速排序的实现:

🤪快排的详细实现原理参见青菜的博客🤪: http://t.csdn.cn/0bf1ghttp://t.csdn.cn/0bf1g下面简单回顾一下快排的核心思想:

快速排序的单趟排序(排升序)(快慢指针法实现):

int PartionV3(int* arr, int left, int right)//完成1个元素的排序,同时分割数组
{
	assert(arr);
	int key = left;										//选取数组左端元素作为key值
	int slow = left;								    //slow从left开始  
	int fast = left + 1;							    //fast从left+1开始遍历数组
	while (fast <= right)
	{
		if (arr[fast] < arr[key])			//fast找到比key小的值
		{
			++slow;				   			//slow指向下一个位置
			if (slow != fast)				//fast和slow相等没必要交换
			{
				swap(&arr[slow], &arr[fast]);//交换slow和fast所指向的值
			}
		}
		++fast;							 //fast遍历数组
	}
	swap(&arr[key], &arr[slow]);         //最后交换key和slow所指向的变量
	return slow;  						 //返回slow位置下标
}

  • 🤪假设乱序数组有N个元素,则需要进行N趟单趟排序(每次单趟排序可以完成一个key元素在有序序列中的归位)
  • 🤪直接循环N-1次单趟排序的时间复杂度为O(N^2)
  • 🤪为了降低排序的时间复杂度的数量级,我们采用分治递归思想来完成这N次单趟排序
  • 🤪基本思想是:每次单趟排序完成后,被归位的key元素为划分点,将数组划分为在排序意义上互不关联两个子数组(左子数组中每个元素都比key小,右子数组中每个元素都比key大),从而使得后续每次单趟排序需要遍历的元素个数呈指数式递减,总体排序的时间复杂度也因此降阶,递归过程中数组被逐步划分过程的图示:(划分结构逻辑上类似于二叉树)
  • 上图中每个数组拆分层次中(一个拆分层次中多个子数组),单趟排序所需遍历的总的元素个数的数量级为O(N),拆分层次的数量级为O(logN),因此上述情形下,快排的总体时间复杂度为O(NlogN).

  • 😜这里我们假设所处理的数组都是逆序数极大乱序序列:这种情况下,每次单趟排序所确定的数组分割点有极大的可能是在数组的中间位置。

  • 😜对应上图完成排序的分治递归代码

    //每完成一次数组分割,就进行左右子数组分治递归完成每一个元素的排序
    void QuickSort2(int* arr, int left, int right)
    {
    	assert(arr);
    	if (left>=right)							    //子数组只剩1个元素时(或left和right错开时)停止递归
    	{
    		return;
    	}
    	int breakpoint = PartionV3(arr, left, right);	//找到数组分割点(同时也完成了一个元素的排序)
    	//左右子数组分治递归
    	QuickSort(arr, left, breakpoint - 1);     //处理左子数组
    	QuickSort(arr, breakpoint + 1, right);    //处理右子数组
    }
  • 😜注意递归结束条件的控制😜

2.未经优化的快排的缺陷

  • 单趟排序时,我们将子数组的左端元素作为key,因此在处理有序或接近有序以及倒序或完全倒序的序列时,大多数情况下经过单趟排序后,数组的分割点(key元素的最终位置)会停留在数组左端(倒序的情况则停留在数组右端),于是整个排序过程中,数组被逐步划分过程的图示:
  • 此时划分层次数量级为O(N),每个划分层次中单趟排序所需遍历的元素个数等差数列递减,此时分治递归的时间复杂度计算公式为等差数列求和式,数量级升阶为O(N^2);
  • 为了避免这种情况出现,我们需要对key元素的选取方式进行优化

二.快速排序的优化

1.三数取中优化

优化思路:

  • 为了避免序列有序或接近有序时,分治递归过程中不断在数组区间端点处对数组进行划分(从而导致数组划分总次数数量级为O(N)),我们采用三数取中的方式优化key的选取:
  1. 设计一个接口,确定数组的首元素,尾元素中间位置元素三者中的中位数(中间大小的数),并返回其下标.接口首部:
    int GetMid(int* arr,int left,int right)

    接口实现: 

    int GetMid(int* arr,int left,int right)
    {
    	int mid = left + ((right - left) >> 2);     //在arr[left],arr[mid],arr[right]三者中取中间值作为key,返回key的下标
    	if (arr[left] < arr[right])
    	{
    		if (arr[left] < arr[mid] && arr[mid] < arr[right])
    		{
    			return mid;
    		}
    		else if (arr[mid] > arr[right])
    		{
    			return right;
    		}
    		else
    		{
    			return left;
    		}
    	}
    	else
    	{
    		if (arr[left] > arr[mid] && arr[mid] > arr[right])
    		{
    			return mid;
    		}
    		else if (arr[mid] > arr[left])
    		{
    			return left;
    		}
    		else
    		{
    			return right;
    		}
    	}
    }
  2. 确定数组的首元素,尾元素中间位置元素三者中的中位数后,将其交换到arr[left]的位置,后续的单趟排序过程我们依然选择arr[left]作为key元素

  3. 三数取中优化后的单趟排序:

    int PartionV3(int* arr, int left, int right)//完成1个元素的排序,同时分割数组
    {
    	assert(arr);
    	swap(&arr[left], &arr[GetMid(arr, left, right)]);
    	int key = left;										//在arr[left],arr[mid],arr[right]三者中取中间值作为key
    	int slow = left;								    //slow从left开始  
    	int fast = left + 1;							    //fast从left+1开始遍历数组
    	while (fast <= right)
    	{
    		if (arr[fast] < arr[key])			//fast找到比key小的值
    		{
    			++slow;				   			//slow指向下一个位置
    			if (slow != fast)				//fast和slow相等没必要交换
    			{
    				swap(&arr[slow], &arr[fast]);//交换slow和fast所指向的值
    			}
    		}
    		++fast;							 //fast遍历数组
    	}
    	swap(&arr[key], &arr[slow]);         //最后交换key和slow所指向的变量
    	return slow;  						 //返回slow位置下标
    }
  4. 经过三数取中后的单趟排序,面对有序序列时,数组分割点(key元素的最终位置)会确定在数组的中间位置,因此分治递归时,函数栈帧的逻辑分布会呈现出满二叉树的结构(递归深度最小),也就意味着,在面对有序(或接近有序)的序列时,三数取中优化后的快排有着最高的排序效率(相比于排序其余乱序序列的情况).这一点大大增加了快排在各种实际场景中适用性

     (每个子数组代表一个函数栈帧)

  5. 同时三数取中优化也让快排的单趟排序在处理各种乱序序列时,令数组的分割点(key元素的最终位置)尽可能地接近数组中间位置

2. 小区间插入排序优化

  • 分治递归过程中的函数栈帧逻辑分布(每一个子数组代表了一次函数递归调用):
  • 根据二叉树的结构特点(满二叉树最后一层结点个数总结点个数的一半)可知,元素个数为1个(以及2个3个等等)的子数组个数最多(一个子数组代表一次函数递归调用),因此我们可以修改递归的结束条件:当子数组的元素个数小于10(可以在一定范围内任意设定)时,对子数组进行插入排序结束递归
  • 插入排序接口:
    void InsertSort(DataType* arr, int size)
    {
    	assert(arr);
    	for (int end = 0; end < size; end++)    //用end来维护数组前end个元素构成的有序序列
    	{
    		int x = arr[end];					//x为待插入有序序列的数据
    		int insert = end;					//用变量insert来确定x要插入的位置
    		while (insert>0)					//通过元素比较确定x要插入的位置
    		{
    			if (x < arr[insert-1])			   //说明insert不是x要插入的位置
    			{
    				arr[insert] = arr[insert-1];   //往后挪动数据为x留出空位
    				insert--;                      //令insert指向序列前一个元素
    			}
    			else
    			{
    				break;	    //有序序列中x>=arr[insert-1]说明insert是x要插入的位置
    			}
    		}					//最坏的情况下x会被插入到数组的首地址处(此时数据比较和挪动了end次)
    		arr[insert] = x;    //完成元素x的插入(继续插入下一个元素)
    	}
    }
    

    小区间插排优化的递归快排:

    //每完成一次数组分割,就进行左右子数组分治递归完成每一个元素的排序
    void QuickSort(int* arr, int left,int right)
    {
    	assert(arr);
    	if (right - left+1 <=10)							//子数组只剩10个元素时(小区间插排)停止递归
    	{
    		InsertSort(arr + left, right - left+1);
    		return;
    	}
    	int breakpoint = PartionV3(arr, left, right);		//找到数组分割点(同时也完成了一个元素的排序)
    	//左右子数组分治递归
    	QuickSort(arr, left, breakpoint - 1);     //处理左子数组
    	QuickSort(arr, breakpoint + 1, right);    //处理右子数组
    }

  • 经过上述小区间插入排序处理后的分治快排,函数的递归调用次数减少了一半以上,这对于递归优化比较落后的编译平台是非常友好的

附:由于现代编译器对于递归函数的时间效率会进行大幅程度的优化,因此快排的小区间插排优化实测效果并不明显

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

1.快排一个难以避免的缺陷(暂不考虑三指针单趟排序优化)

  • 当序列为常序列(所有元素相同)序列中有大量(绝大部分)元素相同时,快排递归中数组的划分过程也会出现下面类似的情况:
  • 其原因与前面未经优化的快排面对有序序列时时间复杂度升阶的原因是一致的(常序列特殊的有序序列),并且面对常序列大量(绝大部分)元素相同的序列时,三数取中优化起不到任何作用的(三个元素都是相同的,取中位数没有任何意义)
  • 因此使用递归快排时,如果处理的序列为常序列大量(绝大部分)元素相同的序列,会有一定的栈溢出风险(上图中的递归深度为O(N),即递归过程中会有O(N)数量级个函数栈帧同时存在)(此时快排的时间复杂度为O(NlogN)
  • (在不考虑三指针单趟排序优化的情形下)常序列(或大量(绝大部分)元素相同的序列)要避免使用快排进行处理
  • 实际运用中为了节省系统栈空间,我们需要用数据结构栈模拟系统栈,利用循环结构实现快排.

2.非递归快排的实现思路

  • 快排递归的过程中,每个函数栈帧中存储的关键变量待排序的子数组的左右端下标(left和right)
  • 函数栈帧进出系统栈的规则满足后进先出
  • 因此我们可以用数据结构中的整形栈来存储每个待排序的子数组左右端点下标,借助数据结构栈我们就可以利用循环结构模拟递归的过程

函数首部: 

void QuickSortNonR(int* arr, int begin, int end)

begin和end是待排序的数组左端下标和右端下标(两个下标都指向有效元素)(闭区间)

  • 为了方便我们借助C++STL的栈对象来完成模拟递归,快排递归(前序递归)遍历次序:

数据结构栈模拟系统栈算法思想:

  • 先将原数组的区间左右端下标压入栈中,接着开始执行循环:
  1. 首先,利用left和right变量接收栈顶区间
  2. [left,right]子数组进行单趟排序,完成单个元素排序的同时确定数组的分割点下标breakpoint
  3. [left,right]数组右子数组[breakpoint+1,right]区间左子数组[left,breakpoint-1]区间压入栈中(注意右子数组区间要先入栈,左子数组区间要后入栈,保证后续左子数组先出栈被处理)(左右子数组区间入栈的前提是区间中的元素个数大于或等于2)
  • 重复上述步骤直到栈为空,则完成了分治递归的模拟过程(相当于模拟了前序遍历的递归过程)
  • 算法gif:

非递归快排代码实现:

void QuickSortNonR(int* arr, int left, int right)
{
	assert(arr);
	stack<int> range;
	if (right > left)                //区间中有两个以上元素时,区间入栈
	{
		range.push(right);           //取的时候先取左边界,因此左边界后入栈(令左边界压在右边界上面)
		range.push(left);            //初始区间入栈;

	}
	while (!range.empty())
	{
		int left = range.top();	     //令栈顶区间出栈
		range.pop();
		int right = range.top();
		range.pop();
		
		int breakpoint = PartionV3(arr, left, right); //确定区间分割点,同时完成一个数组元素的排序

		//栈是后进先出,递归是先向左子数组递归,因此做左子数组区间后入栈
		if (right>breakpoint+1)					      //若子区间元素个数大于一个则区间入栈
		{
			range.push(right);
			range.push(breakpoint + 1);
		}
		if(breakpoint-1>left)
		{
			range.push(breakpoint - 1);
			range.push(left);
		}
	}
}
  • 区间入栈时注意左右端下标入栈顺序

写文章

热门文章

  • 八大排序算法之归并排序(递归实现+非递归实现) 9959
  • 数据结构:堆的实现与建堆时间复杂度分析 5257
  • [计网底层小探索]:实现并部署多线程并发Tcp服务器框架(基于生产者消费者模型的线程池结构) 4017
  • C++:类的构造函数与析构函数 3331
  • C++:运算符重载与类的赋值运算符重载函数 2915

分类专栏

  • 青菜的Linux专栏 17篇
  • 计算机网络 5篇
  • 初学者日志 22篇
  • 进阶C++(C++与进阶数据结构) 13篇
  • 算法笔记 2篇
  • 图论数据结构 6篇
  • 计算机体系结构 2篇
  • 初阶C++ 21篇
  • 剑指offer练习日志 2篇
  • 初阶数据结构 18篇

最新评论

  • 八大排序算法之插入排序+希尔排序

    代码与艺术: 优质好文,博主的文章细节很到位,兼顾实用性和可操作性,感谢博主的分享,期待博主持续带来更多好文 也欢迎您来逛逛我的博客互三哦~

  • 整形家族数据存储之 原码 补码 反码

    做而论道_CS: 计算机根本就不用原码和反码,只用补码。 所以,原码反码,都不需要讨论。 对补码的理解,也不用弄这么复杂。 所谓的补码,不过就是一个小学的知识点而已。 你看十进制,两位数:0~99。 可以有:27 + 99 = (一百) 26 也可以:27 - 1 = 26 如果你忽略进位,依旧保持两位数, 这两种算法的功能,就是相同的。 即,当你舍弃了进位:  正数,就可以当负数使用、用加法,就能实现减法运算。 在计算机中舍弃进位,会怎样?  计算机中,就全是正数了。  没有了负数,减法也不存在了,减法器,当然也没用了。  计算机有一个加法器,就能横行天下! ================== 舍弃进位,就是补码的来历和存在意义。 ================== 在两位十进制时,舍弃进位,就是减去一百。 那么,加上 99,再减去 100,当然就是 “-1 ” 了。 八位二进制数是:0000 0000 ~ 1111 1111。 也就是十进制的:0 ~ 255。 如果出现进位,就是:2^8 = 256。 那么,加上 1111 1111 (255),再减 256,也就是-1。 由此,计算机专家就发明了:-1 的补码是 1111 1111。 同样,-2 的补码就是:1111 1110 (254)。 还有,-3 的补码就是:1111 1101 (253)。 。。。 最后,-128 的补码是:1000 0000 (128)。 转换公式:负数的补码 = 2^8 + 该负数。 同样还有:正数的补码 = 2^8 + 该正数。 但是,正数加上 256,就会出现进位。 那就舍弃吧。 于是就有:正数的补码 = 该正数。 这就证明了:零和正数的补码,就是其本身。 例:求-31 的补码是多少? 解:256-31 = 225 = 1110 0001 (二进制)。 这不就完事了吗? 哪里还用:机器数真值原码反码取反加一符号位不变模 ... 老外脑子不够用,算术不灵,才 “发明” 了这许多的谎言。 谁要是跟老外学算术,立刻、马上,直接就掉沟里去了!

  • 八大排序算法之归并排序(递归实现+非递归实现)

    daiyanyun: 传过来的参数是数组的个数,没有减一把。

  • 八大排序算法之归并排序(递归实现+非递归实现)

    daiyanyun: 哪个两个有序序列的合并为什么不加上边界,left <= right, 传过来的参数是right - 1 吗表情包

  • 深刻解析TCP协议--传输层数据收发机制和内核视角下的数据报文收发过程

    努力的青菜: 谢谢兄弟

大家在看

  • 【人工智能-初级】第1章 人工智能概述 382
  • 后端程序员必备:15个MySQL表设计的经验准则
  • Python从0到100(六十四):Python OpenCV-图像运算进阶实战 637
  • 笔试强训10.17
  • C语言--改写string函数族

最新文章

  • 网络层IP协议和数据链路层--理解NAT/NAPT路由技术
  • 深刻解析TCP协议--传输层数据收发机制和内核视角下的数据报文收发过程
  • 应用层http协议包解析与https加密策略解析
2024年10篇
2023年77篇
2022年7篇

目录

目录

评论 79
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力的青菜

谢谢各位爹娘的打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或 充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值

深圳坪山网站建设公司英文网站优化电池济宁关键词优化网站铜陵娄底网站优化泰州专业seo优化网站报价高州市网站seo优化排名石家庄seo网站优化公司费用速排网站推广优化软件哪里买网站搜索引擎优化的相关技能茂名网站优化费用莱山区个性化网站优化专业网站推广优化服务商网站结构优化分析合肥市pc网站优化网站可以做优化吗宣城网站优化电话邢台成都网站优化不得不思考的网站优化方案月湖区网站seo优化排名西安网站优化公司地址公司兴化网站搜索优化工作室浦东新区公司网站优化机构济南网站建设方案优化邯郸网站优化推广价格郑州百度网站优化平台鹰潭泰州网站优化重庆诚信服务企业网站优化瓦房店网站优化公司网站产品优化称赞火26星至壹起航怎么优化网站关键词乌鲁木齐做网站优化香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声卫健委通报少年有偿捐血浆16次猝死汪小菲曝离婚始末何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言男子被猫抓伤后确诊“猫抓病”周杰伦一审败诉网易中国拥有亿元资产的家庭达13.3万户315晚会后胖东来又人满为患了高校汽车撞人致3死16伤 司机系学生张家界的山上“长”满了韩国人?张立群任西安交通大学校长手机成瘾是影响睡眠质量重要因素网友洛杉矶偶遇贾玲“重生之我在北大当嫡校长”单亲妈妈陷入热恋 14岁儿子报警倪萍分享减重40斤方法杨倩无缘巴黎奥运考生莫言也上北大硕士复试名单了许家印被限制高消费奥巴马现身唐宁街 黑色着装引猜测专访95后高颜值猪保姆男孩8年未见母亲被告知被遗忘七年后宇文玥被薅头发捞上岸郑州一火锅店爆改成麻辣烫店西双版纳热带植物园回应蜉蝣大爆发沉迷短剧的人就像掉进了杀猪盘当地回应沈阳致3死车祸车主疑毒驾开除党籍5年后 原水城县长再被查凯特王妃现身!外出购物视频曝光初中生遭15人围殴自卫刺伤3人判无罪事业单位女子向同事水杯投不明物质男子被流浪猫绊倒 投喂者赔24万外国人感慨凌晨的中国很安全路边卖淀粉肠阿姨主动出示声明书胖东来员工每周单休无小长假王树国卸任西安交大校长 师生送别小米汽车超级工厂正式揭幕黑马情侣提车了妈妈回应孩子在校撞护栏坠楼校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变老人退休金被冒领16年 金额超20万西藏招商引资投资者子女可当地高考特朗普无法缴纳4.54亿美元罚金浙江一高校内汽车冲撞行人 多人受伤

深圳坪山网站建设公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化