快速排序精解(图文版包你学会!)
作者:互联网
最近一直在看排序,看到网课老师讲快排时,感觉好像很简单的样子,直到自己手动敲起了代码,才发现很多地方都有所欠缺,相信很多博友对于快排都不陌生,但是感觉这玩意离自己还是有那么一层纱,如果屏幕前的你也有这种感觉,那么接下来请随我一起揭开这一层薄纱,来一睹它的芳容. ----作者注
进阶排序的一般步骤(快排/归并):
1.问题的分解
2.子问题的递归
3.子问题解的合并
这里简单且草率地说一下快排和归并的联系(如果说的有问题,还望大牛批评指正):快排和归并是一对难兄难弟,快排重点是问题的分解,也就是第一部分Partiiton()上下了大功夫,到了第三部分,就没它什么事了,因为前两部分就已经把数组排序做好了.
归并排序不一样,归并排序在问题的分解上面很随意,取索引中间值作为划分依据,分为左数组和右数组,但是在问题的合并上下了狠功夫,归并排序以后再讲,这里主要讲的还是快排(没办法,因为快排优秀啊~时间复杂度和空间复杂度可接受程度更高,应用更广泛)
基本代码:
//最常见的双向移动指针partition
int Partition(vector<int>& A, int begin, int end) { //问题的划分
int record = A[begin];
int i, j;
i = begin, j = end;
while (i <= j) {
while (A[i] <= record && i <= j) {
++i;
}
while (A[j] >= record && i <= j) {
--j;
}
if (i <= j) {
swap(A[i], A[j]);
}
}
swap(A[begin], A[j]);
return j;
}
void Quick_Sort(vector<int>& A, int begin, int end)
{
if (begin < end) {
int q = Partition(A, begin, end); //问题的分解与解决
Quick_Sort(A, begin, q - 1);
Quick_Sort(A, q + 1, end);
}
}
这里的Quick_Sort()相当于问题的分解,就像是递归二叉树那样进行先序遍历,每一次划分就会使得左边的元素都小于当前元素A[i],这里千万要注意:不要把问题的划分Quick_Sort()和区间的划分Partiotion()混淆了,我们先不管Pariiton是怎么实现的,我们只要明白,对于一个数组,当你左边的元素都小于中间的元素(我们叫他主元),右边的元素都大于主元,递归调用结束后,程序就完成了(对于每个元素来说,左边的元素都是小于自己,右边的元素大于自己,这个数组不就整体有序了吗?)
Partition的三种解法
1.单向扫描分区法
基本思路:定义俩指针,sp起始指针指向A[left]的下一个元素,pivot保存起始值,起始bigger指向最后一个元素,判断sp指向的元素与起始值pivot的大小,如果大了,就和bigger指向的元素交换,先不管原来bigger指向的元素与pivot的大小关系,因为如果还是大了,我们还可以拿sp指针和向前移动一位的bigger指针再次交换,直到sp>bigger或者sp交换到了小于pivot的值
难点:1.当sp>bigger时,sp和bigger的指针位置?
2.最终我们A[left]和谁交换?SP?还是bigger?
int Partition(vector<int>& A, int left, int right)
{ //这里的left,right都表示索引
int pivot = A[left];
int sp = left + 1;
int bigger = right;
while (sp <= bigger) {
if (A[sp] <= pivot) sp++;
else {
swap(A[sp], A[bigger]);
bigger--;
}
}
swap(A[0], A[bigger]);//开始我们不确定这里
return bigger;//也不确定这里
}
初始情况,各指针指向位置如图
我们用图来模拟一下最终指向的位置情况:
经过一轮循环后,sp的位置会先等于bigger,再自己和自己交换,之后bigger会等于原来sp的位置(sp,bigger指针经过一次循环后调换了位置,我们可以得出结论,此时return bigger,swap(A[left],A[bigger]))
再来考虑这种情况:
此时的sp会自增两次到达bigger的右边停下,结束循环,那么我们还是可以发下bigger在sp的左侧,方法同上.
再来考虑这种情况:
交换完成后,sp==bigger,此时的sp指向的元素因为<=pivot,所以bigger又在sp的左边,又是同样的情况.
最后一种情况:我就不画图了,大家想一会明白,bigger两次自减,滑落到sp的左边,而sp左边的元素一定是<=pivot的,bigger就指向了最后一个小于等于pivot的元素,***又是***同样的情况,太巧妙了,不是吗?
2.最常见的方法:双向区间扫描法
随笔:虽然思想很简单,但是实现起来和上边一样,有很多细节要考虑,并且很多题目都是可以用双指针写的,要学会活学活用,用出精髓
思路:头尾指针向中间扫,当左指针停在>=pivot的位置,右指针停在<=pivot的位置,交换两者的位置
难点:考虑边界情况,最终的返回值等
先贴代码:
int Partition(vector<int>& A, int begin, int end) { //问题的划分
int pivot = A[begin];
int i, j;
i = begin, j = end;
while (i <= j) {//注意内层while判断条件里也要写上i<=j,因为内层随时有可能突破这个条件
while (A[i] <= pivot && i <= j) {
++i;
} //左指针右移
while (A[j] >= pivot && i <= j) {
--j; //右指针左移
}
if (i <= j) {
swap(A[i], A[j]);
} //交换左右指针
}
swap(A[begin], A[j]); //未知
return j; //未知
}
同样的,我们考虑四种i,j的指向情况
这里的四种情况的模拟就交给大家啦,让大家更好的体悟循环后的语句是怎么写出来的已级到底是交换谁,到底是返回谁,这个问题.
正确答案:四种情况都是j总是在i的左边紧邻i,且j指向最后一个<=pivot的元素.
快排的重要应用selectK(建议熟记思路伪代码,先关题目经常用到它的变种)
题目描述:以尽量高的时间效率(不是尽量高的时间复杂度)求出一个乱序数组中从小到大的第K个元素值(不是下标为K,是第K个)
基本思路:每次划分,找划分元素为当前数组的第qK个,判断qk==k?如果大于k,就表示第k个元素一定在qK的左边,左递归,如果小于k,就表示第k个元素一定在qK的右边,则右递归,注意:递归的第k个元素变成递归的qK-k个元素
问:为何求qK时那里q-p+1加了1呢?**因为我们说N~M中M是这个序列的第(M-N+1)**号元素,比如:1 2,这个序列2是不是这个序列的第(2-1+1)号元素.
问:为何右递归时传入的参数是qK-k而不是qK-k+1呢?
答:因为这里的k和qK都表示说第…号元素,比如qK=3,k=4,q=2,
数组为:2 1 3 6 7 9
则传入的参数为(A,3,5,?)我们传入的参数应该是q+1,也就是从6开始算的,那么6就算是新数组的第一个元素了,我们再传入k-qK就表示求第一个元素的大小了.
int selectK(vector<int>& A, int p, int r, int k) //要找的第K个元素
{
q = partition(A, p,r);
int qK = q - p + 1; //主元是第qk个元素
if (qK == k) return A[q];
else if(qK>k){ //往左边收缩边界,还是找第k个元素
retrun seleckK(A, p, q - 1,k);
}
else { //右边要找的元素是第k-qk个,因为qK也表示第qK个元素,两者相减时不用减一
return selectK(A, q + 1, r,k-qK);
}
}
快排的理想情况,以及现实中的优化解法
快排为什么可以达到NlogN的复杂度呢?它最坏的情况又是什么呢?
我们先来看一张图
1.分析时间复杂度:每次问题都被分成两部分,那么一共有logN向下取整层(这里简化为logN层),每一层排序,双指针用法时间复杂度为O(N),层数为logN,每一层花的时间都是Partiton所花的时间复杂度O(N),所以最后平均(最好最坏)总的时间复杂度就是O(N*logN),简要记忆推导,上面的就足够了
详细推导可以参考这位神牛牪犇的博客:
神牛牪犇博客
2.这种图很理想主义,每次我们取的pivot总是可以把数组划分为左右两段,但是现实是,最坏的情况:所有元素都比我们选取的要大或者小,此时这个排序就退化为插入排序,时间复杂度斜线上升到O(n^2),所以选择pivot元素就很有讲究了.我们希望每次选取的pivot都可以把数组化分为左一半,右一半,而且二者大小基本相等.
但是如果划分出来的元素个数左右绝对相等,又会多出来"找出这个绝对中值元素"的时间复杂度,很多时候也并不合算,所以,我们在现实中(比赛中)往往使用三点中值法.
我们在A[left],A[mid],A[right]三个元素中选择介于中间的元素,我们总不会那么倒霉,也就好比是原来出门踩香蕉皮摔倒的概率是%70,现在减少到%33.333(23333333).
而且更重要的是,这个比较操作很简单,花费的时间复杂度还是常数级别的,可以说四两拨千斤吧~
这里还是贴出代码:
int Partition(vector<int>& A, int begin, int end) { //问题的划分
int mid_Index;
int mid = begin + ((end - begin) >> 1);
if ((A[begin] >= A[mid] && A[begin] <= A[end])||(A[begin] >= A[end] && A[begin]<=A[mid]))
{
mid_Index = begin;
}
else if ((A[end] >= A[mid] && A[end] <= A[begin]) || (A[end] >= A[begin] && A[end] <= A[mid]))
{
mid_Index = end;
}
else {
mid_Index = mid;
}
swap(A[mid_Index], A[begin]);
int record = A[begin];
int i, j;
i = begin, j = end;
while (i<=j) {
while (A[i] <= record&&i<=j) {
++i;
}
while (A[j] >= record&&i<=j) {
--j;
}
if (i <= j) {
swap(A[i], A[j]);
}
}
swap(A[begin], A[j]);
return j;
}
感想
最后再来说说为什么这一篇做的这么认真(随便) 吧.
短学期,空荡的机房,一个人的寝室,似乎也就只有陌生人的力量才能让我觉得片刻的温暖…
当我正打算漫不经心地再水一篇博客时,突然看到有人评论我的文章,我的第一个反应是忐忑,想想是不是因为昨天的博文最后因为时间匆忙,有些烂尾,被人指责不认真,太过随意,最终,自嘲地想想,可能好歹别人还算是愿意给你指出来哪里不对,你不应该虚心接受吗?在超我以及好奇心的驱使下我点开了评论,出乎我意料,竟然是鼓励,说我写的认真,那一刻,我很不争气地泪目了,我突然觉得很对不起他,我写的并没有那么好,但是,他就是那样,夸赞了我的认真,这对他而言真的是一件微乎其微的小事,但是这可能是,这个冬天,我收到过的最暖心的礼物了,谢谢你QWQ.
我保证在接下来寒假时间里,会做出更多高质量的文章来回馈那些给我温暖的人,同时,如果文章里有哪些内容不懂,或者我写的某处有错误,也都欢迎大家私信或者评论指出.
赠人玫瑰,手有余香,祝:生活愉快!
标签:begin,sp,end,版包,int,元素,精解,bigger,图文 来源: https://blog.csdn.net/Alanadle/article/details/111992103