【数据结构】快速排序详解(递归版本)

news2024/9/21 2:42:31

目录

0. 前言

1. 快速排序的基本思想

2. 快速排序的不同版本的实现

2.1 hoare版本

2.1.1 单趟排序动图演示

2.1.2 递归展开多个子区间进行单趟排序

2.1.3 代码的具体实现

2.1.3.1 霍尔法单趟排序代码

2.3.1.2 霍尔法递归代码 

2.2 挖坑法

2.2.1 单趟排序方法动图演示:

2.2.2 递归展开多个子区间进行单趟排序

2.2.3 代码的具体实现

2.2.3.1 挖坑法单趟排序代码

2.2.3.2 挖坑法递归代码 

2.3 前后指针法

2.3.1 单趟排序方法动图

 

2.3.2 递归展开多个子区间进行单趟排序

​编辑

2.3.3 代码的具体实现

2.3.3.1 前后指针法单趟排序代码

2.3.3.2 前后指针法递归代码

0. 前言

快速排序算法是霍尔(Hoare)在1962年提出来的。

本期博客给同学们介绍,如何通过递归的不同写法的方法来实现简单的快速排序。 

1. 快速排序的基本思想

快速排序是一种 二叉树结构 的交换排序方法,其基本思想为:

任取待排序元素序列中的某元素作为 基准值(称其为key) ,按照该排序码将待排序集合 分割成两子序列 ,左子序列中所有元素 均小于 基准值,右子序列中所有元素 均大于 基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。  

这一过程相当于二叉树的 前序 遍历,先让基准值排列在相应位置,然后分割子序列,每次分割左右子序列都排好一个基准值,然后再分割左右子序列,直到序列中只有一个元素时即为最小子问题,当每一个根都排在了相应位置,那么序列就变得有序了,所以很容易就想到了用递归来实现快速排序这一算法。

2. 快速排序的不同版本的实现

  为了简单描述快排的思想,这里我们排整形数据而且排成升序。

2.1 hoare版本

2.1.1 单趟排序动图演示

单趟排序的结果:

 

  我们可以看到,我们以这个区间内的最左边的元素为key,然后定义了左、右指针来指向数组的下标(这里是两个整型变量,存储下标),先让右指针从数组右边开始向左遍历元素,当遍历到的值key值小时停下来,然后启动左指针,从左边开始遍历数组,当遍历到的值key值大时停下来,这个时候左指针指向数组中key值大的数值的下标,右指针指向数组中比key值小的数值的下标,这个时候我们交换这两个值,这样小的值就被换到了前面,大的值就被换到了后面,直到左右指针相遇。当左右指针相遇时的地方就是key值应该排在的相应位置,这个时候将key和相遇的地方交换数值就完成了单趟的排序。此时发现,key左边的值都比key小,右边的值都比key大。这样我们就排好了key这个元素。

在这个过程中,有同学可能会有这样的问题:

如果相遇位置的值比基准值 key 位置上的值大怎么办呢?

但其实我们发现这种情况是不存在的,

交换后的结果仍然是符合左边值的比key小,右边的值比key大

  其原因是,我们是先让右指针先向左遍历找比key小的值,找到后才让左指针向右遍历,这样可以保证相遇的时候指向的值比key小。

这需要分两种情况进行讨论的:一种是 key 在左边,另一种就是 key 在右边。

  第一种情况是:当右指针停下了,左指针向右遍历时没找到比key大的值,左指针与右指针相遇,这个时候指向的值一定是比key小的。

另一种情况是:右指针向左遍历的时候没有找到比key小的值,直接与左指针相遇了,这个时候又可以分成两种情况

  第一种情况是:左指针没动,指向key的位置,这个时候说明key本身的位置就是其相应的位置,不需要进行移动,这里自己跟自己交换相当于没移动。

 

  第二种情况是:左指针先前发生过与右指针的交换,交换后左指针指向的值一定是小于key的,这个时候右指针与左指针相遇后指向的值与key值交换,可以保证交换的值比key小。

 

2.1.2 递归展开多个子区间进行单趟排序

  我们通过单趟排序将key排到了正确的位置上,按照同样的方法,key位置左区间右区间分别进行同样的操作,每次单趟排序选出key排到相应位置,每排好key的位置后,就将key的左右分成两个区间,再在左右区间中选出key排好后再分成左右区间,我们发现他是一个函数递归的过程,递归的思想本质就是把大事化小,最后区间内只剩下一个元素时(只有一个元素可以认定为有序)或者区间内没有元素时是最小子问题。最后每个元素都排在了相应的位置上,数组就变有序了。

 下面是递归展开图:

   我们可以从上面的递归展开图看到,我们每次在区间内排出好key的位置,然后再在key的左右区间进行递归,再次排出key的位置,再分成两个子区间,每次递归就排好了一个值直到区间内只有一个元素或者没有元素时结束递归。这样下来,我们的数组就变有序了。

2.1.3 代码的具体实现

2.1.3.1 霍尔法单趟排序代码
void Swap(int* px, int* py)
{
    int tmp = *px; 
    *px = *py;
    *py = tmp;
}

// 快速排序hoare版本 单趟排序
int PartSort1(int* a, int left, int right)
{
	if (left >= right)
		return 0;
	//最小子问题 :
	//区间内只有一个值(left == right)或者为空(left >= right)
 
	int end = right;//先从右边往左找比key值小的数丢到前面
	int begin = left;//从左边下标从右找比key大的数丢到后面
	int key = left;//要排序的下标
 
	while (begin < end)//当左右指针相遇时结束
	{
		//从右往左找比key小的数
		while (begin < end && a[end] >= a[key])
		{
			end--;
		}
 
		//找到比key小的数后就从左往右找比key大的数
		while (begin < end && a[begin] <= a[key])
		{
			begin++;
		}
 
		//交换这两个数
		Swap(&a[begin], &a[end]);//功能相当于swap
 
	}
	//出来后说明begin和end相遇了,此时该下标位置就是key值排序后所在的位置
	Swap(&a[key], &a[begin]);//将key换到相应的位置
 
	key = begin;//更新key的下标
	return key;//返回排好了的元素的下标
}
2.3.1.2 霍尔法递归代码 
void QuickSort(int* a, int left, int right)
{
	if (left >= right)//当只有一个元素时是最小子问题
		return;
	//此区间执行单趟排序
	 
	//hoare法
	int key = PartSort1(a, left, right);//接收已经排好了的元素下标
 
	//递归子区间   [left,key - 1]key[key + 1,right]
	QuickSort(a, left, key - 1);
	QuickSort(a, key + 1, right);
	return;
}

 写完后我们可以随机生成一个数组

我们就用刚才的那组数据{6,1,2,7,9,3,4,5,10,8}, 来测试一下

 我们发现他按照升序排序成了{1,2,3,4,5,6,7,8,9,10},说明我们的代码没有任何问题!

2.2 挖坑法

挖坑法的思路与霍尔法的思路大致相同。

挖坑法思路过程分析:

  1. 创建左右指针。
  2. 首先从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)

2.2.1 单趟排序方法动图演示:

单趟排序后的结果:

 

下面我给大家画个图具体演示上面动图的过程

还是以同样的数列 6,1,2,7,9,3,4,5,8,10 为例:

31.png

1. 先将第一个数存放到 key 中,形成一个坑位:分别有 left 和 right 指向数列的最左端与最右端;

42.png

2.  right 先走,找到比 key 小的数,将该数丢到坑里;同时又形成了一个新的坑;

43.png

3. left 开始走,找到比 key 大的数,将该数丢到坑里;同时形成一个新的坑;

44.png

4. right继续找小,进行重复的操作;

45.png

5. left 找大;

46.png

6. right 找小;

47.png

7. left 找大;

8.若二者相遇就停下来;将 key 值放入坑;

48.png

至此,一趟排序已经完成!

  这个方法和上面的hoare版本都是先让右指针向左遍历找比key小的值,所以不会存在导致出现排完序后出现key值左边出现大于key的元素。

2.2.2 递归展开多个子区间进行单趟排序

 快速排序挖坑法的递归和hoare法差不多,将排好后的key值左右两边分成左右子区间进行递归。但是有一点要注意:这两种方法对同一个数组进行一次单趟排序后的数组元素位置不一定对应相同。

 我们将下面这组数进行递归,每次递归进行一次单趟排序排好key的位置

 

如果是hoare法,排好6后元素的顺序是这样:

如果是挖坑法,排好6后元素的顺序是这样:

我们可以发现这两种单趟排序后6的左右区间的元素顺序不是一一对应的,但是都符合快速排序的思想,即6的左边元素都小于6,6的右边的元素都大于6。

递归展开图:

2.2.3 代码的具体实现

2.2.3.1 挖坑法单趟排序代码
//单趟 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	//最小子问题 :
	//区间内只有一个值(left == right)或者为空(left >= right)
	if (left >= right)
		return left;
 
	//先从右边往左找比key值小的数填到坑里 然后right指向的地方就变成了新坑
	int end = right;
 
	//一开始坑是最左边的元素。
	//之后从左边下标从右找比key大的数填到右边的坑中。
	//然后左指针指向的元素就变成了新坑.
	int begin = left;
 
	int key = a[left];//保存要排序的值
 
	while (begin < end)//当左右指针相遇时结束
	{
		while (begin < end && a[end] >= key)//从右往左找比key小的值填到坑里
		{
			end--;
		}
 
		//此时begin位置是坑
		a[begin] = a[end];//将比key小的值填入坑
 
		while (begin < end && a[begin] <= key)//从左往右找比key大的值填到坑中
		{
			begin++;
		}
 
		//此时end位置是坑
		a[end] = a[begin];
	}
 
	//begin和end相遇的地方是key对应的位置
	a[end] = key;
	return end;//返回排好位置的元素的下标
}
2.2.3.2 挖坑法递归代码 
void QuickSort(int* a, int left, int right)
{
	if (left >= right)//当只有一个元素时是最小子问题
		return;
 
	//此区间执行单趟排序
	//hoare法
	//int key = PartSort1(a, left, right);//接收已经排好了的元素下标
	//挖坑法
	int key = PartSort2(a, left, right);//接收已经排好了的元素下标
 
	//递归子区间   [left,key - 1]key[key + 1,right]
	QuickSort(a, left, key - 1);
	QuickSort(a, key + 1, right);
	return;
}

2.3 前后指针

2.3.1 单趟排序方法动图

  

 单趟排序的结果:

 

和hoare版本的排序一样,将最左边的值作为待排序的key值,然后定义了前后指针prevcur。这里的prevcur指针不再是一左一右遍历,而是都以最左边为起点,然后cur先向右遍历,直到cur指向的值比key小,然后prev向后走一步,交换curprev指向的值,然后cur再向右遍历,找比key小的值,重复以上步骤,直到cur走到尽头的下一个越界的位置,这个时候我们prev所指向的位置就是key值所对应的位置,我们再交换这两个值,key就排到了其相应的位置上。

单趟排序存在的问题思考:

  这里单趟排序是把cur找到小于key的值与prev交换,所以prev指向的值不是key自己就是比key小的数,所以最后一次将keyprev位置对换时,必定是小的数和key交换,所以不会出问题。

2.3.2 递归展开多个子区间进行单趟排序

 有了前面的铺垫,这里的递归方法也是相同的,就是单趟排序排好key的位置后,递归key的左右子区间,再次排出key,再次递归左右子区间。

  注意:这三种排key的方法都不相同,所以单趟排序同一个数组后的元素排列顺序也不尽相同。

我们将下面这个数组进行递归,每次递归进行一次单趟排序排好key的位置

下面是递归展开图:

2.3.3 代码的具体实现

2.3.3.1 前后指针法单趟排序代码
void Swap(int* px, int* py)
{
    int tmp = *px; 
    *px = *py;
    *py = tmp;
}

//单趟排序 快速排序前后指针法
int PartSort3(int* a, int left, int right)//返回排好了的数据key下标
{
	//最小子问题 :
	//区间内只有一个值(left == right)或者为空(left >= right)
	if (left >= right)
		return left;
 
	int prev = left, cur = left + 1;//前后指针
	while (cur <= right)//当cur指向数组外时结束
	{
		//让后指针向右遍历找到比要排序的数小的值,
		// 如果找到了,那就让prev先++ 如果prev和cur指向同一个地方那就不交换
		// prev和cur指向不同元素就交换prev和cur的值 
		// 当cur走到排序的区域外时,prev的位置就是要排序的数所在的位置
		if (a[cur] < a[left] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;//cur指针往右走
	}
 
	//此时prev的位置就是key的位置
	Swap(&a[prev], &a[left]);
	return prev;//返回排好位置的下标
}
2.3.3.2 前后指针法递归代码
void QuickSort(int* a, int left, int right)
{
	if (left >= right)//当只有一个元素时是最小子问题
		return;
 
	//此区间执行单趟排序
	//hoare法
	//int key = PartSort1(a, left, right);//接收已经排好了的元素下标
	//挖坑法
	//int key = PartSort2(a, left, right);//接收已经排好了的元素下标
	//左右指针法
	int key = PartSort3(a, left, right);//接收已经排好了的元素下标
 
	//递归子区间   [left,key - 1]key[key + 1,right]
	QuickSort(a, left, key - 1);
	QuickSort(a, key + 1, right);
	return;
}

以上就是我们的三种不同的单趟排序进行递归实现快速排序的方法!

那么有同学思考说,有没有不用递归的方法呢?

这期博客我们先就讲到这儿,下期博客我们来探究一下,用非递归的方法如何实现快速排序!

如果你觉得博主讲的还不错对你有帮助的话,给我的博客留个赞和关注,

后期不断给大家讲解新的知识。我们下期见哦~👋👋

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

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

相关文章

二叉树的层序遍历(c)

我们先来了解一下什么是二叉树的层序遍历&#xff01; 层序遍历的理解 下图是一棵二叉树&#xff0c; 层序遍历这棵二叉树的顺序是&#xff1a; 1→2→3→4→5→6→7&#xff0c; 看图也很简单易懂&#xff0c; 第一层→第二层→第三层。 层序遍历的实现 如果二叉树的结…

Python3将Excel数据转换为文本文件

文章目录 python3安装使用Python将Excel数据转换为文本文件&#xff1a;逐步指南openpyxl库简介前提条件脚本解析代码详细解析实际应用场景使用示例 结论 python3安装 centos安装python3 Python3基础知识 使用Python将Excel数据转换为文本文件&#xff1a;逐步指南 在数据处理…

文本多语言 AI 摘要 API 数据接口

文本多语言 AI 摘要 API 数据接口 文本 / 文本摘要 AI 生成文本摘要 AI 处理 / 智能摘要。 1. 产品功能 支持多语言摘要生成&#xff1b;支持长文本处理&#xff1b;基于 AI 模型&#xff0c;持续迭代优化&#xff1b;不存储 PDF 文件&#xff0c;处理完即释放&#xff0c;保…

公路数据集、桥梁数据集、隧道数据集、地铁数据集、水坝数据集、挡土墙数据集

数据集概览 这个大规模的数据集专注于建筑裂缝检测&#xff0c;涵盖了地上设施&#xff08;如公路桥梁、铁路桥梁、水坝、挡土墙&#xff09;和地下SOC设施&#xff08;如公路/铁路隧道、地铁、水隧道&#xff09;。数据集包含了来40个市、县、区的不同SOC设施的52万张图像&…

显卡GPU电源、ATX电源、主板电源的一些关系?如何连接显卡/GPU电源?

文章目录 背景ATX电源在ATX接出来的电源线 实测数据PC主机开关机和复位&#xff1a;3.3V显卡16-pin 12VHPWR 如何连接显卡/GPU电源综述 背景 折腾装机、装显卡&#xff0c;ATX电源&#xff0c;各种转来转去。搞得云里雾里&#xff0c;如何删繁就简。找到根源。 本文介绍ATX电源…

数学公式篇

【一元二次方程的根】 x − b b 2 − 4 a c 2 a x {\frac{-b\sqrt{b^2-4ac}}{2a}} x2a−bb2−4ac ​​ △ b 2 − 4 a c △ b^2-4ac △b2−4ac 其中根的判别式 △ > 0 &#xff0c;有两个实根 △>0&#xff0c;有两个实根 △>0&#xff0c;有两个实根 其中根…

AI 时代程序员的应变之道

一、AI 浪潮来袭&#xff0c;编程界风云变幻 随着 AIGC 大语言模型如 ChatGPT、Midjourney、Claude 等的涌现&#xff0c;AI 辅助编程工具日益普及&#xff0c;程序员的工作方式正经历着深刻的变革。 分析公司 OReilly 日前发布的《2023 Generative AI in the Enterprise》报告…

Excel爬虫使用实例-百度热搜

原来excel也能爬虫抓取数据&#xff0c;而且简单好用 目标网址&#xff1a; https://top.baidu.com/board?tabrealtime 下面是一个excel爬虫的小小例子&#xff0c;爬取了百度热搜的前50&#xff08;还有一个置顶的热搜没有1&#xff0c;2&#xff0c;3编号&#xff09; 实现…

JVM面试真题总结(十二)

文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 对比Java内存模型与JVM内存模型的不同点 Java内存模型&#xff08…

PyTorch 池化层详解

在深度学习中&#xff0c;池化层&#xff08;Pooling Layer&#xff09;是卷积神经网络&#xff08;CNN&#xff09;中的关键组成部分。池化层的主要功能是对特征图进行降维和减少计算量&#xff0c;同时增强模型的鲁棒性。本文将详细介绍池化层的作用、种类、实现方法&#xf…

BLE 协议之物理层

目录 一、概述二、Physical Channel1、物理通道2、物理通道的细分 三、调制1、调制方式2、GFSK 四、发射机五、接收机六、收发机 一、概述 物理层&#xff08;Physical Layer&#xff09;是 BLE 协议栈最底层&#xff0c;它规定了 BLE 通信的基础射频参数&#xff0c;包括信号频…

Minio环境搭建(单机安装包、docker)(一)

前言&#xff1a; 项目中客户不愿意掏钱买oss&#xff0c;无奈只能给他免费大保健来一套。本篇文章只是记录验证可行性&#xff0c;毕竟minio太少文档了&#xff0c;参考着官网来。后面还会再出一套验证集群部署的文章。 一、资料 MinIO官网&#xff1a; MinIO | S3 Compatib…

Windows 安装 ZooKeeper 以及 IDEA 安装 zoolytic 连接工具

目录 前言 下载 解压 配置 启动服务 zoolytic 前言 在前公司做微服务开发时&#xff0c;使用的都是 Spring Cloud 的生态&#xff0c;服务的注册与发现中心用的 Eureka&#xff0c;也有使用 Nacos 的&#xff0c;远程调用则是用的 OpenFeign&#xff0c;换工作后&#x…

istio中serviceentry结合vs、dr实现多版本路由

假设有一个外部服务&#xff0c;外部服务ip为&#xff1a;10.10.102.90&#xff0c;其中32033为v1版本&#xff0c;32034为v2版本。 现在需要把这个服务引入到istio中&#xff0c;并且需要配置路由规则&#xff0c;使得header中x-version的值为v1的路由到v1版本&#xff0c;x-…

Gitee Pipeline 从入门到实战【详细步骤】

文章目录 Gitee Pipeline 简介Gitee Pipeline 实战案例 1 - 前端部署输入源NPM 构建Docker 镜像构建Shell 命令执行案例 2 - 后端部署全局参数输入源Maven 构建Docker 镜像构建Shell 命令执行参考🚀 本文目标:快速了解 Gitee Pipeline,并实现前端及后端打包部署。 Gitee Pi…

MYSQL数据库——MYSQL管理

MYSQL数据库安装完成后&#xff0c;自带四个数据库&#xff0c;具体作用如下&#xff1a; 常用工具 1.mysql 不是指mysql服务&#xff0c;而是指mysql的客户端工具 例如&#xff1a; 2.mysqladmin 这是一个执行管理操作的客户端程序&#xff0c;可以用它来检查服务器的配置和…

SpringMVC映射请求;SpringMVC返回值类型;SpringMVC参数绑定;

一&#xff0c;SpringMVC映射请求 SpringMVC 使用 RequestMapping 注解为控制器指定可以处理哪些URL请求 1.1RequestMapping修饰类 注解RequestMapping修饰类&#xff0c;提供初步的请求映射信息&#xff0c;相对于WEB应用的跟目录。 注&#xff1a; 如果在类名前&#xff0…

【车载开发系列】ParaSoft单元测试环境配置(三)

【车载开发系列】ParaSoft单元测试环境配置(三) 【车载开发系列】ParaSoft单元测试环境配置(三) 【车载开发系列】ParaSoft单元测试环境配置(三)一. 去插桩设置Step1:静态解析代码Step2:编辑Parasoft文件Step3:确认去插桩二. 新增测试用例Step1:生成测试用例Step2:执…

【网络安全】Node.js初探+同步异步进程

未经许可,不得转载。 文章目录 Node.js 基础介绍NPM 包管理安装同步与异步fs 模块示例child_process 模块Node.js 基础介绍 Node.js 是运行在服务器端的 JavaScript 环境。它基于 Chrome 的 V8 引擎,拥有高效的执行性能。Node.js 采用事件驱动的 I/O 模型,使得它在处理高并…

策略路由与路由策略的区别

&#x1f423;个人主页 可惜已不在 &#x1f424;这篇在这个专栏 华为_可惜已不在的博客-CSDN博客 &#x1f425;有用的话就留下一个三连吧&#x1f63c; 目录 一、主体不同 二、方式不同 三、规则不同 四、定义和基本概念 一、主体不同 1、路由策略&#xff1a;是为了改…