【算法】基于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);
}
尾声
看到这里,我相信伙伴们对快速排序已经有了比较深入的了解了,在这快速里面,其实渗透了很多人的智慧结晶,我们掌握这些排序知识一部分,更重要的还是掌握里面精妙的算法思路。
如果这篇博客对你有帮助,一定不要忘了点赞关注和收藏哦!