写给前面:
💛生活的真谛从来都不在别处,就在日常一点一滴的奋斗里🧡
任取待排序元素序列中的某元素作为
基准值
,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值
,右子序列中所有元素均大于基准值
,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
听上去有些抽象?
实际上 这似乎和二叉树的前序遍历有些像~
先根,然后递归左,回来递归右
key
,一般我们找数组的最左或者最右边(以左边基准值为例)left
和right
right
向左找比key小的数left
向右去找比key大的数
注意:
right
向左找,一直找不到比key小的,说明key右边全部比key小left
向右找,如果相遇,即left
==right
,此时的值一定比key小,因为right
向左找到一个位置,这个位置一定是小于key right
才会停下来等待left
此时 相当于完成了局部的排序:左边都小于key 右边都大于key
此时 被分成了三部分[begin,keyi-1]
,keyi
,[keyi+1,begin]
然后就去以相同的步骤重复左子区间
和右子区间
为什么key为最左边的时候,从右向左找?
这里是hoare方法中比较别扭的地方
结论:因为要保证相遇的位置比key小,或者就是key的位置
分析
越界访问问题
解决方法:加一个条件left<right
同时这个条件也保证了:left
和right
不会错过,只要相等就会跳出
// 霍尔版本
void QuickSort(int* a, int begin,int end)
{
//递归的返回条件
if (begin >= end)
{
return;
}
int left = begin;
int right = end;
int keyi = left;
while (left < right)
{
//右边先走,找小
while (left < right && a[right] >= a[keyi])
{
--right;
}
//左边走,找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
//找到交换
Swap(&a[left], &a[right]);
}
//当left==right的时候,交换keyi和left的值
Swap(&a[keyi], &a[left]);
//更新keyi
keyi = left;
// [begin,keyi-1] keyi [keyi+1,end]
//分治思想, 把keyi左右的序列都变成有序 整体就是有序了!
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
上面的hoare方法中存在一个问题:
我们如果选取最左边为key,需要先从右边向左找小于key的值
同理,如果选取最右边为key,需要先从左边向右找大于key的值
坑位
坑
,哪边有坑
,另一边就去找值去填坑
)坑
中,然后把该位置变成新的坑
坑
,改位置变成新的坑
坑
)坑
的位置[begin,pit-1]
,[pit+1,end]
填坑法的出现就是解决了 这个有点别扭的问题
哪里有坑,就从另一侧去找值 来天坑,更自然一些
但是 挖坑法单躺得到的顺序可能和hoare版本得到的不同
// 填坑法
void QuickSort_pit(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
//拿出坑位元素
int pit = begin;
//假设初始坑位为最左侧
int key = a[pit];
int left = begin;
int right = end;
while (left < right)
{
//右边去找小
while (left < right && a[right] >= key)
{
--right;
}
//右边找到小了,填坑,
a[pit] = a[right];
//同时right位置为新的坑,可以被任意填
pit = right;
//左边去找大
while (left < right && a[left] <= key)
{
++left;
}
//左边找到了,填坑
a[pit] = a[left];
//更新坑位
pit = left;
}
//当left==right的时候
//把key填入坑位
a[pit] = key;
//然后去递归 左右子区间
//[begin,pit-1],pit,[pit+1,end]
QuickSort_pit(a, begin, pit - 1);
QuickSort_pit(a, pit + 1, end);
}
挖坑法其实效率上并没有带来提升
只是相对于hoare方法好理解了一点点
但是有一类题喜欢考
选择题:给你一个数组,经过一趟快速排序,下列可能的结果有(不定项选择)
这时候需要考虑到 hoare法和挖坑法了
定义两个下标
prev
和cur
还是假设 key为开头第一个元素
prev
指向序列开头,cur
指向prev
的下一个位置cur
去向后走去找小于key的数cur
指向的数据小于key,那么prev
先向后走一步,然后交换prev
和cur
指向的数据,然后cur
下标向后走(cur
++)cur
指向的数据大于等于key,不做处理cur
走完,就交换prev
和key的值[begin,prev-1]
,[prev+1,end]
经过一次排序,prev
前面的 都小于key,后面的都大于key
分析
为什么要这样做呢?
prev
和cur
是挨着的,cur
是一直向后走的,如果cur
指向的值大于key,那么cur
直接++,这样prev
和cur
之间就是比key大的数cur
遇到比key小的,就换到prev
的后面prev
一定是最后一个比key小的注意:如果cur
的下一个就是prev
就不需要交换
所以prev
的意思就是 大于key 的数据的前一个!
void QuickSort_prev(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = begin;
int prev = begin;//标记比key大的前一个位置
int cur = begin + 1;
while (cur <= end)
{
//如果cur小于key 并且cur与prev之间存在比key大的元素 那么prev的后一个与cur交换
if (a[cur] < a[keyi] && ++prev < cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
//最后交换prev与keyi位置 --keyi的位置变成了prev
Swap(&a[prev], &a[keyi]);
//此时 prev前面的都比key小 后面的都比key大
//递归,[begin,keyi-1],keyi,[keyi+1,end]
keyi = prev;
QuickSort_prev(a, begin, keyi - 1);
QuickSort_prev(a, keyi + 1, end);
}
思考一个问题
以上的三种快排的实现,每一趟都是O(N)
那么快排的效率与什么有关呢? –key
的选取
最好情况:每一次key都是中间的数,这样每一次都是二分,一共需要递归logN
层
时间复杂度为O(N*logN)
最坏情况:数组已经升序/降序或接近有序,这样每一次key都选的最大的 或者最小的,需递归N层,并且数据量较大的情况会发生栈溢出
那么key如何选取,才能提高效率?
分析:如果是有序的情况下,一定是两端是最大或者最小
所以尽量选中间附近的
我们选好key之后,还是把最左侧作为key(算法保持不变)
把选出的key与最左侧begin
位置交换就可以了!
//三数取中法
int GetMidIndex(int* a, int begin, int end)
{
int mid = begin + (end - begin) / 2;//找出中间坐标
if (a[begin] < a[mid])
{
if (a[mid] < a[end])//如果end最大
{
return mid;
}
else if (a[begin] < a[end])//来到这说明mid最大,看begin和end
{
return end;
}
else
{
return begin;
}
}
else//来到这说明begin>=mid
{
if (a[mid]>a[end])//说明begin最大
{
return mid;
}
else if (a[begin]>a[end])//来到这说明mid最小 看begin和end
{
return end;
}
else
{
return begin;
}
}
}
void QuickSort_prev(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = begin;
int prev = begin;//标记比key大的前一个位置
int cur = begin + 1;
//三数取中法优化
int midi = GetMidIndex(a, begin, end);//找到midi下标
Swap(&a[keyi], &a[midi]);//与key位置交换
while (cur <= end)
{
//如果cur小于key 并且cur与prev之间存在比key大的元素 那么prev的后一个与cur交换
if (a[cur] < a[keyi] && ++prev < cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
//最后交换prev与keyi位置 --keyi的位置变成了prev
Swap(&a[prev], &a[keyi]);
//此时 prev前面的都比key小 后面的都比key大
//递归,[begin,keyi-1],keyi,[keyi+1,end]
keyi = prev;
QuickSort_prev(a, begin, keyi - 1);
QuickSort_prev(a, keyi + 1, end);
}
由于快速排序是递归实现,当递归的小区间小到一定程度的时候,如果还是继续进行递归,反而影响效率
比如:如果区间长度为10,递归次数约为12
次
这时候,递归带来的消耗是大于直接对这些小区间进行排序的消耗的!所以当递归划分小区间的时候,区间比较小的时候,就不用递归去划分子区间了,可以考虑直接使用其他小排序对区间处理,一般使用简单排序中最优的----插入排序
希尔、堆排等在数据量较大的时候才会有明显优势
注意:由于当前编译器对递归的优化已经挺高了,所以优化的效果没有特别特别明显,但是有一些优化的。
所以
如果假设区间小于10的时候,不再递归小区间,插入排序的消耗小于递归的消耗,将极大的优化整个排序
void QuickSort_prev(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
if (end - begin > 10)
{
int keyi = begin;
int prev = begin;//标记比key大的前一个位置
int cur = begin + 1;
//三数取中法优化
int midi = GetMidIndex(a, begin, end);//找到midi下标
Swap(&a[keyi], &a[midi]);//与key位置交换
while (cur <= end)
{
//如果cur小于key 并且cur与prev之间存在比key大的元素 那么prev的后一个与cur交换
if (a[cur] < a[keyi] && ++prev < cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
//最后交换prev与keyi位置 --keyi的位置变成了prev
Swap(&a[prev], &a[keyi]);
//此时 prev前面的都比key小 后面的都比key大
//递归,[begin,keyi-1],keyi,[keyi+1,end]
keyi = prev;
QuickSort_prev(a, begin, keyi - 1);
QuickSort_prev(a, keyi + 1, end);
}
else//如果区间小于10 进行插入排序
{
// 注意 数组是a+begin,a+begin是每一个小区间数组的开头
// end-begin+1是小数组大小
InsertSort(a + begin, end-begin+1);
}
}
插入排序如果忘了看这:插入排序
看一下小区间优化减少的递归次数
没优化:
我们假设区间为<30的时候进行优化
优化了:
第一个是用时(ms)
第二个是调用次数
可以看到递归的调用次数 几乎减少了一个量级!!
递归的大问题:
极端场景下,递归层次太深,会出现栈溢出
因为栈很小
递归改成非递归的方法:
步骤:
begin
和end
入栈(end
先入,begin
后入)left
和right
接受[left,right]
进行一趟快排[left,keyi-1]
,keyi
,keyi+1,right]
left
和right
接收left
和right
区间[left,keyi-1]
,keyi
,keyi+1,right]
因为C中没有现成的写好的栈 所以还要自己写栈
如果忘了,戳这:数据结构-栈C实现
int PartSort3(int* a, int begin, int end)
{
int keyi = begin;
int prev = begin;//标记比key大的前一个位置
int cur = begin + 1;
while (cur <= end)
{
//如果cur小于key 并且cur与prev之间存在比key大的元素 那么prev的后一个与cur交换
if (a[cur] < a[keyi] && ++prev < cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
//最后交换prev与keyi位置 --keyi的位置变成了prev
Swap(&a[prev], &a[keyi]);
//此时 prev前面的都比key小 后面的都比key大
//递归,[begin,keyi-1],keyi,[keyi+1,end]
keyi = prev;
return keyi;
}
//快排改成非递归
void QuickSortNore(int* a, int begin, int end)
{
//创建一个栈
ST st;
StackInit(&st);
//先把区间[begin,end]入栈
StackPush(&st, end);
StackPush(&st, begin);
while (!StackEmpty(&st))
{
//每一次取出左右边界
int left = StackTop(&st);
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int keyi = PartSort3(a, left, right);
//子区间[left,keyi-1],keyi,[keyi+1,right]
//先入右子区间
//判断一下 如果区间长度不为0才入栈
if (keyi + 1 < right)
{
StackPush(&st, right);
StackPush(&st, keyi+1);
}
//左子区间
if (left < keyi - 1)
{
StackPush(&st, keyi - 1);
StackPush(&st, left);
}
}
//用完栈销毁
StackDestory(&st);
}
✨感谢阅读~ ✨
❤️码字不易,给个赞吧~❤️