快速排序详解(递归实现与非递归实现)

news2024/11/23 21:42:28

目录

一、快速排序的基本思想

二、将序列划分成左右区间的常见方法

2.1hoare版本(动图+解释+代码实现)

2.2挖坑法

2.3前后指针法

三、快速排序的初步实现

四、快速排序的优化实现

4.1快排的特殊情况

4.2对区间划分代码的优化

4.3小区间优化

五、快速排序的非递归实现

六、总结


一、快速排序的基本思想

快速排序是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);
}
上述为快速排序递归实现的主框架,会发现与二叉树前序遍历规则非常像,先取中间,递归左区间,再递归右区间。

二、将序列划分成左右区间的常见方法

从上面的主框架就可以看出,我们需要一个partion函数,将序列分为左区间和右区间。下面我将介绍三种划分左右区间的方法。我通常是选取最左边的数作为基准值,将比它小的数都换到它的左边,比它大的数都换到它的右边。

2.1hoare版本(动图+解释+代码实现)

hoare版本的思想就是选取最左边的数作为基准值,一个左标志一个右标志,左标志指向序列的第一个元素,右标志指向序列的最后一个元素。让右标志先开始往左走,找比基准值小的数,找到了就停下来;再让左标志开始往右走,找比基准值大的数,找到了就停下来,然后交换左右标志所指向的值然后重复红色字体的过程,直到左标志和右标志相遇,最后交换基准值和相遇标志所指向的值。基准值就出现在了它最后应该出现的地方,它的左区间都是小于等于它的值,右区间都是大于等于它的值。这时可能有人就会问了,相遇标志换到最前面了,一定能保证相遇标志对应的值比基准值小吗?答案是一定的。因为如果是左标志停下来了右标志走来跟左标志相遇,左标志对应的值一定是比基准值要小的(交换完以后左标志停下来右标志继续走,此时左标志对应的值一定是比基准值要小的);如果是右标志停下来左标志走来跟右标志相遇,那右标志一定是找到比基准值小的数才会停下来,同样可以保证相遇标志对应的值比基准值要小。

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]);
	return left;
}

2.2挖坑法

挖坑法虽然与hoare的方法实现上有差异,但思想上其实差别不大。

坑位就是最终key要去的位置。

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--;
		}
		a[hole] = a[right];
		hole = right;
		//找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	//左右标志相遇在坑位上,相遇时坑位的左边都小于等于key,右边都大于等于key,
	//坑位就是key应该去的位置。
	a[hole] = key;
	return hole;
}

2.3前后指针法

prev最终在的位置的值一定会比key的值要小,prev最终在的位置也就是key最终要去的位置。交换到最后prev位置以及它前面的值都是比key要小的,后面的值都是大于等于key的。

int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = prev + 1;
	int keyi = left;
	while (cur <= right)
	{
		if ( a[keyi] > a[cur]  && ++prev != cur)
			//a[keyi] > a[cur]找小,但如果a[keyi] > a[cur]一直成立,
			//++prev 会一直== cur不会拉开差距。只有a[keyi] <= a[cur],++prev 才会和cur拉开差距。
			Swap(&a[prev], &a[cur]);
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}

三、快速排序的初步实现

void QuickSort(int* a, int left, int right)
{
	if (left >= right)//区间只有一个值或区间不存在时就返回
		return;
	
	int keyi = PartSort1(a, left, right);
    //这里用PartSort1/PartSort2/PartSort3都可以,时间效率都是一样的
	QuickSort(a, left, keyi-1);
	QuickSort(a, keyi+1, right);
	//不断递归左区间和右区间
}

四、快速排序的优化实现

4.1快排的特殊情况

上面的写法面对绝大多数情况的排序已经可以实现时间复杂度接近O(n*logn),但面对某些特殊的情况,比如说你要将一个序列排成一个升序序列,然而这个序列本身就是一个升序序列,那就会造成keyi的左边没有数而其他数都在它的右边的情况,这样就会造成时间复杂度变成O(n^{2}),影响了快排的效率。看下图解释,红色矩形代表keyi的位置,如果是一个完全无序的序列进行排序,keyi所处的为值一般不会特别靠左也不会特别靠右,能保证排序的时间复杂度接近O(n*logn)

但如果是下面这种情况keyi一直位于最左侧,就会使时间复杂度为 O(n^{2})(每一次遍历时间复杂度为O(n),遍历n次)。

 

所以为了防止这种特殊情况的出现,可以对划分区间的代码进行一点优化。

4.2对区间划分代码的优化

我们还是可以取最左边的数作为key,只是要将最左边的数换成一个在序列中不是那么大也不是那么小的数,在此我们引进了一段三数取中代码。

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

这段代码负责找到序列中最左边,最右边,中间三个数中第二大的那个数。区间划分代码加入三数取中后,就可以规避掉刚才所说的那种特殊情况了。

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)
	{
		//找小
		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]);
	return left;
}

这里以hoare版本为例,其它两种区间划分方法也可以加上三数取中

4.3小区间优化

因为在递归到后期时,有的小序列已经接近有序,使用直接插入排序效率就会很高。(直接插入排序在希尔排序那篇博客已有实现,在这里就不再赘述)

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	//小区间优化
	if ((right - left + 1) > 10)//当区间长度大于10时
	{
		int keyi = PartSort1(a, left, right);
		QuickSort(a, left, keyi-1);
		QuickSort(a, keyi+1, right);
	}
	else//区间长度小于10时
	{
		InsertSort(a + left, right - left + 1);
	}
}

五、快速排序的非递归实现

快排使用到了递归的思想和方法,但是递归如果递归太深的话就会有爆栈的风险,所以在这里也介绍一下快速排序的非递归实现方法。因为快排是先处理左边的序列再处理右边的序列,这里就需要用到数据结构中的栈了,先入右再入左,进行排序。(栈的结构在之前的博客中已经实现过了,在这里同样不赘述)

void QuickSortNonR(int* a, int left, int right)
{
	Stack st;
	StackInit(&st);

	StackPush(&st, right);
	StackPush(&st, left);

	while (StackEmpty(&st))
	{
		int begin = StackTop(&st);
		StackPop(&st);
		int end = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort1(a, begin, end);
        //不断把大区间化成小区间处理
		if (keyi + 1 < end)
		{
			StackPush(&st, end);
			StackPush(&st, keyi+1);
		}

		if (begin < keyi - 1)
		{
			StackPush(&st, keyi-1);
			StackPush(&st, begin);
		}
	}
	StackDestroy(&st);
}

六、总结

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定

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

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

相关文章

邮政编码,格式校验:@ZipCode(自定义注解)

目标 自定义一个用于校验邮政编码格式的注解ZipCode&#xff0c;能够和现有的 Validation 兼容&#xff0c;使用方式和其他校验注解保持一致&#xff08;使用 Valid 注解接口参数&#xff09;。 校验逻辑 有效格式 不能包含空格&#xff1b;应为6位数字&#xff1b; 不校验…

5G安卓核心板-MT6833/MT6853核心板规格参数

随着智能手机的不断发展&#xff0c;芯片技术在推动手机性能和功能方面发挥着关键作用。MT6833和MT6853安卓核心板是两款高度集成的基带平台&#xff0c;为LTE/5G/NR和C2K智能手机应用提供强大的处理能力和多样化的接口。 这两款安卓核心板都集成了蓝牙、FM、WLAN和GPS模块&…

CSS网页标题图案和LOGO SEO优化

favicon图标 将网页的头名字旁边放入一个图案 想将想要的图案切成png图片 然后把png图片转换成ico图案可以借助进行访问 将语法引用到head里面 SEO译为搜索引擎优化。是一种利用搜索引擎的规则提高网站有关搜索引擎的自然排名的方式 SEO的目的是对网站进行深度的优化&…

SQL Server 创建表

切换数据库&#xff0c;判断是否存在 --切换数据库 use DBTEST--判断表是否存在 --创建的所有表都可以在sys.boject中找到&#xff0c;所以这里在sys.objects中查找是否有名字为department的表并且type为U 即用户生成的表 if exists(select * from sys.objects where namedepa…

轻松制作SOP作业指导书:掌握流程,节省时间

企业启用标准作业程序sop能够将企业生产作业的操作步骤、技术经验以及要求用统一的格式描述记录下来&#xff0c;以此规范和指导日常的工作&#xff0c;sop这种形式对企业的长远发展和精益化生产有能够带来巨大的帮助。 制作sop作业指导书其一&#xff0c;能够把企业的技术、经…

uniapp +vue3 练习 首页页面展示 我的页面展示 登录展示 拨打固定的电话 页面跳转

uniapp拨打固定的电话 function Hotline() {// 拨打电话uni.makePhoneCall({phoneNumber: 19969547693})}页面跳转 //普通跳转function homepage() {uni.navigateTo({url: /pages/homepage/homepage});}//二、uni.redirectTo关闭当前页面&#xff0c;跳转到应用内的某个页面。…

图像滤波总结

中值滤波器 中值滤波器是一种常用的非线性滤波器&#xff0c;其基本原理是&#xff1a;选择待处理像素的一个邻域中各像素值的中值来代替待处理的像素。主要功能使某像素的灰度值与周围领域内的像素比较接近&#xff0c;从而消除一些孤立的噪声点&#xff0c;所以中值滤波器能够…

使用clip-path来画不同的形状,三角形,多边形,菱形,六边形等等

介绍 单词"clip path"的直译过来就是&#xff1a;裁剪路径 clip-path CSS 属性可以创建一个只有元素的部分区域可以显示的剪切区域。区域内的部分显示&#xff0c;区域外的隐藏。剪切区域是被引用内嵌的 URL 定义的路径或者外部 SVG 的路径。 也就是说&#xff0c…

LeetCode【11】 盛水最多的容器

题目&#xff1a; 分析&#xff1a; 1、双指针&#xff0c;储水为&#xff08;R-L &#xff09;* 二者较小高度&#xff0c;如题目&#xff0c;(9-2)* 7 49 2、双指针向中间靠&#xff0c;每次移动较矮的指针。 代码&#xff1a; public int maxArea(int[] height) {int l…

STC89C51基础及项目第15天:小车测速、添加语言识别控制

1. 小车测速的原理&#xff08;281.126&#xff09; 测速模块 用途&#xff1a; 广泛用于电机转速检测&#xff0c;脉冲计数,位置限位等。 逻辑&#xff1a; 有遮挡&#xff0c;输出高电平&#xff1b;无遮挡&#xff0c;输出低电平 接线 VCC 接电源正极 3.3 - 5VGND 接电源负…

YOLOv7改进:极简的神经网络模型 VanillaNet---VanillaBlock助力检测,实现暴力涨点 | 华为诺亚2023

💡💡💡本文属于原创独家改进:极简模块VanillaBlock,以极简主义的设计为理念,网络中仅仅包含最简单的卷积计算,去掉了残差和注意力模块,二次创新引入到YOLOv7中取得了不俗的效果。 极简模块VanillaBlock | 亲测在多个数据集实现涨点; 收录: YOLOv7高阶自研专…

【JAVA】最容易忽视的数据类型——枚举

个人主页&#xff1a;【&#x1f60a;个人主页】 系列专栏&#xff1a;【❤️初识JAVA】 前言 Java枚举是一个特殊的类一般表示一组常量,比如一年的 4个季节,一年的 12 个月份,一个星期的7天,方向有东南西北等。今天就让我们来学习一下在JAVA中这个特殊的类。 枚举 枚举是一…

LeetCode: 1395. 统计作战单位数

目录 1. 解法一&#xff1a;枚举中点 2. 解法二&#xff1a;树状数组 离散化 优化解法一 原题链接&#xff1a;1395. 统计作战单位数 - 力扣&#xff08;LeetCode&#xff09; 题目描述&#xff1a; n 名士兵站成一排。每个士兵都有一个 独一无二 的评分 rating 。 每 3 个士…

【JWT】快速了解什么是jwt及如何使用jwt

一、导言 1、什么是jwt及组成部分 JWT&#xff08;JSON Web Token&#xff09;是一种用于在网络应用间安全传递声明&#xff08;claim&#xff09;的开放标准。它由三部分组成&#xff1a;头部&#xff08;Header&#xff09;、载荷&#xff08;Payload&#xff09;和签名&…

ARM 按键控制 LED灯,蜂鸣器,风扇

main.c: #include "uart.h" #include "key_it.h" int main() {all_led_init();uart4_init();//串口初始化//中断初始化key_it_config();key3_it_config();buzzer_init();fan_init();while(1){//保证主程序不结束}return 0; }src/key_it.c: #include"…

嵌入式面试常见问题(一)

目录 1.什么情况下会出现段错误&#xff1f; 2.swap() 函数为什么不能交换两个变量的值 3.一个函数有六个参数 分别放在哪个区&#xff1f; 4.定义一个变量&#xff0c;赋初值和不赋初值分别保存在哪个区&#xff1f; 5.linux查看端口状态的命令 6.结构体中->和.的区…

简单多状态dp【动态规划】

目录 一、按摩师 二、打家劫舍 三、删除并获得点数 四、粉刷房子 五、买卖股票的最佳时机 六、买卖股票的最佳时机&#xff08;含手续费&#xff09; 七、买卖股票的最佳时机III 八、买卖股票的最佳时机IV 一、按摩师 class Solution { public:int massage(vector<int>…

Avalonia如何更改全局背景色

1.项目下载地址&#xff1a;https://gitee.com/confusedkitten/avalonia-demo 2.UI库Semi.Avalonia&#xff0c;项目地址 https://github.com/irihitech/Semi.Avalonia 3.ColorView&#xff0c;使用Semi.Avalonia.ColorPicker&#xff0c;Nuget获取就行 4.样式预览 以下是…

MyCat实战

概念介绍 在 MyCat 的整体结构中&#xff0c;分为两个部分&#xff1a;上面的逻辑结构、下面的物理结构。 在 MyCat 的逻辑结构主要负责逻辑库、逻辑表、分片规则、分片节点等逻辑结构的处理&#xff0c;而具体的数据存储还是在物理结构&#xff0c;也就是数据库服务器中存储的…

云原生之使用Docker部署开源建站工具Halo-V2.10版本

云原生之使用Docker部署开源建站工具Halo-V2.10版本 一、Halo-V2.10介绍1.1 Halo简介1.2 Halo特点 二、本次实践规划2.1 本地环境规划2.2 本次实践介绍 三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本 四、下载Halo镜像五、部署Halo5.1…