【数据结构】快速排序(三种实现方式)

目录

一、基本思想

二、动图演示(hoare版)

三、思路分析(图文)

四、代码实现(hoare版)

五、易错提醒

六、相遇场景分析

6.1 ❥ 相遇位置一定比key要小的原因

6.2 ❥ 右边为key,左边先走

6.3 ❥ 一边为key,另一边先走的原因

七、时间复杂度分析

八、快排的优化

8.1 ❥ key值的选取

8.1.1 随机数选key

8.1.2 三数取中

8.2 ❥ 小区间优化

九、挖坑法

9.1 ❥ 动图演示

9.2 ❥ 思路详解

9.3 ❥ 代码实现

十、前后指针法

10.1 ❥ 动图演示

10.2 ❥ 思路详解

10.3 ❥ 代码实现


一、基本思想

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。

其基本思想为:

  1. 任取待排序元素序列中的某元素作为基准值,按照该排序将待排序集合分割成两个子序列
  2. 子序列中所有元素均小于基准值,子序列中的所有元素均大于基准值
  3. 然后分别对左右两部分重复上述操作,直到将无序序列排列成有序序列。

二、动图演示(hoare版)

三、思路分析(图文)

以下以升序为例:

简言之,就是先进行单趟的排序,单趟排完之后,key已经放在它合适的位置上,分割出了一个左区间和右区间,然后进行递归排序,当左右区间都有序时,那么就整体有序。

四、代码实现(hoare版)

void swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}//hoare版
void QuickSort(int* a, int left, int right) //参数为数组下标
{//递归结束条件 if (left >= right){return;}int keyi = left;int begin = left;int end = right;//单趟排序while (begin < end){while (begin < end && a[end] >= a[keyi]){end--;}while (begin < end && a[begin] <= a[keyi]){begin++;}swap(&a[begin], &a[end]);}swap(&a[begin], &a[keyi]);keyi = begin;	//将begin下标位置赋给keyi//分割出左右区间// [left, keyi-1] keyi [keyi+1, right]//整体排序 递归QuickSort(a, left, keyi - 1);QuickSort(a, keyi+1,right);}

五、易错提醒

我们看如下一段代码:

void QuickSort(int* a, int left, int right) 
{if (left >= right){return;}int keyi = left;int begin = left;int end = right;while (begin < end){while (a[end] >= a[keyi]){end--;}while (a[begin] <= a[keyi]){begin++;}swap(&a[begin], &a[end]);}swap(&a[begin], &a[keyi]);keyi = begin;QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}

上述代码是有问题存在的

通过调试可知,第二个while遇到相遇要停止,这里while少了相遇停止条件,否则可能会一直死循环下去

为何要创建begin和end?

通过上述思路分析易知,区间的每次分割,left都需要指向原始序列第一个元素的位置,right指向原始序列最后一个元素的位置,所以这里专门定义一个begin和end 而不是用left和right去++ --,就是为了便于分割区间。

六、相遇场景分析

6.1 ❥ 相遇位置一定比key要小的原因

我们发现,每次L与R相遇时与key进行交换时,L的值都小于key,这是为什么呢?

这里对他们相遇的场景进行分析:

相遇时无非两种场景,要么R遇见L,要么L遇见R

L遇R:

R先走,找小,停下来。

R停下条件是:遇见比key小的值,R停的位置一定比key小,L没有找到大的,遇见R停下

所以说:L遇R,它们相遇的位置就是R的位置

R遇L:

R先走,找小,没有找到比key小的,直接跟L相遇了。

L停留的位置是上一轮交换的位置

上一轮交换,把比key小的值,换到了L的位置

6.2 ❥ 右边为key,左边先走

我们发现,上面相遇场景都是左边做key,如果右边做key,让左边先走呢?

右边做key时:相遇位置一定比key要大

如下图所示:

结论:

  • 左边做key,右边先走,可以保证相遇位置一定比key小
  • 右边做key,左边先走,可以保证相遇位置一定比key大

6.3 ❥ 一边为key,另一边先走的原因

有人肯定会疑惑,为什么要一边做key,另一边先走,不可以做key的一边先走吗?

可以验证一下:

上图是让key在左边,且左边先走,在8相遇,然后与key==5进行交换

交换完后,5换到了数组下标为5的位置,并没有换到他所对应的正确位置,且左区间的8比5大。

我们知道,快排是当一趟排完之后,左区间都比key小,右区间都比key大,且key刚好在正确位置上,这样才可以继续分左右区间进行递归排序。

因此,不可以做key的一边先走

结论:一边做key,只能让另一边先走

七、时间复杂度分析

在比较理想的情况下,快排的递归结构接近完全二叉树,所以层数为logn层,每一层排序次数近似为n,(即单趟的时间复杂度为n)

故时间复杂度为:O(nlogn)

但是在有序场景下使用快排会性能会下降,时间复杂度为O(N^2)

如下图所示:

  • 当key在左边时,右边R找小就会找不到,然后一直往左走,直到在key处相遇,
  • 然后自己跟自己交换,结束一趟的排序。分割出左右区间。
  • 此处没有左区间,只存在右区间
  • 就这样依次类推......
  • 那么总共执行的次数就会是一个等差数列
  • 即:时间复杂度为O(N^2)
  • 它的效率就会大幅度降低。

八、快排的优化

  • 经过时间复杂度的分析,发现当前的快排算法还是存在一些缺陷的,那就是在有序场景下使用快排会性能会下降,此外,还有可能导致栈溢出。
  • 为什么在有序场景下会发生栈溢出?
  • 因为每走一层就是一个递归,这里递归的深度太深会有栈溢出的风险。
  • 所以快排在此还是有较为明显的缺陷的,面对这些缺陷,我们在此应怎么解决呢?
  • 我们知道,时间复杂度为O(nlogn)的前提是每次区间的划分都是二分,也就是每次选择交换的key,都是接近中间位置的值,哪怕不那么接近二分,但整体深度是logn就可以
  • 所以key值的选取非常关键,如果固定的选择最左边(下标为0)的值,就有可能选到最小的值,然后出现效率退化栈溢出的风险
  • 那如何选key才能避免有序的情况下效率退化呢?
  • 下面提供了两种选取key值的方式

8.1 ❥ key值的选取

8.1.1 随机数选key

  • 如果想要输出给定范围[a,b]内的随机数,需要使用rand()%(b-a+1)+a
  • 缺陷:可能刚刚好选到最大或者最小值

代码如下:

void rand_key(int* a, int left, int right)
{int randi = left + (rand() % (right - left + 1));swap(&a[left], &a[randi]);
}

8.1.2 三数取中

所谓三数取中,就是从最左边,最右边,最中间三个位置,选择中间的值(不大不小的值)作为key(赋值给key)

代码如下:

int GetMidi(int* a, int left, int right)
{int midi = (left + right) / 2;if (a[left] > a[right]){if (a[left] < a[midi]) // r<l<m{return left;}else if(a[midi]<a[right])	//m<r<l{return right;}else	//r<m<l{return midi;}}else{if (a[right]<a[midi])	//l<r<m{return right;}else if (a[midi]<a[left])	//m<l<r{return left;}else   //l<m<r{return midi;}}	
}

注意

这里是选出的中间值还应跟最左边的值进行交换,还应该让最左边的值作为key

8.2 ❥ 小区间优化

为何要有小区间优化:

当将一组待排序列进行快排,递归到只剩下5个值时,我们还要进行选key,分割左右区间等操作让5个值有序,此刻使用递归调用花费代价太大(最后一层递归调用就要占整体递归调用的50%),这就引入了小区间优化的方式。

小区间优化目的:

当待排区间长度小于等于某个阈值时,不再递归分割排序,减少递归调用的深度和对栈空间的使用,避免过度分割导致的效率下降,可以在处理小规模数据时获得更好的性能,从而提高整体排序算法的效率。

思路分析:

  1. 这里选择直接插入排序,首先希尔排序适合数据量较大时使用,这里不适合使用。
  2. 直接插入排序在同是O(N^2)的情况下,它的速度要更快
  3. 因为通常情况下,它是达不到O(N^2),只有在完全有序的情况下,才能达到O(N^2)
  4. 所以同级情况下,它要比其它排序更快一点,它的实践意义也在于此。
  5. 当然,引入小区间优化会使得效率低下,增加了算法的复杂度。

代码如下:

//直接插入算法
void InsertSort(int* a, int n)
{for (int i = 0; i < n - 1; i++){int end = i;int tmp = a[end + 1];while (end >= 0){if (tmp < a[end]) {a[end + 1] = a[end];end--;}else{break;}}a[end + 1] = tmp;}
}//交换算法
void swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}//三数取中
int GetMidi(int* a, int left, int right)
{int midi = (left + right) / 2;if (a[left] > a[right]){if (a[left] < a[midi]) {return left;}else if (a[midi] < a[right])	{return right;}else	{return midi;}}else{if (a[right] < a[midi])	{return right;}else if (a[midi] < a[left])	{return left;}else {return midi;}}
}//hoare版
void QuickSort(int* a, int left, int right) //参数为数组下标
{if (left >= right){return;}// 小区间优化,不再递归分割排序,减少递归的次数if ((right - left + 1) < 10){InsertSort(a + left, right - left + 1);}else{//三数取中int midi = GetMidi(a, left, right);swap(&a[left], &a[midi]);int keyi = left;int begin = left;int end = right;while (begin < end){while (begin < end && a[end] >= a[keyi]){end--;}while (begin < end && a[begin] <= a[keyi]){begin++;}swap(&a[begin], &a[end]);}swap(&a[begin], &a[keyi]);keyi = begin;QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);}
}


九、挖坑法

这里的挖坑法是以单趟排序的思路优化出的挖坑法。

该方法没有效率的提升(因为单趟排序效率无提升空间,至少都得遍历一遍)

但理解起来更简单,因为它们相遇的位置是坑,所以不用分析左边做key,右边先走的问题,也不用分析相遇位置比key小的原因

9.1 ❥ 动图演示

9.2 ❥ 思路详解

  1. 将序列的第一个元素作为基准值,存放在临时变量key中,此时的第一个坑位形成
  2. L指向第一个元素,R指向最后一个元素
  3. R开始向前移动,R--,找比key小的值,找到后,将R指向的值填入L的坑位,此时R形成一个坑位
  4. 然后L开始向后移动,L++,找比key大的值,找到后,将L指向的值填入R的坑位,此时L形成一个坑位
  5. R和L交错移动,形成新的坑位,直到R与L相遇
  6. 此时将key值填入L和R共同所指向的坑位
  7. 单趟排序完成
  8. 然后分割左右区间进行递归排序
  9. 最后排成一个有序序列

9.3 ❥ 代码实现

//挖坑法
void QuickSort1(int*a,int left,int right)
{//递归结束条件 if (left >= right){return;}int key = a[left];int begin = left;int end = right;//单趟排序while (begin < end){while (begin<end&&a[end] >= key){end--;}a[begin] = a[end];	//甩给右区间坑while (begin<end&&a[begin] <= key){begin++;}a[end] = a[begin];	//甩给左区间坑}a[begin] = key;	//将key填入相遇的坑//进行递归排序QuickSort1(a, left, begin - 1);QuickSort1(a, begin + 1, right);}


十、前后指针法

前后指针法只是单趟逻辑改变,整体递归思路并没有改变。

该方法没有效率的提升。

10.1 ❥ 动图演示

10.2 ❥ 思路详解

  • 将key指向序列的第一个元素,设为基准值
  • prev指向key的位置,cur指向prev的下一个位置
  • 对cur进行判断:

如果cur>=key,则cur++ 

如果cur<key,prev++,交换cur和prev所指向的值,然后cur++

  • 再对cur进行判断,直到cur++到序列的最后一个元素的下一个位置,交换prev与key的值
  • 此时单趟排序完成
  • 然后分割左右区间进行递归排序
  • 最后排成一个有序序列

10.3 ❥ 代码实现

void swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}//前后指针法
void QuickSort2(int* a, int left, int right)
{if (left >= right){return;}//单趟排序int keyi = left;int prev = left;int cur = left + 1;while (cur<=right){if (a[cur] < a[keyi]) //cur的值比keyi的值小{prev++;if (prev != cur)	//判断prev与cur是否指向同一位置{swap(&a[prev], &a[cur]);}}cur++;}swap(&a[prev], &a[keyi]);QuickSort2(a, left, prev - 1);QuickSort2(a, prev + 1, right);}

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

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

相关文章

redis详细教程(2.List教程)

List是一种可以存储多个有序字符串的数据类型&#xff0c;其中的元素按照顺序排列&#xff08;可以重复出现&#xff09;&#xff0c;可以通过数字索引来访问列表中的元素&#xff0c;索引可以从左到右或者从右到左。 Redis 列表可以通过两种方式实现&#xff1a;压缩列表&…

PVE 一键安装WIKI.js

Wiki.js 一个轻量的知识库管理工具 在PVE 的shell 下执行如下代码&#xff08;国内访问需自行调整&#xff09;&#xff0c;一键安装&#xff0c;默认使用了sqlLite 作为数据库&#xff1a; bash -c "$(wget -qLO - https://github.com/tteck/Proxmox/raw/main/ct/wikijs…

电脑维修指南

1.输入法切换 1.右键悬浮窗 2.选择全拼 2.换壁纸 壁纸给你准备好了 https://wwyz.lanzoul.com/b00g2g2vyd 密码:da72浏览器下载解压, 然后就有了 右键, 挑选 3.清理垃圾 浏览器输入这个地址 https://wwyz.lanzoul.com/ijMin2di41ih普通下载 找一个喜欢的地方 右键, 解压 …

[SWPUCTF 2022 新生赛]py1的write up

开启靶场&#xff0c;下载附件&#xff0c;解压后得到&#xff1a; 双击exe文件&#xff0c;出现弹窗&#xff1a; 问的是异或&#xff0c;写个python文件来计算结果&#xff1a; # 获取用户输入的两个整数 num1 int(input("Enter the first number: ")) num2 int…

排序算法(冒泡,插入),希尔排序(插入升级),希尔排序和插入排序时间比较!

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;排序算法 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 一.冒泡排序&#xff1a; 时间复杂度&#xff1a;O&#xff08;N^2&#xff09;。 &#x1f3c4;‍♂️思路…

【Nas】X-DOC:搞机之PVE部署All In One(黑群晖NAS 软路由OpenWrt Docker Win10远程桌面)

【Nas】X-DOC&#xff1a;搞机之PVE部署All In One&#xff08;黑群晖NAS & 软路由OpenWrt & Docker & Win10远程桌面&#xff09; 1、原硬件配置清单&#xff1a;2、改AIO后增加配置清单&#xff1a;3、虚拟化平台PVE&#xff1a;4、搭建的关键服务&#xff1a; 1…

Web高级开发实验:EL基本运算符与数据访问

一、实验目的 掌握EL的定义&#xff0c;即Expression Language&#xff0c;用于提高编程效率。学习和掌握在开发环境中创建Java文件&#xff0c;并在jsp文件中使用EL表达式去调用其中的方法与属性等。 二、实验所用方法 上机实操 三、实验步骤及截图 1、创建javaweb项目&a…

Springboot项目中常用注解

文章目录 Springboot相关注解EnableAspectJAutoProxy(exposeProxy true)内部实现机制 EnableTransactionManagementServletComponentScanMapperScan(basePackages {"com.xxx.mapper"})ComponentScan(basePackages{"*"})lombok Data注解Controller中的相关…

jvm虚拟机介绍

Java虚拟机&#xff08;JVM&#xff09;是Java语言的运行环境&#xff0c;它基于栈式架构&#xff0c;通过加载、验证、准备、解析、初始化等类加载过程&#xff0c;将Java类文件转换成平台无关的字节码&#xff0c;并在运行时动态地将其翻译成特定平台的机器码执行。 JVM的核心…

基于SSM农业信息管理系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;农业技术管理&#xff0c;种植户管理&#xff0c;农产品类型管理&#xff0c;农资订单管理&#xff0c;系统管理 种植户账号功能包括&#xff1a;系统首页&#xff0c;个人中心&a…

01C++书写hello world、注释、变量、常量

#include <iostream> using namespace std; int main()//main为一个程序的入口&#xff0c;每个程序都必须仅有一个 { cout<<"hello world"<<endl; } //#输出结果为 //单行注释的符号 /* 多行注释的符号 */ //变量创建的语法&#xff1a;数据类…

OpenAI GPT-o1实现方案记录与梳理

本篇文章用于记录从各处收集到的o1复现方案的推测以及介绍 目录 Journey Learning - 上海交通大学NYUMBZUAIGAIRCore IdeaKey QuestionsKey TechnologiesTrainingInference A Tutorial on LLM Reasoning: Relevant methods behind ChatGPT o1 - UCL汪军教授Core Idea先导自回归…

shodan2---清风

注&#xff1a;本文章源于泷羽SEC&#xff0c;如有侵权请联系我&#xff0c;违规必删 学习请认准泷羽SEC学习视频:https://space.bilibili.com/350329294 实验一&#xff1a;search 存在CVE-2019-0708的网络设备 CVE - 2019 - 0708**漏洞&#xff1a;** 该漏洞存在于远程桌面…

offset Explorer连接云服务上的kafka连接不上

以上配置后报连接错误时&#xff0c;可能是因为kafka的server.properties配置文件没配置好&#xff1a; 加上面两条配置&#xff0c;再次测试连接&#xff0c;成功 listeners和advertised.listeners

C++的相关习题(2)

初阶模板 下面有关C中为什么用模板类的原因&#xff0c;描述错误的是? ( &#xff09; A.可用来创建动态增长和减小的数据结构 B.它是类型无关的&#xff0c;因此具有很高的可复用性 C.它运行时检查数据类型&#xff0c;保证了类型安全 D.它是平台无关的&#xff0c;可移植…

Vue.js 组件开发教程:从基础到进阶

Vue.js 组件开发教程:从基础到进阶 引言 在现代前端开发中,Vue.js 作为一款流行的 JavaScript 框架,以其简单易用和灵活性赢得了开发者的青睐。Vue 组件是 Vue.js 的核心概念之一,理解组件的开发和使用对构建复杂的用户界面至关重要。本篇文章将详细介绍 Vue.js 组件的开…

NFS练习

一、实验目的 1、开放/nfs/shared目录&#xff0c;供所有用户查询资料 2、开放/nfs/upload目录&#xff0c;为192.168.xxx.0/24网段主机可以上传目录&#xff0c; 并将所有用户及所属的组映射为nfs-upload,其UID和GID均为210 3、将/home/tom目录仅共享给192.168.xxx.xxx这台…

MySQL全文索引检索中文

MySQL全文索引检索中文 5.7.6版本不支持中文检索&#xff0c;需要手动修改配置 ft_min_word_len 1 &#xff0c;因为默认配置 4 SHOW VARIABLES LIKE ft%; show VARIABLES like ngram_token_size;配置 修改 MySQL 配置文件 vim /etc/my.cnf在配置的 [mysqld] 下面添加**ft_…

C++——string的模拟实现(上)

目录 引言 成员变量 1.基本框架 成员函数 1.构造函数和析构函数 2.拷贝构造函数 3.容量操作函数 3.1 有效长度和容量大小 3.2 容量操作 3.3 访问操作 (1)operator[]函数 (2)iterator迭代器 3.4 修改操作 (1)push_back()和append() (2)operator函数 引言 在 C—…

【C++单调栈 贡献法】907. 子数组的最小值之和|1975

本文涉及的基础知识点 C单调栈 LeetCode907. 子数组的最小值之和 给定一个整数数组 arr&#xff0c;找到 min(b) 的总和&#xff0c;其中 b 的范围为 arr 的每个&#xff08;连续&#xff09;子数组。 由于答案可能很大&#xff0c;因此 返回答案模 109 7 。 示例 1&#x…