交换排序图解_快速排序精解(图文版包你学会!)

最近一直在看排序,看到网课老师讲快排时,感觉好像很简单的样子,直到自己手动敲起了代码,才发现很多地方都有所欠缺,相信很多博友对于快排都不陌生,但是感觉这玩意离自己还是有那么一层纱,如果屏幕前的你也有这种感觉,那么接下来请随我一起揭开这一层薄纱,来一睹它的芳容. ----作者注

进阶排序的一般步骤(快排/归并):

1.问题的分解

2.子问题的递归

3.子问题解的合并

这里简单且草率地说一下快排和归并的联系(如果说的有问题,还望大牛批评指正):快排和归并是一对难兄难弟,快排重点是问题的分解,也就是第一部分Partiiton()上下了大功夫,到了第三部分,就没它什么事了,因为前两部分就已经把数组排序做好了.

归并排序不一样,归并排序在问题的分解上面很随意,取索引中间值作为划分依据,分为左数组和右数组,但是在问题的合并上下了狠功夫,归并排序以后再讲,这里主要讲的还是快排(没办法,因为快排优秀啊~时间复杂度和空间复杂度可接受程度更高,应用更广泛)

基本代码:

//最常见的双向移动指针partition

int Partition(vector& 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& 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& 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& 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& 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& 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值