【算法】基于hoare快速排序的三种思想和非递归,基准值选取优化【快速排序的深度剖析-超级详细的注释和解释】你真的完全学会快速排序了吗?

前言
先赞后看好习惯 打字不容易,这都是很用心做的,希望得到支持你 大家的点赞和支持对于我来说是一种非常重要的动力 看完之后别忘记关注我哦!️️️
我们都知道,排序算法是编程高级语言里面极其重要的算法。 在此其中,比较重要的就是快速排序了。但是在很多人学习快速排序的过程中,其实并没有很好地理解快速排序。其实快速排序有三种思想。
- hoare版本
- 挖坑法
- 前后指针法
对于C语言中的qsort()函数,博主在早期的博客中做过详细的介绍,暂时还不会用的伙伴可以通过传送门食用哦
对于九大排序还不太清楚的朋友们,这里 博主提供汇总的传送门给大家。
另外还有一篇博客,是排序算法的复杂度测试源代码,博主也在这里提供给大家,等下再下面学习过程中,我们也要用到。
那么这里博主先安利一下一些干货满满的专栏啦!
数据结构专栏:数据结构 这里包含了博主很多的数据结构学习上的总结,每一篇都是超级用心编写的,有兴趣的伙伴们都支持一下吧!
算法专栏:算法 这里可以说是博主的刷题历程,里面总结了一些经典的力扣上的题目,和算法实现的总结,对考试和竞赛都是很有帮助的!
力扣刷题专栏:Leetcode 想要冲击ACM、蓝桥杯或者大学生程序设计竞赛的伙伴,这里面都是博主的刷题记录,希望对你们有帮助!
C的深度解剖专栏:C语言的深度解剖 想要深度学习C语言里面所蕴含的各种智慧,各种功能的底层实现的初学者们,相信这个专栏对你们会有帮助的!
为了方便伙伴们测试,这里博主提供一份测试用的main()函数里的源代码,各位测试的时候可以直接复制来进行测试。
本篇使用了一些C++包装的函数,例如swap,max等。
所以读者自己记得加上所需IO,算法等头文件
本期博客使用的编程语言是C++,但是,为了让目前只会C语言的伙伴也可以很好地掌握里面的算法核心,本篇博客没有使用C++包装的vector,全部使用了数组来进行演示。
本篇博客的代码实现的都是int类型数据的升序,想要实现降序或者其它数据类型排序的伙伴可以在此基础上做修改,我们本篇的目标是理解算法思想即可。
注:本篇博客的动画全部来自互联网
什么是快速排序
快速排序实质上的实现:
- 每次确定一个
key值(在优化之前一般选区间最左边的值) - 把
key这个值排好,举个例子,原来的数组第一个数是6,数组是[1,2,3,4,5,6,7,8,9,10]打乱的顺序,那么如果6当key,排完单趟之后,6应该回到下标为5的位置。 - 然后递归对由
keyi(keyi是key的下标)划分的子区间数组再重复上面的步骤。也就是说,对[begin,keyi-1]和[keyi+1,end]这两个区间,排好它们的key。其实这个就是一个分治的过程,类似于二叉树的遍历。 - 非递归的方式其实就是用数据结构栈模拟递归过程,单趟的排序是一样的。
单趟排序有三种实现思想
1.hoare思想
2.挖坑法
3.前后指针法
快速排序的递归实现
void _QuickSort(int* a, int begin, int end) {
//区间不存在或者只会有一个值不需要再处理
//快排:每次把key弄好,递归解决key两边的数
if (begin >= end)return;
//利用单趟排序找到key并排好,然后得到key的下标keyi
int keyi = PartSort(a, begin, end);
//PartSort即为单趟排序
_QuickSort(a, begin, keyi - 1);
_QuickSort(a, keyi + 1, end);
}
void QuickSort(int* a, int n) {
int begin = 0;
int end = n - 1;
_QuickSort(a, begin, end);
}
tips:关于PartSort单趟排序的实现,我放在后面详细讲那三种实现思想。
快速排序的非递归实现
其实说白了就是用数据结构栈来模拟递归的过程,这里和二叉树dfs遍历的非递归方法其实是一样的。
#include<stack>
void QuickSortNonR(int* a, int begin, int end) {
//1.直接改循环
//2.用数据结构栈模拟递归过程
stack<int>st;
st.push(end);
st.push(begin);
//栈里面没有区间了,就结束了
while (!st.empty()) {
int left = st.top();
st.pop();
int right = st.top();
st.pop();
//利用单趟排序找到key并排好,然后得到key的下标keyi
int keyi = PartSort(a, left, right);
//[left,keyi-1]keyi[keyi+1,right]
//怎么迭代
//先入右再入左
//栈里面的区间都会拿出来,单趟排序分割,子区间再入栈
if (left < keyi - 1) {
st.push(keyi - 1);
st.push(left);
}
if (keyi + 1 < right) {
st.push(right);//同样,先入右再入左
st.push(keyi + 1);
}
}
}
//
void QuickSort(int* a, int n) {
int begin = 0;
int end = n - 1;
QuickSortNonR(a, begin, end);
}
单趟排序详解
hoare思想
- 让
begin从前往后找比基准值大的元素,找到之后停止;让end从后往前找比基准值小的元素,找到之后停止;如果begin和end没有相遇:将begin位置上的元素和end位置上的元素进行交换;循环结束后将基准值和begin位置上的元素进行交换。

//hoare版本 O(nlogn)
//1.选出一个key,一般是最左边或者最右边的值
//2.单趟排完之后:要求左边比key小,右边比key大
//左边key,右边先走
//右边key,左边先走
//每次排完一趟,key的位置的值就是准确的了,不用动了
int PartSort1(int* a, int begin, int end) {//hoare
int left = begin;
int right = end;
int keyi = left;//保存下标,也就是指针是最优的
//下面是单趟
while (left < right) {//相遇就停下来
//右边先走,找小
while (left < right && a[right] >= a[keyi])--right;//while一定要带等号,否则会死循环
//而且要带多一个调节,否则有可能会越界
//左边再走,找大
while (left < right && a[left] <= a[keyi])++left;
//交换
swap(a[left], a[right]);
}
//交换key和相遇位置换
swap(a[keyi], a[right]);
//为什么左边做key,要让右边先走?
//因为要保证相遇的位置的值比key小
keyi = left;
//[begin,keyi-1]和[keyi+1,end]有序即可
return keyi;
}
挖坑法
挖坑法其实只是对hoare版本的一个改写而已,其实并没有得到真正意义上的改变。
以下是挖坑法的思路:

//挖坑法
//和hoare相同的地方就是-排完单趟-做到key左边比key小,右边比key大
int PartSort2(int* a, int begin, int end) {
int key = a[begin];
int piti = begin;//begin是第一个坑
int left = begin;
int right = end;
while (left < right) {
//右边找小,填到左边的坑
while (left < right && a[right] >= key) {
right--;
}
a[piti] = a[right];
piti = right;//自己变成坑
while (left < right && a[left] <= key){
left++;
}
a[piti] = a[left];
piti = left;
}
//一定相遇在坑的位置
a[piti] = key;
return piti;
}
前后指针法
定义两个指针,prev和cur,再定义key的值,cur指针从left开始,遇到比key大的就过滤掉,比key小的,就停下来,prev++,判断prev和cur是否相等,如果不相等,就将两个值进行交换,最后一趟排序下来,比key小的,都留在了key的左边,比key大的都在key的右边。
动图中i代表cur,s代表prev

//前后指针版
int PartSort3(int* a, int begin, int end) {
//排完之后prev之前的比prev小,prev后的比prev大
int prev = begin;
int cur = begin + 1;//一开始cur和prev要错开
int keyi = begin;
while (cur <= end) {
//如果cur的值小于keyi的值
if (a[cur] < a[keyi] && ++prev != cur) {//只有这种情况要处理一下
swap(a[prev], a[cur]);
}
++cur;
}
//
swap(a[prev], a[keyi]);
//此时prev的位置就是keyi的位置了
keyi = prev;
return keyi;
}
快速排序的优化
以上三种找key的方法,如果在数组是有序的时候,效率就会很低:
为什么,因为效率变成
n+n-1+n-2+n-3.....
因此为:O(n^2)
什么时候快排的效率是最高的?–每次的key都是区间的中位数的时候就是最快的,这样就是一个严格的二分分治,效率为O(NlogN)
而且因为是使用递归,所以如果数字稍微大一些,就会出现栈溢出的现象。
我们可以用TestOP来测试(效率测试见文章开头传送门)
如果用快排来排有序的时候,在debug版本小就可能会爆
而且效率会变得非常低。


那么我们怎么去优化呢?
三种优化方案:
- 随机选一个数作为
key值 - 三数取中
- 小区间优化
第一种优化方案很好理解,这里重点介绍下面两种方式进行优化。
三数取中
三数取中的意思就是–选取区间中第一个数,中间那个数,最后那个数中不大不小的那个作为该区间的key值。
//优化-三数取中
int GetMidIndex(int* a, int begin, int end) {
int mid = (begin + end) / 2;
if (a[begin] < a[mid]) {
if (a[mid] < a[end]) {
return mid;
}
else if (a[begin] < a[end]) {
return end;
}
else {
return begin;
}
}
else {//a[begin]>a[mid]
if (a[mid] > a[end]) {
return mid;
}
else if (a[begin] < a[end]) {
return begin;
}
else {
return end;
}
}
}
小区间优化
当区间比较小的时候,就不再递归划分去排序这个小区间。可以考虑其他排序。
这里建议直接用插入排序。
这里只需要对刚才的_QuickSort()函数里做个调整即可。
void _QuickSort(int* a, int begin, int end) {
//记录递归次数
callCount++;
//区间不存在或者只会有一个值不需要再处理
//快排:每次把key弄好,递归解决key两边的数
if (begin >= end)return;
//小区间优化
if (end - begin > 10) {//当区间大于10的时候,继续递归
int keyi = PartSort3(a, begin, end);//每一个partsort负责找到key
_QuickSort(a, begin, keyi - 1);
_QuickSort(a, keyi + 1, end);
}
else {//当区间较小的时候,直接使用插入排序
InsertSort(a + begin, end - begin + 1);//注意+1和a+begin
}
}
快速排序整体代码
在完整代码中,只对PartSort3()使用了三数取中,其它单趟排一个道理,也可以直接用,把key换掉就行了,很简单。
//优化-三数取中
int GetMidIndex(int* a, int begin, int end) {
int mid = (begin + end) / 2;
if (a[begin] < a[mid]) {
if (a[mid] < a[end]) {
return mid;
}
else if (a[begin] < a[end]) {
return end;
}
else {
return begin;
}
}
else {//a[begin]>a[mid]
if (a[mid] > a[end]) {
return mid;
}
else if (a[begin] < a[end]) {
return begin;
}
else {
return end;
}
}
}
//hoare版本 O(nlogn)
int PartSort1(int* a, int begin, int end) {//hoare
int left = begin;
int right = end;
int keyi = left;//保存下标,也就是指针是最优的
//下面是单趟
while (left < right) {//相遇就停下来
//右边先走,找小
while (left < right && a[right] >= a[keyi])--right;//while一定要带等号,否则会死循环
//而且要带多一个调节,否则有可能会越界
//左边再走,找大
while (left < right && a[left] <= a[keyi])++left;
//交换
swap(a[left], a[right]);
}
//交换key和相遇位置换
swap(a[keyi], a[right]);
keyi = left;
//[begin,keyi-1]和[keyi+1,end]有序即可
return keyi;
}
//挖坑法
int PartSort2(int* a, int begin, int end) {
int key = a[begin];
int piti = begin;//begin是第一个坑
int left = begin;
int right = end;
while (left < right) {
//右边找小,填到左边的坑
while (left < right && a[right] >= key) {
right--;
}
a[piti] = a[right];
piti = right;//自己变成坑
while (left < right && a[left] <= key){
left++;
}
a[piti] = a[left];
piti = left;
}
//一定相遇在坑的位置
a[piti] = key;
return piti;
}
//前后指针版
int PartSort3(int* a, int begin, int end) {
//排完之后prev之前的比prev小,prev后的比prev大
int prev = begin;
int cur = begin + 1;//一开始cur和prev要错开
int keyi = begin;
//加入三数取中的优化
int midi = GetMidIndex(a, begin, end);
swap(a[keyi], a[midi]);//这样换一下,key就变成GetMidIndex()函数的取值了-后面的代码都不变了
while (cur <= end) {
//如果cur的值小于keyi的值
if (a[cur] < a[keyi] && ++prev != cur) {//只有这种情况要处理一下
swap(a[prev], a[cur]);
}
++cur;
}
swap(a[prev], a[keyi]);
keyi = prev;
return keyi;
}
void _QuickSort(int* a, int begin, int end) {
//区间不存在或者只会有一个值不需要再处理
//快排:每次把key弄好,递归解决key两边的数
if (begin >= end)return;
//小区间优化
if (end - begin > 10) {
int keyi = PartSort3(a, begin, end);//每一个partsort负责找到key
_QuickSort(a, begin, keyi - 1);
_QuickSort(a, keyi + 1, end);
}
else {
InsertSort(a + begin, end - begin + 1);//注意+1和a+begin
}
}
void QuickSortNonR(int* a, int begin, int end) {
//1.直接改循环
//2.用数据结构栈模拟递归过程
stack<int>st;
st.push(end);
st.push(begin);
//栈里面没有区间了,就结束了
while (!st.empty()) {
int left = st.top();
st.pop();
int right = st.top();
st.pop();
int keyi = PartSort3(a, left, right);
//[left,keyi-1]keyi[keyi+1,right]
//怎么迭代
//先入右再入左
//栈里面的区间都会拿出来,单趟排序分割,子区间再入栈
if (left < keyi - 1) {
st.push(keyi - 1);
st.push(left);
}
if (keyi + 1 < right) {
st.push(right);//同样,先入右再入左
st.push(keyi + 1);
}
}
}
void QuickSort(int* a, int n) {
int begin = 0;
int end = n - 1;
_QuickSort(a, begin, end);//这里也可以改成用非递归那个函数
//QuickSortNonR(a, begin, end);
}
尾声
看到这里,我相信伙伴们对快速排序已经有了比较深入的了解了,在这快速里面,其实渗透了很多人的智慧结晶,我们掌握这些排序知识一部分,更重要的还是掌握里面精妙的算法思路。
如果这篇博客对你有帮助,一定不要忘了点赞关注和收藏哦!
【算法】基于hoare快速排序的三种思想和非递归,基准值选取优化【快速排序的深度剖析-超级详细的注释和解释】你真的完全学会快速排序了吗?的更多相关文章
- 基于ftp服务的三种登录方式及其相关的访问控制和优化
ftp(简单文件传输协议),是一种应用广泛的网络文件传输协议和服务,占用20和21号端口,主要用于资源的上传和下载. 在linux对于ftp同widows一样具有很多的种类,这里主要介绍vsfptd( ...
- 基于Java的二叉树的三种遍历方式的递归与非递归实现
二叉树的遍历方式包括前序遍历.中序遍历和后序遍历,其实现方式包括递归实现和非递归实现. 前序遍历:根节点 | 左子树 | 右子树 中序遍历:左子树 | 根节点 | 右子树 后序遍历:左子树 | 右子树 ...
- 8皇后以及N皇后算法探究,回溯算法的JAVA实现,非递归,循环控制及其优化
上两篇博客 8皇后以及N皇后算法探究,回溯算法的JAVA实现,递归方案 8皇后以及N皇后算法探究,回溯算法的JAVA实现,非递归,数据结构“栈”实现 研究了递归方法实现回溯,解决N皇后问题,下面我们来 ...
- 基于visual Studio2013解决面试题之0401非递归遍历二叉树
题目
- 蓝桥杯 2014本科C++ B组 李白打酒 三种实现方法 枚举/递归
标题:李白打酒 话说大诗人李白,一生好饮.幸好他从不开车. 一天,他提着酒壶,从家里出来,酒壶中有酒2斗.他边走边唱: 无事街上走,提壶去打酒. 逢店加一倍,遇花喝一斗. 这一路上,他一共遇到店5次, ...
- python 三种遍历列表里面序号和值的方法
list = ['html', 'js', 'css', 'python'] # 方法1 # 遍历列表方法1:' for i in list: print("序号:%s 值:%s" ...
- 用Python计算幂的两种方法,非递归和递归法
用Python计算幂的两种方法: #coding:utf-8 #计算幂的两种方法.py #1.常规方法利用函数 #不使用递归计算幂的方法 """ def power(x, ...
- 基于visual Studio2013解决C语言竞赛题之1036递归求值
题目 解决代码及点评 /* 36.已知有如下递推公式 求该数列的前n项.不允许使用数组. */ float fp50036(int n,float x,float ...
- JVM三种垃圾收集算法思想及发展过程
JVM垃圾收集算法的具体实现有很多种,本文只是介绍实现这些垃圾收集算法的三种思想和发展过程.所有的垃圾收集算法的具体实现都是遵循这三种算法思想而实现的. 1.标记-清除算法 标记-清除(Mark-Sw ...
- [排序算法] 快速排序 (C++) (含三种写法)
快速排序解释 快速排序 Quick Sort 与归并排序一样,也是典型的分治法的应用. (如果有对 归并排序还不了解的童鞋,可以看看这里哟~ 归并排序) 快速排序的分治模式 1.选取基准值,获取划分位 ...
随机推荐
- 【每日一题】1. tokitsukaze and Soldier (优先队列 + 排序)
题目链接:Here 思路:这道题很容易看出来是考察 优先队列(priority_queue) 和 sort . 对于容忍人数越高的人来说,团队人数低也更能做到: for i = 0 to n - 1: ...
- 3D编程模式:介绍设计原则
大家好~本文介绍6个设计原则的定义 系列文章详见: 3D编程模式:开篇 目录 单一职责原则(SRP) 依赖倒置原则(DIP) 接口隔离原则(ISP) 迪米特法则(LoD) 合成复用原则(CARP) 开 ...
- echarts网络拓扑图动态流程图
https://aixiaodou.blog.csdn.net/article/details/93712083?utm_medium=distribute.pc_relevant.none-task ...
- uni-app阿里图标引用
@font-face { font-family: "iconfont"; /* Project id 2566540 */ src: url('~@/static/fonts/i ...
- freeswitch APR库内存池
概述 freeswitch的核心源代码是基于apr库开发的,在不同的系统上有很好的移植性. apr库中的大部分API都需要依赖于内存池,使用内存池简化内存管理,提高内存分配效率,减少内存操作中出错的概 ...
- gradle简介与windows安装操作
本文为博主原创,转载请注明出处: 目录 1.Gradle 简介 2.gradel 与 maven 对比 3.安装 gradle 3.1.安装jdk 3.2.下载gradle 3.3.下载解压到指定目录 ...
- 【转】获取本地图片的URL
在写博客插入图片时,许多时候需要提供图片的url地址.作为菜鸡的我,自然是一脸懵逼.那么什么是所谓的url地址呢?又该如何获取图片的url地址呢? 首先来看一下度娘对url地址的解释:url是 ...
- Laravel - Could not open input file: artisan 的解决方法
cd 到 laravel的目录中执行 就可以了
- [转帖]从Linux源码看TIME_WAIT状态的持续时间
https://zhuanlan.zhihu.com/p/286537295 从Linux源码看TIME_WAIT状态的持续时间 前言 笔者一直以为在Linux下TIME_WAIT状态的Socket持 ...
- [转帖]Nginx 保留 Client 真实 IP
https://lqingcloud.cn/post/nginx-01/#:~:text=%E5%9C%A8%20Nginx%20%E4%B8%AD%E5%8F%AF%E4%BB%A5%E9%80%9 ...