ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2020-12-30 19:01:58  阅读:842  来源: 互联网

标签:begin sp end 版包 int 元素 精解 bigger 图文


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

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

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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有