【数据结构】-快速排序的四种方法实现以及优化

news2025/1/11 14:49:38

作者:小树苗渴望变成参天大树
作者宣言:认真写好每一篇博客
作者gitee:gitee
在这里插入图片描述
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!

快速排序

  • 前言
  • 一、hoare法(左右指针法)
  • 二、挖坑法
  • 三、前后指针法
  • 四、优化版本
    • 4.1随机数
    • 4.2三数取中
  • 五、非递归版本
  • 四、总结


前言

今天讲一种不一样的排序,听名字就知道这个排序不拐弯抹角的,我们来看看它又多快速,并且快速排序的前三种方法都是递归思想,最后一种是非递归,我们就来重点介绍着集中方法来实现快排(以升序为例)


一、hoare法(左右指针法)

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

我们来看看动图,再来画图详细给大家介绍一下:

我们选择一个keyi,保持不动,a[keyi]作为关键字,一趟排序下来这个关键字左边的值都比它小,右边的值都比它大,说明这个关键字来到了它最终来到的地方
请添加图片描述

通过左右指针将小的数跳到关键字的左边,大的数跳到关键字的右边,这个图表示的一趟排序接下来看看画图分析
在这里插入图片描述

通过这个图,我们看到要注意的两个细节,一个是找大找小的左右判断,不能错过了,而是判断条件要不要加等于号,把这两个细节掌握号,单趟就掌握好了,我们来看看单趟排序的代码吧:

int partsort1(int* a, int left, int 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位置的值和左边的值交换
	keyi = left;//改变keyi的位置
}

看单趟排序结果:
在这里插入图片描述
相信大家看到这里对于函数体里面的内容已经了解了,但是为什么我的函数设计要带返回值呢??

原因是我再前言就讲过,快排的前三种都是递归的方法,思想都是一样的,就是单趟排序的思想不一样,因为一趟排序之后,关键字会跳到它最终的位置,下一趟排序,它就不需要参加排序,只需要把它左边区间和右边你区间进行排序,返回这个关键字的下标,目的是方便递归时找到左右区间的范围

我们来看图:
在这里插入图片描述
我们来看完整的快排:

void quicksort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int keyi = partsort1(a, left, right);
	quicksort(a, left, keyi - 1);//对关键字左区间排序
	quicksort(a, keyi+1, right);//对关键字右区间排序
}

相比较而言,这里的递归还没有二叉树那些递归难理解,主要理解单趟排序的思想,就行了,接下来的两种方法只需要修改partsort函数体里面的内容就行了,我们开始介绍下面两种思想

二、挖坑法

挖坑法是后面的牛人觉得hoare的方法有点难理解,又想出来的一种新的方法,思想是,把数组的最左边和最右边先当成一个坑,把这个坑的数据先保存起来,这样有数过来就直接放到这个坑位里面,不怕原数据被覆盖了,这个数就变成新的坑位,采取的还是左边找大,右边找小,找到就放到坑位里面,同时找到的位置变成新的坑位
我们先来看看动图演示:

在这里插入图片描述
我们再来看看图解:
在这里插入图片描述
通过这个图,我们要注意结束条件和找大找小条件都是和第一种一样的那我们来看看这个单趟排序的代码时怎样的:

int partsort2(int* a, int left, int right)
{
	int key = a[left];//将坑位的关键字保存起来
	int pivot = left;//定义一个坑位下标
	while (left < right)
	{
		while (left < right && a[right] >= key)//找小
		{
			right--;
		}
		a[pivot] = a[right];//把找到小的数放到左坑
		pivot = right;
		while (left < right && a[left] <= key)//找大
		{
			left++;
		}
		a[pivot] = a[left];//把找到大的数放到右坑
		pivot = left;
	}
	a[pivot] = key;//把保存的关键字放到最后的坑位
	pivot = left;
	return pivot;
}

我们来看看单趟运行结果:
在这里插入图片描述

和我们画的图解结果一样,这两种单趟的大思想都是把大的数快速跳到右边,比较小的跳到左边,关键字跳到它最终要出现的位置,最后都是通过递归再对左右区间进行排序

我们来看最终的快速排序:

void quicksort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int keyi = partsort2(a, left, right);//就改了一下这个函数体,其余和之前一样
	quicksort(a, left, keyi - 1);
	quicksort(a, keyi+1, right);
}

相信大家看到这里对于挖坑法应该理解,希望大家下来可以取尝试理解一下,接下来我将讲解另一种方法,前后指针法

三、前后指针法

这个方法相比较而言比前面两种更好,而且不容易出错,那我就来介绍一下这种方法,思想是:通过一个cur指针来找小,找到把prev指针+1把值进行交换,大的数跳到前面,小的数跳到后面,+1找到的是大的数,因为是cur跳过的数,所以是大的数,然后重复上面的步骤,知道cur结束,最后把keyi的值和prev的值进行交换
我们来看看动图演示:
在这里插入图片描述
接下来看看图解:
在这里插入图片描述
这个方法理解了就很好理解,一定要多画图,接下来我们看一下单趟排序的代码:

int partsort3(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)//结束条件
	{
		if (a[cur] < a[keyi])//找到小
		{
			++prev;//就++prev
			swap(&a[prev], &a[cur]);//两者交换
		}
		cur++;//cur一直都需要++
	}
	swap(&a[prev], &a[keyi]);//循环结束
	keyi = prev;
	return keyi;
}

这个方法的好处就在于些的时候不容易出错,就一趟遍历即可,前两种方法在条件判断的时候容易出错,我们来看看这个方法的完整快排代码:

void quicksort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int keyi = partsort3(a, left, right);
	quicksort(a, left, keyi - 1);
	quicksort(a, keyi+1, right);
}

希望这三种方法你可以学会,接下来我要将这三种方法还可以进行优化。

四、优化版本

在这里插入图片描述

我们递归的思想大致是这样的,每增加一行的递归就会选出两倍的关键字到最终的位置,但是在有序的情况,我们看看会怎么样

在这里插入图片描述

这样看来快排在有序的时间效率最低, 那我们就把它弄成不有序,有两种方法,一个是随机数法,一个是三数取中

4.1随机数

一、我们来看看随机数,随便取一个a[keyi]然后和左边交换,在进行排序,来看代码:

void quicksort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int mid = left+rand() % (right - left + 1);
	if (mid != left)
	{
		swap(&a[left], &a[mid]);
	}
	int keyi = partsort3(a, left, right);
	quicksort(a, left, keyi - 1);
	quicksort(a, keyi+1, right);
}

为什么要加一个left,防止是递归右区间的时候。

4.2三数取中

我们再来看看三数取中,意思是在三个数中选出中间那个数,我们来看看代码:

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

也是选出来和最左边的数交换一下,但整体而言三叔取中用到比较多,因为随机数选出来的可能刚好是最小的keyi,这样和没选没啥区别,而三数取中保证选出来的keyi不是最小的

这里我就不在给大家测试性能了,如果大家感兴趣可以取我的这篇测排序性能博客调用我的函数去测试,对于快排尽量数据搞多些,效果明显,也可以和其他排序进行对比

对于优化其实还有一种方法就是小区间优化,对于数据量不是那么大的时候不需要使用递归来做,我们可以使用直接插入排序来做,具体为什么我就不做介绍了,大家了解一下就好了,反正越到最后递归的次数越多,浪费的时间就多,用直接插入排序节省一点时间,像希尔排序还要分组没必要和选择排序最好最坏情况一样的,直接插入排序在有序的时候比较好,所以选择了直接插入排序:

我们来看看怎么写的:

void quicksort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int mid = GetMid(a, left, right);

	int keyi = partsort3(a, left, right);
	if (keyi - 1 - left > 10)
	{
		quicksort(a, left, keyi - 1);
		quicksort(a, keyi + 1, right);
	}
	else
	{
		insertsort(a + left, keyi - 1 - left + 1);//a+left是左区间起始位置,keyi - 1 - left + 1元素个数
	}
	if ((right - keyi - 1) > 10)
	{
		quicksort(a, left, keyi - 1);
		quicksort(a, keyi + 1, right);
	}
	else
	{
		insertsort(a + keyi + 1, right - keyi - 1 + 1);// a + keyi + 1是右区间起始位置,right-keyi-1+1元素个数
	}
}

这差不多是最好效率的快排排序了,大家可以把优化的和不优化的都测测看看性能是不是提高了,接下来我们来讲非递归的快排了。

五、非递归版本

我们递归的本质是,开辟先的函数空间,主要保存的左右区间的下标,我们可以通过栈来实现,分别把左右区间入栈,然后在进行单趟排序,调用之前的方法,然后再左右区间入栈
来看看图解:
在这里插入图片描述
大家跟着我的步骤走,我·采用的单趟排序是hoare法,我们来看代码:

void QuickSortNonR(int* a, int left, int right)
{
	stack ST;
	STackInit(&ST);
	if (left >= right)
		return;
	StackPush(&ST, right);//先入右
	StackPush(&ST,left);//再入左
	while (!StackEmpty(&ST))
	{
		int left = StackTop(&ST);//出左
		StackPop(&ST);
		int right = StackTop(&ST);出右
		StackPop(&ST);
		int keyi = partsort1(a, left, right);//前三种方法都可以
		if (keyi - 1 - left >=1)
		{
			StackPush(&ST, keyi - 1);//左区间右边入
			StackPush(&ST, left);//左区间左边入
		}
		if (right - keyi - 1 >= 1)
		{
			StackPush(&ST, right);//右区间右边入
			StackPush(&ST, keyi+1);//右区间左边入
		}
	}
}

这个方法我们要先导入一个之前写过的栈,再之前的博客有介绍过,然后就利用栈的特性来实现快排的非递归,大家一定要多画画图。希望我这个图解能给大家解答困惑

四、总结

今天加的内容非常的多,大家一定要多消化消化,其实快速排序是选择排序家族,冒泡也是选择家族的一种,因为冒泡比较简单,我就没有和快排一起介绍了,希望大家可以知道,今天的,关于时间复杂度我可能再后期的博客会更新,把前面集中博客进行对比的介绍,到时候欢迎大家过来支持一下,今天的知识就先分享到这里了,我们下篇再见
在这里插入图片描述

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

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

相关文章

Midjourney AI绘画工具使用保姆级教程

系列文章目录 之后补充 文章目录系列文章目录写在前面一、Midjourney是什么&#xff1f;二、使用步骤1.完成Discord注册2.打开Midjourney官网3.开始画图后记写在前面 据悉&#xff0c;自3月30日&#xff0c;Midjourney已叫停免费试用服务&#xff0c;如上图所示。 创始人表示原…

代码随想录_226翻转二叉树、101对称二叉树

leetcode 226. 翻转二叉树 ​​​226. 翻转二叉树 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1]示例 2&#xff1a; 输入&#xff1a;r…

【Android开发】App Bundle技术之动态功能模块

前言 自 2021 年 8 月起&#xff0c;Google Play 将开始要求新应用使用 Android App Bundle 进行发布。该格式将取代 APK 作为标准发布格式。虽然这个政策目前还无法影响到国内应用&#xff0c;但是作为Android开发者&#xff0c;对于新的动态还是要有一定的认识。 Android A…

产品经理必读|用户研究方法总结②

随着互联网的发展&#xff0c;用户体验设计逐渐成为了产品设计中不可或缺的一部分&#xff0c;而要进行好的用户体验设计&#xff0c;就需要进行用研。但是&#xff0c;如何选用合适的用研方法&#xff0c;却是很多产品经理和设计师面临的难题。下面&#xff0c;就让我们来探讨…

3.3 泰勒公式

学习目标&#xff1a; 复习微积分基础知识。泰勒公式是微积分的一个重要应用&#xff0c;因此在学习泰勒公式之前&#xff0c;需要复习微积分的基本概念和技能&#xff0c;包括函数的导数和微分、极限、定积分等。可以参考MIT的微积分课程进行复习和加强。 学习泰勒级数和泰勒…

ftp传输文件大小有限制吗 ftp文件传输工具有哪些

这两年&#xff0c;线上办公逐渐常态化&#xff0c;相信大家对ftp这个概念也比较熟悉了。ftp&#xff0c;即文件传输协议&#xff0c;线上办公就是用ftp软件进行文件传输的。那ftp传输文件大小有限制吗,ftp文件传输工具有哪些我们一起来看看。 一、ftp传输文件大小有限制吗 f…

还在手动测试?那是那还不知道Python自动化测试的强大之处

目录&#xff1a;导读 引言 1.关于自动化测试的概述 2.Selenium元素定位实战 写在最后 引言 Python自动化测试是当今广泛使用的自动化测试技术之一。它的简单易学、开放源代码和丰富的第三方库使得其成为程序员和测试人员的首选工具之一。 Python自动化测试不仅可以帮助我…

【LeetCode: 剑指 Offer 60. n个骰子的点数 | 数学+ 暴力递归=>记忆化搜索=>动态规划】

&#x1f34e;作者简介&#xff1a;硕风和炜&#xff0c;CSDN-Java领域新星创作者&#x1f3c6;&#xff0c;保研|国家奖学金|高中学习JAVA|大学完善JAVA开发技术栈|面试刷题|面经八股文|经验分享|好用的网站工具分享&#x1f48e;&#x1f48e;&#x1f48e; &#x1f34e;座右…

git的操作使用三大块,常用的命令都在这里简单明了

码云网址&#xff1a;Gitee - 企业级 DevOps 研发效能平台 注册登录 创建仓库 仓库名称&#xff1a;必填&#xff0c;每个仓库都需要有一个名称&#xff0c;同一个码云账号下的仓库名称不能重复 路径&#xff1a;访问远程仓库时会使用到&#xff0c;一般无需手动指定&#xf…

C. Pinkie Pie Eats Patty-cakes(二分)

Problem - C - Codeforces 小粉饼买了一袋不同馅料的馅饼饼!但并不是所有的馅饼饼在馅料上都各不相同。换句话说&#xff0c;这个袋子里有一些馅料相同的馅饼。小粉派一个接一个地吃蛋糕。她喜欢玩&#xff0c;所以她决定不只是吃馅饼蛋糕&#xff0c;而是尽量不经常吃同样馅料…

Android 性能优化——ANR监控与解决

作者&#xff1a;Drummor 1 哪来的ANR ANR(Application Not responding):如果 Android 应用的界面线程处于阻塞状态的时间过长&#xff0c;会触发“应用无响应”(ANR) 错误。如果应用位于前台&#xff0c;系统会向用户显示一个对话框。ANR 对话框会为用户提供强制退出应用的选项…

Mybatis03学习笔记

目录 使用注解开发 设置事务自动提交 mybatis运行原理 注解CRUD lombok使用&#xff08;偷懒神器&#xff0c;大神都不建议使用&#xff09; 复杂查询环境&#xff08;多对一&#xff09; 复杂查询环境&#xff08;一对多&#xff09; 动态sql环境搭建 动态sql常用标签…

Unity Game FrameWork—模块使用—对象池分析

官方说明&#xff1a;提供对象缓存池的功能&#xff0c;避免频繁地创建和销毁各种游戏对象&#xff0c;提高游戏性能。除了 Game Framework 自身使用了对象池&#xff0c;用户还可以很方便地创建和管理自己的对象池。 下图是Demo中用到的对象池&#xff0c;所有的实体以及UI都使…

SpringBoot集成ChatGPT实现AI聊天

前言 ChatGPT已经组件放开了&#xff0c;现在都可以基于它写插件了。但是说实话我还真没想到可以用它干嘛&#xff0c;也许可以用它结合文字语音开发一个老人小孩需要的智能的说话陪伴啥的。 今天我就先分享下SpringBoot结合ChatGPT&#xff0c;先看看对话效果。 一、依…

清明-前端

明天面快手前端&#xff0c;正好借这个机会&#xff0c;做做毕设吧。顺便整理一下前端的面试内容。 何良蓉说&#xff0c;他觉得学的时候开心&#xff0c;玩的时候也开心。我觉得不开心。可能他掌握生活的秘密了吧。 如果他没对我撒谎的话。 看了眼别人的面经&#xff0c;就知…

一键禁用系统防火墙

你也可以通过批处理命令来实现 桌面空白地方右键选择新建记事本将下面代码复制到记事本里&#xff0c;然后保存为.bat类型的文件&#xff1b;保存完成运行即可。 Echo off Echo -------------------------------------------------------------------------- Echo 禁用系统防火…

【CSS】定位 ② ( 静态定位 | 相对定位 )

文章目录一、静态定位二、相对定位1、标准流下的盒子模型代码示例2、相对定位下的盒子模型代码示例一、静态定位 CSS 中的 静态定位 是 默认的定位方式 , 就是无定位 , 设置该定位方式 , 定位盒子不生效 ; 为盒子模型 设置 静态定位 模式 , 该 盒子模型 就会按照标准流的方式 …

【面试】spring中怎么解决循环依赖问题?

文章目录前言1、什么是循环依赖&#xff1f;2、Spring怎么解决循环依赖3、如何解决&#xff1f;4、怎么样的循环依赖无法处理?5、总结:前言 思考: 什么是循环依赖&#xff1f;Spring怎么解决循环依赖Spring对于循环依赖无法解决的场景 1、什么是循环依赖&#xff1f; 循环…

Run Loops

Run Loops 运行循环是与线程相关的基本基础结构的一部分。运行循环是事件处理循环&#xff0c;用于安排工作并协调传入事件的接收。运行循环的目的是在有工作要做时让线程保持忙碌&#xff0c;在没有工作要做时让线程休眠。 运行循环管理不是完全自动的。您仍然必须设计线程代…

【Java Web】012 -- SpringBootWeb综合案例(登录功能、登录校验、异常处理)

目录 一、登录功能 1、基础登录功能 ①、SQL语句 ②、接口参数 ③、实现思路 ④、实现步骤 2、联调Bug&#xff08;没有Cookie或Session&#xff09; 二、登录校验 1、登录校验的实现思路 2、会话技术 ①、会话与会话跟踪 ②、会话跟踪方案对比 Cookie Session …