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

目录

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.rhkb.cn/news/425752.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【数据结构】图的概念和存储结构

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《C游记》《进击的C》《Linux迷航》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 引言一、图的概念二、图的存储结构2.1 邻接矩阵2.1.1 成员变量与默认成员函数2.1.2 GetIndex2.1.3 AddEdge2.1.4 Pr…

【设计模式-外观】

这里写自定义目录标题 定义UML图角色作用代码使用场景 定义 为子系统中一组相关接口提供一致界面&#xff0c;定义一个高级接口&#xff0c;使得子系统更加容易使用。 UML图 角色作用 外观&#xff08;Facade&#xff09;角色&#xff1a;这是外观模式的核心&#xff0c;它知…

pdf去水印怎么去掉免费?6个pdf去除水印的方法快码住,超级好用!

pdf去水印怎么去掉免费&#xff1f;您是否有一些带有水印的pdf文档&#xff0c;让您感觉到头疼&#xff1f;您又是否希望能够去除这些水印&#xff0c;或者想用其他水印来替换现有的水印&#xff1f;如果是这样的话&#xff0c;我非常推荐您继续阅读本篇文章。本文将为您提供一…

openeuler 22.03 lts sp4 使用 kubeadm 部署 k8s-v1.28.2 高可用集群

文章目录 [toc]废话篇这篇文章什么时候写的为什么是 openeuler为什么是 22.03 lts sp4高可用架构题外话 干活篇环境介绍系统初始化相关关闭防火墙关闭 selinux关闭 swap开启内核模块开启模块自动加载服务 sysctl 内核参数调整清空 iptables 规则安装各种依赖和工具修改 .bashrc…

Qt --- 信号和信号槽

前言 Linux信号Signal&#xff0c;系统内部的通知机制&#xff0c;进程间通信方式。 信号源&#xff1a;谁发的信号。 信号的类型&#xff1a;哪种类别的信号。 信号的处理方式&#xff1a;注册信号处理函数&#xff0c;在信号被触发的时候自动调用执行。 Qt中的信号和Lin…

JUC学习笔记(三)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 八、共享模型之工具--JUC8.1 AQS 原理1. 概述2 实现不可重入锁自定义同步器自定义锁 3.心得起源目标设计1) state 设计2&#xff09;阻塞恢复设计3&#xff09;队列…

计算机毕业设计选题推荐-共享图书管理系统-小程序/App

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

【Linux】查看操作系统开机时初始化的驱动模块列表的一个方法

这个方法是摸索出来的&#xff0c;也不一定对&#xff1a; 1、驱动层module_init(module_init_function)作为模块初始化&#xff0c;并且提供模块内部初始化的函数名&#xff1b; 2、找到所有驱动目录drivers下所有module_init(module_init_function)&#xff0c;在内核6.9.0…

你的绩效是不是常年都是B

原创不易&#xff0c;求赞&#xff0c;求关注&#xff0c;&#x1f64f;&#x1f64f;&#x1f64f;&#x1f64f;&#x1f64f;&#x1f64f;&#x1f64f;&#x1f64f; 目录 原创不易&#xff0c;求赞&#xff0c;求关注&#xff0c;&#x1f64f;&#x1f64f;&#x1f64…

http连接github远程仓库密码问题解决办法

目录 一、问题&#xff1a;使用http连接失败 二、解决办法&#xff1a;使用个人访问令牌。 1、生成访问令牌&#xff1a; 步骤 1: 登录 GitHub 步骤 2: 进入设置页面 步骤 3: 生成新的访问令牌 步骤 4: 配置访问令牌 步骤 5: 复制令牌 2. 使用访问令牌 一、问题&#…

图像滤波---各项异性扩散滤波使用笔记及代码

图像滤波---各项异性扩散滤波使用笔记及代码 一、文章内容介绍二、各项异性扩散滤波和各项同性滤波1、各项同性滤波2、各项异性扩散滤波3、各项异性和各项同性的对比 三、各项异性扩散滤波的原理介绍四、各项异性扩散滤波公式五、公式中的参数使用说明1、扩散速率 λ \lambda λ…

kubernetes中pause容器的作用与源码详解

概述 摘要&#xff1a;上一篇文章我们介绍了kubernetes是如何通过pause容器来构建一个pod。本文我们对pause容器做一个总结&#xff0c;并再此基础上次深入浅出&#xff0c;从pause容器的源码详细了解pause容器的实现原理。 正文 pause容器是什么 在 Kubernetes 中&#xff…

STM32(十五):I2C通信

I2C通信 I2C&#xff08;Inter IC Bus&#xff09;是由Philips公司开发的一种通用数据总线&#xff0c;实现单片机读写外部模块寄存器的功能。 两根通信线&#xff1a;SCL&#xff08;Serial Clock&#xff09;、SDA&#xff08;Serial Data&#xff09; 同步&#xff0c;半双工…

css百分比布局中height:100%不起作用

百分比布局时&#xff0c;我们有时候会遇到给高度 height 设置百分比后无效的情况&#xff0c;而宽度设置百分比却是正常的。 当为一个元素的高度设定为百分比高度时&#xff0c;是相对于父元素的高度来计算的。当没有给父元素设置高度&#xff08;height&#xff09;时或设置…

Celery的使用

Celery 一、Celery概述1. 特点:2. celery组成3. 安装与使用4. 邮箱配置二、Celery的使用实操——发送邮件1. 安装2. 配置一、Celery概述 1. 特点: 2. celery组成 配置任务队列Broker,采用redis保存要执行的任务队列 Client:任务的发出者 Worker:任务的处理者 3. 安装与使用…

『功能项目』第三职业弓弩的平A【58】

我们打开上一篇57第二职业法师的平A的项目&#xff0c; 本章要做的事情是实现第三职业弓弩的平A伤害 首先修改脚本&#xff1a;MagicBall.cs 将脚本挂载在Sphere预制体身上 注意组件设置 运行项目 本章做了第三职业弓弩的平A伤害及显示伤害UI 接下来文章的内容&#xff1a; …

如何升级用 Helm 安装的极狐GitLab Runner?

本分分享如何对 Helm 安装的 Runner 进行升级。整个过程分为三步&#xff1a;1、确定 Runner 最新版本或者想要升级的版本是否存在&#xff1b;2、用 Helm upgrade 命令进行升级&#xff1b;3、升级确认。 极狐GitLab 为 GitLab 的中国发行版&#xff0c;中文版本对中国用户更…

Mac笔记本上查看/user/目录下的文件的几种方法

在Mac笔记本上查看/user/下的文件&#xff0c;可以通过多种方法实现。以下是一些常见的方法&#xff1a; 一、使用Finder 打开Finder&#xff1a;点击Dock栏中的Finder图标&#xff0c;或者使用快捷键Command F。 导航到用户目录&#xff1a; 在Finder的菜单栏中&#xff0…

编译运行 webAssembly(wasm)

环境准备&#xff1a; lunix下docker 参考https://hub.docker.com/r/emscripten/emsdk 拉编译环境 docker pull emscripten/emsdk 编译 随便找个目录&#xff0c;敲下面命令&#xff0c;编译一个webAssembly 程序 # create helloworld.cpp cat << EOF > hellowo…

【nginx】搭配okhttp 配置反向代理

nginx的默认是一个反向代理。 nginx会默认把输入的请求,转向其他的服务器执行。 这些转向的服务器与客户端发起的服务器不是同一个。 客户端只认识nginx,不知道ngiix转向何方。 正向代理修改okhttp的proxy,实际上很多代理都是正向的。 反向代理修改请求路径到nginx。 感觉还…