前言

先赞后看好习惯 打字不容易,这都是很用心做的,希望得到支持你 大家的点赞和支持对于我来说是一种非常重要的动力 看完之后别忘记关注我哦!️️️

我们都知道,排序算法是编程高级语言里面极其重要的算法。 在此其中,比较重要的就是快速排序了。但是在很多人学习快速排序的过程中,其实并没有很好地理解快速排序。其实快速排序有三种思想

  • 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从后往前找比基准值小的元素,找到之后停止;如果beginend没有相遇:将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;
}

前后指针法

定义两个指针,prevcur,再定义key的值,cur指针从left开始,遇到比key大的就过滤掉,比key小的,就停下来,prev++,判断prevcur是否相等,如果不相等,就将两个值进行交换最后一趟排序下来,比key小的,都留在了key的左边,比key大的都在key的右边。
动图中i代表curs代表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快速排序的三种思想和非递归,基准值选取优化【快速排序的深度剖析-超级详细的注释和解释】你真的完全学会快速排序了吗?的更多相关文章

  1. 基于ftp服务的三种登录方式及其相关的访问控制和优化

    ftp(简单文件传输协议),是一种应用广泛的网络文件传输协议和服务,占用20和21号端口,主要用于资源的上传和下载. 在linux对于ftp同widows一样具有很多的种类,这里主要介绍vsfptd( ...

  2. 基于Java的二叉树的三种遍历方式的递归与非递归实现

    二叉树的遍历方式包括前序遍历.中序遍历和后序遍历,其实现方式包括递归实现和非递归实现. 前序遍历:根节点 | 左子树 | 右子树 中序遍历:左子树 | 根节点 | 右子树 后序遍历:左子树 | 右子树 ...

  3. 8皇后以及N皇后算法探究,回溯算法的JAVA实现,非递归,循环控制及其优化

    上两篇博客 8皇后以及N皇后算法探究,回溯算法的JAVA实现,递归方案 8皇后以及N皇后算法探究,回溯算法的JAVA实现,非递归,数据结构“栈”实现 研究了递归方法实现回溯,解决N皇后问题,下面我们来 ...

  4. 基于visual Studio2013解决面试题之0401非递归遍历二叉树

     题目

  5. 蓝桥杯 2014本科C++ B组 李白打酒 三种实现方法 枚举/递归

    标题:李白打酒 话说大诗人李白,一生好饮.幸好他从不开车. 一天,他提着酒壶,从家里出来,酒壶中有酒2斗.他边走边唱: 无事街上走,提壶去打酒. 逢店加一倍,遇花喝一斗. 这一路上,他一共遇到店5次, ...

  6. python 三种遍历列表里面序号和值的方法

    list = ['html', 'js', 'css', 'python'] # 方法1 # 遍历列表方法1:' for i in list: print("序号:%s 值:%s" ...

  7. 用Python计算幂的两种方法,非递归和递归法

    用Python计算幂的两种方法: #coding:utf-8 #计算幂的两种方法.py #1.常规方法利用函数 #不使用递归计算幂的方法 """ def power(x, ...

  8. 基于visual Studio2013解决C语言竞赛题之1036递归求值

          题目 解决代码及点评 /* 36.已知有如下递推公式 求该数列的前n项.不允许使用数组. */ float fp50036(int n,float x,float ...

  9. JVM三种垃圾收集算法思想及发展过程

    JVM垃圾收集算法的具体实现有很多种,本文只是介绍实现这些垃圾收集算法的三种思想和发展过程.所有的垃圾收集算法的具体实现都是遵循这三种算法思想而实现的. 1.标记-清除算法 标记-清除(Mark-Sw ...

  10. [排序算法] 快速排序 (C++) (含三种写法)

    快速排序解释 快速排序 Quick Sort 与归并排序一样,也是典型的分治法的应用. (如果有对 归并排序还不了解的童鞋,可以看看这里哟~ 归并排序) 快速排序的分治模式 1.选取基准值,获取划分位 ...

随机推荐

  1. Android NativeCrash 捕获与解析

    Android 开发中,NE一直是不可忽略却又异常难解的一个问题,原因是这里面涉及到了跨端开发和分析,需要同时熟悉 Java,C&C++,并且需要熟悉 NDK开发,并且解决起来不像 Java异 ...

  2. 如何在iOS手机上查看应用日志

    ​ 引言 在开发iOS应用过程中,查看应用日志是非常重要的一项工作.通过查看日志,我们可以了解应用程序运行时的状态和错误信息,帮助我们进行调试和排查问题.本文将介绍两种方法来查看iOS手机上的应用日志 ...

  3. vscode报错Already included file name ‘xxx‘ differs from file name ‘xxx‘ only in casing的解决方法:

    场景:我们创建了一个文件是小写开头的,又改成大写开头的. 比如: relationDemo.vue 改成 RelationDemo.vue 原因:缓存的判重逻辑是不区分大小写导致的.在这种情况下,vs ...

  4. SpringBoot RabbitMQ 实战

    RabbitMQ RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件).RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台 ...

  5. freeswitch查看所有通道变量

    概述 freeswitch 是一款好用的开源软交换平台. 实际应用中,我们经常需要对fs中的通道变量操作,包括设置和获取,set & get. 但是,fs中有众多的内部定义通道变量,也有外部传 ...

  6. java基础-IO流-day13

    目录 1. IO的概念 2. 一个一个字符 完成文件的复制 3. 字节流 4. 转换字节流 5. System.in 7.基本数据类型的数据 8. object的处理 1. IO的概念 计算机内存中的 ...

  7. docker 安装 nacos

    转载请注明出处: https://www.jianshu.com/p/54f6da71ac48

  8. HanLP — 感知机(Perceptron)

    感知机(Perceptron)是一个二类分类的线性分类模型,属于监督式学习算法.最终目的: 将不同的样本分本 感知机饮食了多个权重参数,输入的特征向量先是和对应的权重相乘,再加得到的积相加,然后将加权 ...

  9. Linux-用户组-groupad-groupdel-usermod

  10. MySQL shell 备份数据库

    MySQL shell 备份数据库 背景 之前使用 mysqldump 和 mysql source 的方式备份数据库非常缓慢 有时候要耗费非常长的时间 今天发现有一个可以快速备份数据库的 mysql ...