[数据结构基础]排序算法第三弹 -- 快速排序

news2024/11/14 11:04:08

目录

一. 快速排序的基本思想

二. 快速排序的递归实现

2.1 单趟快速排序的实现

2.1.1 Hoare法实现单趟快排

2.1.2 挖坑法实现单趟快排

2.1.3 前后指针法实现单趟快排

2.2 递归快排的整体实现

三. 快速排序的时间复杂度分析

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

4.1 快速排序非递归实现的思路

4.2 非递归实现快排函数代码

五. 对于快速排序的两种优化方案

5.1 三数取中优化

5.2 小区间非递归优化


前置说明:本文以排升序为例进行讲解

一. 快速排序的基本思想

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:对于任意一组待排序数据,选取其中任何一个数据作为关键字key,将这组数据进行调整,排升序时,应使位于key前面的数据都小于等于key,使key后面的数据都大于等于key。假设我们称key之间的数据集合为左子序列,key后面的数据集合为右子序列,在左右子序列中选key,进一步划分左右子序列,直到所有数据都按顺序(升序或降序)排好。

一般来说,选取待排序数组的第一个元素或最后一个元素作为key值。

图1.1 快速排序的基本思想图解

二. 快速排序的递归实现

2.1 单趟快速排序的实现

所谓的单趟快速排序,就是给定一组数据并选取key值,调整数据,让左子序列都小于或等于key,右子序列都大于或等于key的过程。

实现单趟快速排序的方法有三种:Hoare法(由发明快排的人提出)、挖坑法、前后指针法。

2.1.1 Hoare法实现单趟快排

Hoare法的基本思路为:选定key值之后,给定位于左侧和右侧的两个指针left和right,left和right分别运动,排升序时在数据集左边找大于key的值,在数据集右边找小于key的值,将右边小于key的值和左边大于key的值进行交换。当left和right相遇时,将相遇点的值和key值交换,完成单趟快速排序。

注意:left和right指针并不是同时运动的,而是有一定的先后顺序。

  • 如果选取最左侧的数据作为key值,则right先向左运动。
  • 如果选择最右侧的数据作为key值,在left先向右运动。

left和right遵循这样的先后运动规则的目的是:交换key和左右指针相遇点数据后,保证key前面的左子序列中没有大于key的数,key后面的右子序列中没有小于key的数。

图2.1以数组arr[]={6,1,2,7,9,3,4,5,10,8}为例,展示了以最左侧数据为key的单趟快速排序实现过程。

图2.1 以最左侧数据为key的Hoare法单趟快排实现过程

Hoare法单趟快排实现代码:

//数据交换函数
void swap(int* px, int* py)
{
	assert(px && py);
	int tmp = *px;
	*px = *py;
	*py = tmp;
}

//单趟快速排序(hoare法)
int PartionSort1(int* a, int left, int right)
{
	assert(a);
	//最左侧数据做key值
	int keyi = left;   //key的下标
	int key = a[keyi];  

	while (left < right)
	{
		//找右侧小于key的数
		while (left < right && a[right] >= key)
		{
			--right;
		}

		//找左侧大于key的数
		while (left < right && a[left] <= key)
		{
			++left;
		}

		//交换大于key和小于key的数
		swap(&a[left], &a[right]);
	}

	//将key放到right和left相遇的位置
	swap(&a[keyi], &a[left]);

	return left;
}

2.1.2 挖坑法实现单趟快排

挖坑法,顾名思义,就是在某一位置挖一个坑,填入相应数据。假设取数组中最左边的值为key,定义左指针left和右指针right,挖坑法的基本实现流程为:

  1. 保存key值,初始化坑为hole=left。
  2. 右指针先向左移动,找最右侧比key小的数,并将这个数放入左侧的坑中,更新坑为此时的right值。
  3. 左侧指针再向右移动,找左侧比key大的数,并将这个数放入右侧的坑中,更新坑为此时的left值。
  4. 重复步骤2和3,直到left和right相遇。将坑更新为相遇点的下标,将key值放入坑中。

图2.2以arr[]={6,1,2,7,9,3,4,5,10,8}为例,展示了挖坑法实现单趟快排的过程。

图2.2 挖坑法实现单趟快排的过程

挖坑法单趟快排的实现代码:

//单趟快速排序(挖坑法)
int PartionSort2(int* a, int left, int right)
{
	assert(a);

	int keyi = left;
	int key = a[keyi];

	int hole = left;  //坑的下标

	while (left < right)
	{
		//找右边小于val的数,放入左边的坑中
		while (left < right && a[right] >= key)
		{
			--right;
		}

		a[hole] = a[right];
		hole = right;

		//找左边大于val的数,放入右边的坑中
		while (left < right && a[left] <= key)
		{
			++left;
		}

		a[hole] = a[left];
		hole = left;
	}

	a[hole] = key;

	return hole;  //返回最终坑的下标(左右子序列的分界)
}

2.1.3 前后指针法实现单趟快排

定义前后两个指针prev和cur,排升序,cur向右运动,找比key小的数据,当找到比key小的数据时,prev先执行自加操作,然后再交换cur处的数据和prev处的数据,当cur走出数组时,交换key和prev处数据。图2.3展示了以最左侧数据为key值和最右侧数据为key值prev和cur的初始位置,以最左侧数据为key时,选取初值prev=0、cur=1,以最右侧数据为key时,选取初值prev=-1、cur=0,图2.4以arr[] = {6,1,2,7,9,3,4,5,10.8}为例,展示了前后指针法实现单趟排序的过程。

图2.3 以最左侧数据和最右侧数据为key时前后指针的初值情况

图2.4  前后指针法实现单趟快排流程

前后指针法实现单趟快排的函数代码:

//单趟快速排序(前后指针法)
int PartionSort3(int* a, int left, int right)
{
	assert(a);

	int key = a[left];

	int cur = left + 1;  //前指针
	int prev = left;  //后指针

	while (cur <= right)
	{
		//如果cur处的数据小于key,prev先自加,然后交换prev和cur处的数据
		//++prev != cur是为了避免数据与本身交换而造成的运算资源浪费
		if (a[cur] <= key && ++prev != cur)
		{
			swap(&a[cur], &a[prev]);
		}
		++cur;
	}

	swap(&a[left], &a[prev]);

	return prev;
}

2.2 递归快排的整体实现

递归实现快排,是一种类似二叉树的递归调用。其整体思路为:

  1. 对整组数组进行单趟快排,使key值到它应该放置的位置,单趟快排后的左子序列值都小于或等于key,右子序列值均大于或等于key。
  2. 递归,分别对左子序列和右子序列进行单趟快排,然后进一步分解左子序列和右子序列,直到所有数据均被放置到正确位置上。

图2.5展示了快排的递归调用逻辑,图中长方块表示进行单趟快排的数组,短方块表示单趟快排后key所处的位置,每次对一组数据执行完一次单趟快排,数组就会被key分割为左右两部分,再采用递归的方式的左右两部分数组单趟快排直到快排全部完成即可。

注:单趟排序方法的选择不改变快排的整体递归逻辑,也不影响排序效果。

图2.5  快速排序的递归逻辑

快速排序函数代码:

//快速排序
void quickSort(int* a, int left, int right)
{
	assert(a);

	if (left >= right)
	{
		return;
	}

	int midi = PartionSort3(a, left, right);   //单趟快排

	quickSort(a, left, midi - 1);    //左子序列单趟快排
	quickSort(a, midi + 1, right);   //右子序列单趟快排
}

三. 快速排序的时间复杂度分析

快速排序最理想情况下的时间复杂度

如图3.1所示,在理想情况下,快速排序的每次单趟排序选出来的key值都恰好是待排序数组中的中位数,因此,每次快排后的左子序列和右子序列的数据个数相同。假设待排序数据的个数为N,每次递归要遍历的数据个数都大体为N,因此,整个快排过程要遍历数据NlogN次。

综上,最理想情况下,快排时间复杂度为O(NlogN)

图3.1 理想情况下key的位置及递归深度图解

快速排序最坏情况下的时间复杂度

如果待排序数据顺序有序或者逆序有序,如arr[]={9,8,7,6,5,4,3,2,1},那么无论选择数组最左边的元素为key还是最右边的元素为key,每次单趟快排完成后,key都位于被排序数组的最左侧或最右侧。如图3.2所示,对N个有序数据进行排序,共需要进行N层递归,每次递归遍历的数据都大体为N。因此,最坏情况下快排的时间复杂度为O(N)

除此之外,如果待排序数据已经有序,造成递归深度过大,还很有可能存在栈溢出的问题。

图3.2 最坏情况下key的位置及递归深度图解

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

在本文第三章中写道,如果对大量的有序数据或接近有序的数据进行排序,则很有可能会造成栈溢出的问题,为了能让快排适用于规模庞大的接近有序的数据,用非递归的方法实现快排显得尤为重要。

4.1 快速排序非递归实现的思路

对于快速排序的递归实现,需要记录单趟快排之后key值所处的下标,然后以key值为分界线,分别通过递归的方法,对左子序列和右子序列进行单趟快排。

非递归实现快排的思想与递归相同,我们只需要在一次快排之后记录下左子序列和右子序列的起始位置即可。这里,就需要借助栈这种数据结构来实现。假设给定含有10个数据,下标范围为0-9的数组arr[10],对arr采用非递归方法进行快速排序的过程如下:

  1. 设left = 0,right = 9,left先入栈,right再入栈。
  2. 判断栈是否为空,如果不为空从栈中两次提取数据,先取end为栈顶元素,然后删除栈顶元素,再取begin为栈顶元素,再删除栈顶元素。对下标位于[begin, end]之间的数据进行单趟快排,获取key在单趟快排之后的下标keyi。
  3. 进行完步骤2的单趟快排后,左子序列的下标范围是[begin, keyi - 1],右子序列的下标范围是[keyi+1, end],先判断end > keyi + 1是否成立,如果成立,end和keyi+1先后入栈,再判断keyi - 1 > begin是否成立,如果成立,keyi - 1和begin先后入栈。
  4. 重复步骤2和步骤3,直到栈为空。

图4.1以arr[10]为例,假设每次单趟快排后的key值的下标都满足关系keyi = (begin + end) / 2,对非递归快排的实现流程及入栈出栈数据进行图解演示。

图4.1 非递归实现快排图解

4.2 非递归实现快排函数代码

//快速排序函数(非递归)
void quickSortNonR(int* a, int left, int right)
{
	assert(a);

	ST ps;  //建立栈
	StackInit(&ps);  //初始化栈

	StackPush(&ps, left);  //第一趟排序的左右区间入栈
	StackPush(&ps, right);

	while (!StackEmpty(&ps))  //栈不为空
	{
		int end = StackTop(&ps);
		StackPop(&ps);     //拿出单趟排序右侧区间

		int begin = StackTop(&ps);
		StackPop(&ps);   //拿出单趟排序左侧区间

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

		if (keyi + 1 < end)
		{
			StackPush(&ps, keyi + 1);
			StackPush(&ps, end);
		}

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

	StackDestroy(&ps);  //栈销毁
}

五. 对于快速排序的两种优化方案

快速排序在对有序数组或接近有序的数组进行排序时,效率低下,很容易出现栈溢出的问题,采用三数取中选key值的方法进行优化。同时,对于数据量较小的数据,为了减少栈帧的建立,采用小区间非递归的方法进行优化。

5.1 三数取中优化

三数取中优化是为了对付接近有序的数据而采用的,三数取中就是在选取key值时,选择待排序数组中最左侧、最右侧和中间位置的三个数据中次大的那个作为key值,并将key值放到数组的头部或其尾部位置,以此来避免单趟排序后key值位于数组起始位置或末尾位置而造成大量的栈帧开辟。三数取中优化除了key值选取与不加优化有所不同以外,其余相同。

下面的代码以前后指针法实现单趟排序为例,展示了经三数取中优化后的快排函数代码。

//获取中间值下标函数
int GetMidPoint(int* a, int left, int right)
{
	assert(a);
	int mid = (left + right) / 2;  //中间节点下标

	//找出左边、右边和中间位置的三个数中次大的那个的下标
	if (a[left] > a[mid])
	{
		if (a[right] > a[left])
		{
			return left;
		}
		else if (a[right] > a[mid])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
	else
	{
		if (a[right] > a[mid])
		{
			return mid;
		}
		else if (a[right] > a[left])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

//数据交换函数
void swap(int* px, int* py)
{
	assert(px && py);
	int tmp = *px;
	*px = *py;
	*py = tmp;
}

/单趟快速排序(前后指针法)
int PartionSort3(int* a, int left, int right)
{
	assert(a);

	int keyi = GetMidPoint(a, left, right);  //获取key值坐标
	swap(&a[keyi], &a[left]);

	int key = a[left];

	int cur = left + 1;  //前指针
	int prev = left;  //后指针

	while (cur <= right)
	{
		if (a[cur] < key && ++prev != cur)
		{
			swap(&a[cur], &a[prev]);
		}
		++cur;
	}

	swap(&a[left], &a[prev]);

	return prev;
}

//快速排序
void quickSort(int* a, int left, int right)
{
	assert(a);

	if (left >= right)
	{
		return;
	}

	int midi = PartionSort3(a, left, right);   //对整个数组排序

	quickSort(a, left, midi - 1);
	quickSort(a, midi + 1, right);
}

5.2 小区间非递归优化

小区间优化就是在进行单趟快排的数据量较小时,为了减少函数栈帧的开辟,采用插入排序或冒泡排序等方法对数据进行排序。一般来说,当right - left + 1 < 10,即数据量小于10时,采用插入或冒泡排序法进行排序。

小区间非递归优化快排代码:


//快速排序
void quickSort(int* a, int left, int right)
{
	assert(a);

	if(right - left + 1 < 10)  //如果数据量小于10,采用插入排序
	{
		InsertSort(a + left, right - left + 1);
	}
	else
	{
		int midi = Partion3(a, left, right);   //整个数组单趟快排

		quickSort(a, left, midi - 1);  //左子序列单趟快排
		quickSort(a, midi + 1, right); //右子序列单趟快排
	}
}

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

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

相关文章

Promise详解与手写实现

Promise详解与手写实现Promise1、Promise介绍与基本使用1.1 Promise概述1.2 Promise的作用1.3 Promise的使用2、Promise API3、Promise关键问题4、Promise自定义封装5、async与await5.1. mdn文档5.2.async函数5.3.await表达式5.4.注意Promise 1、Promise介绍与基本使用 1.1 P…

电商维权,维权方法汇总【超全】

电商维权&#xff0c;就是维护线上渠道中自己的合法权益&#xff0c;其中包括消费者维权、品牌方维权、卖家维权。今天&#xff0c;我们来聊一聊消费者维权。 1、 维权类型 消费者在网购过程中难免遇到各种问题&#xff0c;主要就是产品质量问题、产品价格问题、产品售后问题、…

JavaWeb-VUEElement

JavaWeb-VUE&Element 1&#xff0c;VUE 1.1 概述 Vue 是一套前端框架&#xff0c;免除原生JavaScript中的DOM操作&#xff0c;简化书写。 Mybatis 是用来简化 jdbc 代码编写的&#xff1b;而 VUE 是前端的框架&#xff0c;是用来简化 JavaScript 代码编写的。前一天我们…

PID控制的方波响应

被控对象为一延迟对象&#xff1a;采样时间为20s&#xff0c;延迟时间为4个采样时间&#xff0c;即 80s&#xff0c;被控对象离散化为&#xff1a;y(k) -den(2)y(k- 1)num(2)u(k - 5)由于方波信号的速度、加速度不连续&#xff0c;当位置跟踪指令为方波信号时&#xff0c;如采用…

C++ 树进阶系列之树状数组的树形之路

1. 前言 树状数组也称二叉索引树&#xff0c;由Peter M. Fenwick于1994发明&#xff0c;也可称为Fenwick树。 树状数组的设计非常精巧&#xff0c;多用于求解数列的前缀和、区间和等问题&#xff0c;为区间类型问题提供了模板式解决方案。 数状数组简单易用&#xff0c;但对…

【100个 Unity实用技能】 | 修改Unity UI控件中默认字体配置

Unity 小科普 老规矩&#xff0c;先介绍一下 Unity 的科普小知识&#xff1a; Unity是 实时3D互动内容创作和运营平台 。包括游戏开发、美术、建筑、汽车设计、影视在内的所有创作者&#xff0c;借助 Unity 将创意变成现实。Unity 平台提供一整套完善的软件解决方案&#xff…

【C++提高编程1】一文带你吃透函数模板和类模板(附测试用例源码、测试结果图及注释)

&#x1f4dd;我的个人主页 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​&#x1f4ac;总结&#xff1a;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f58a;✉️今天你做别人不想做的事&…

域内权限维持:注入SSP

01、简介 SSP(Security Support Provider)是Windows操作系统安全机制的提供者。简单地说&#xff0c;SSP是个DLL文件&#xff0c;主要用来实现Windows操作系统的身份认证功能。在系统启动时&#xff0c;SSP 将被加载到lsass.exe进程中&#xff0c;攻击者通过自定义恶意的DLL文件…

解决ModuleNotFoundError: No module named ‘torch.fx‘

运行yolo v5 train python train.py 报错 ModuleNotFoundError: No module named ‘torch.fx’ torch版本不匹配 目前版本torchu1.7 #卸载pytorch pip uninstall torch 再安装 python -m pip install torch -i https://mirrors.aliyun.com/pypi/simple/ python -m pip是…

本周大新闻|Quest Pro降价至1099美元,传苹果AIGC或用于XR内容生成

本周大新闻正值春节假期&#xff0c;因此包含近两周&#xff08;1月16-1月29日&#xff09;的AR/VR新闻汇总。关于2022&#xff0c;近期我们发布了2022年AR/VR行业融资报告、2022年AR硬件总结、2022年VR硬件总结。AR方面&#xff0c;最新消息称苹果AIGC曝光&#xff0c;或用Sir…

通信数据中心供电系统故障影响区域分析定位

&#xff08;华北石油通信有限公司&#xff09;摘要&#xff1a;供电系统对于通信机房而言至关重要&#xff0c;一旦供电系统发生严重故障&#xff0c;需要快速制定出应急预案&#xff0c;使故障影响可控。本文提供一种对机房供电系统故障影响区域快速定位方法。该方法的实现思…

可观察性和安全性融合的紧迫性越来越高

根据一份新报告&#xff0c;融合可观察性和安全性的紧迫性越来越大。 软件情报公司 Dynatrace 公布了一项针对大型组织的 1,300 名 CIO 和高级 DevOps 经理&#xff08;包括来自澳大利亚的 100 名&#xff09;进行的独立全球调查的结果。 调查结果表明&#xff0c;随着对连…

OS 学习笔记(3) 操作系统的发展与分类

OS 学习笔记(3) 操作系统的发展与分类 这篇笔记对应的王道考研 1.2 操作系统的发展与分类&#xff0c;同时参考了 《Operating System Concepts, Ninth Edition》和 《 Operating Systems: Three Easy Pieces》&#xff08;俗称ostep&#xff09; 文章目录OS 学习笔记(3) 操作系…

【数据结构】堆的应用——TOP-K问题详解

目录 &#x1f34e;前言&#x1f34e;&#xff1a; &#x1f95d;一、TOP-K 问题概述&#x1f95d;&#xff1a; &#x1f349;二、不同解决思路实现&#x1f349;&#xff1a; ①排序法&#xff1a; ②直接建堆法&#xff1a; ③K 堆法&#xff08;最优解&#xff09;&a…

Redis简单入门

Redis简介 Redis是一个开源的使用C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value的NoSQL数据库。特点如下: 读写速度快&#xff1a;Redis官网测试读写能到10万左右每秒。速度快的原因这里简单说一下&#xff0c;第一是因为数据存储在内存中&#xff0c;我们知…

标准有效的项目开发流程

代码版本管理在项目中&#xff0c;代码的版本管理非常重要。每个需求版本的代码开发在版本控制里都应该经过以下几个步骤。在master分支中拉取该需求版本的两个分支&#xff0c;一个feature分支&#xff0c;一个release分支&#xff1b;feature分支用于接受个人分支merge过来的…

二叉树DFS、BFS

目录 1&#xff0c;DFS遍历 2&#xff0c;DFS遍历OJ实战 力扣 144. 二叉树的前序遍历 力扣 94. 二叉树的中序遍历 力扣 145. 二叉树的后序遍历 力扣 105. 从前序与中序遍历序列构造二叉树 力扣 106. 从中序与后序遍历序列构造二叉树 力扣 889. 根据前序和后序遍历构造二…

C++中的new、operator new与placement new

new operator 当我们使用了new关键字去创建一个对象时&#xff0c;你知道背后做了哪些事情吗&#xff1f; A* a new A;实际上这样简单的一行语句&#xff0c; 背后做了以下三件事情&#xff1a; 分配内存,如果类A重载了operator new&#xff0c;那么将调用A::operator new(…

TencentOS安装并运行多版本php

TencentOS版本3.1安装并运行php7&#xff0c;现在需要同时运行一个php8. php选择使用了php v8.0.27 采用编译安装的方式&#xff0c;编译命令如下&#xff1a; ./configure --prefix/application/php8 --with-config-file-path/application/php8/etc --with-mhash --with-o…

51单片机学习笔记-4矩阵键盘

4 矩阵键盘 [toc] 注&#xff1a;笔记主要参考B站江科大自化协教学视频“51单片机入门教程-2020版 程序全程纯手打 从零开始入门”。 注&#xff1a;工程及代码文件放在了本人的Github仓库。 4.1 矩阵键盘介绍 在键盘中按键数量较多时&#xff0c;为了减少I/O口的占用&#…