快排思想及基础步骤
任意定枢轴版
基本思想:
采用无重复循环(每个地方在一次循环中指针只经过一次)每经过一次循环,枢轴左边的数都比它小,右边的数都比它大,
即差不多可以理解为走一遭就定好了枢轴的位置。然后递归,左边递归左边的,右边递归右边的,典型分治问题(大问题分割慢慢变成子问题的过程)最后递归出口就是两指针出发位置一样了,也就是越分越小分到只有自己一个数了,就结束了。
!!!一个轮回结束是以当前选定的枢轴归位为标志的
需要的东西:
- 定枢轴
- 前后两指针
- 递归
step1: 确定好枢轴。就是中间点,我们设为key,不是说它一定要在中间,就是定个偶像标杆,明白吗?但是呢,定在中间最好,为方便选择,一般是让key=q[0]或q[n-1]或q[n-1/2]这三种。
step2: 定两个指针,我们设为i,j。i从0位置的前一个位置出发,j从最后一个元素的后面出发,没有数组越界哈,因为真正用的时候还是0和n-1开始的,这里直接看代码比较好。
step3: 写递归式,注意边界(敲重要)
代码模板
递归出口
走的过程
递归式
void quick_sort3(int q[],int l,int r)
{
if(l>=r)return;
int i=l-1,j=r+1,key=q[l+r>>1];
while(i<j)
{
do i++;while(q[i]<key);
do j--;while(q[j]>key);
if(i<j) swap(q[i],q[j]);
}
quick_sort3(q,l,j);
quick_sort3(q,j+1,r);
}
细节问题(边界致命)
可能不明白的地方:
- l不一定要>=r,就=r也是可以的;
- 用
l+r>>1
是二进制除以2的方法,速度快些; - 用
do-while
循环不用while-do的原因是:
while判断的是<key,如果某一时刻ij都=key,你说你怎么收场
两指针就会停滞不前到第三步交换,换了发现还是没变,就会陷
入无限循环;
- 只能<key不能<=Key的原因:
如果=Key,那key这个位置的数岂不是永远不动忽略掉了吗?
会造成两边都有所谓的基准数,其实这种并不是什么让基准数归
位的做法,而是让左边都比它小,右边都比它大那种,然后它
自己归位
而下面那种原始的快排 就要=
- 为什么swap是需要i<j的条件呀?
非常美好的理想就是ij相遇即一次循环结束,但是鉴于里面不止
一个while循环,所以它会在里面自己走,ij可能来不及经历那
两个比较就跳着跳着交叉了错过了彼此,注意,只要交叉了,那
就说明已经结束一轮路程了;
---------
目前遇到的例子ij一轮后都是相邻的,所以j,j+1和i-1,i一样
这个还是需要数学推导证明;(等我遇到回来补充)
- 递归式的两个边界需要注意什么呢?怎么确定的?
结论
//枢轴选的q[l]:递归式边界就
quick_sort3(q,l,j);
quick_sort3(q,j+1,r);
//枢轴选的q[r]:递归式边界就
quick_sort3(q,l,i-1);
quick_sort3(q,i,r);
//其它无所谓
此处有一个小小的数学推导:
由算法的过程我们可以知道q[l...i-1]<key,不包括i,因为此
时i停下来了,q[i]>=x,同理,q[j]<=x,因为走完一轮j<=i了
所以j必在l...i-1之中,所以q[l..j]均<=key;
--------------------------------------------------
>>>现在我们再来看为什么选i或j定边界还有枢轴限定条件:
若定j位分界点,而枢轴选择q[r]就可能会遇到无限划分(n,0)
和(0,n)就是分块分来分去都是这样,因为j有可能=r,在r处
停下来,而此时正好r前面所有的数都<q[r],j就在r处等,一直
等到i的那个循环停止,此时i也=r,停下来了,由于也不满足
swap条件,它就出去了,结果子子孙孙都是这样,也就是不断
n和0(枢轴左边n个数,右边0个数,枢轴作为最后一个数)
---------------
同理,选i定界不能让l作为枢轴,j从右边一直过来,i在l处等
l右边的数都比枢轴大,就是0和n的情况
- j-1,j+1这个为什么不行?
那请问j它自己怎么办?这个适用于枢轴取的是第一个或最后
一个且为本列数中的最小值或最大值;这个时候就不会造成
无限划分,因为j-1刚好跳过了无限划分的情况,反正最后一
个都是最大,它就会确定好它的位置去排别人就恢复正常了
一旦无限划分,就会造成内存出限,因为它一直在递归
(要记得递归是要有递归出口才能溜的)
但是 如果很不巧 你选的某个枢轴刚好是最大的或最小的,记得更改哈,不然排鬼序,笑死,永远卡在那。
如一组测试数据:
3 1 7 2 6 4 5 你选这中间某个比如7作为枢轴,你看,好像不是第一个也不是最后一个,应该不怕,但是注意这只是第一轮,一轮结束,7就跑到最后一个地方去了,无限划分开始了…
到这里 你就会发现 真正的结论并不是上面那个固定的什么,其实是避免最大最小的情况,而不是要避免定在l或r
- 内存超限(无限递归)和没排好自己出来了的区别:
没排好出来一定是到了递归出口,这是j-1,j+1的情况,因为j
没人管啊,它终有一天是独自在那,所以还是乱序的
-------
而内存超限一定是因为递归递不出来了hhh
正常普通版:
枢轴选第一个:
基本思想: ij相遇的位置就是选定当前枢轴(第一个数)最终的位置,每次一轮确定一个;
定的q[l],就要从右边j开始(从对立面开始可保
证ij相遇时temp是小于基准数的,最后换到第一位去)
第一种写法:边换边归位
首先,j停下来(可能是找到比key小的数也可能是遇上i)让
q[i]存下来j停下来的数,现在的i就是第一个枢轴
i停下来,让j位置换上i的数,
现在的情况就是key存了枢轴的,枢轴存了j的,j换上了i的
key=枢轴
枢轴=j(第一轮)i=j(后面)
j=i
i=key
最终交换了枢轴和ij相遇点
key=q[l],key=q[i]
q[i]=q[j]
q[j]=q[i]
q[i]=key
#include<iostream>
using namespace std;
const int N = 1e6;
int n, q[N];
void quick_sort(int q[],int l, int r)
{
if (l >= r) return;
int i = l, j = r, key = q[l];
while (i < j)
{
while (q[j] >= key && i < j) j--;
q[i] = q[j];
while (q[i] <= key && i < j) i++;
q[j] = q[i];
}
q[i] = key;//ij都可 反正指的同一个数
quick_sort(q, l, j - 1);
quick_sort(q, j + 1, r);
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++) cin >> q[i];
quick_sort(q,0, n - 1);
for (int i = 0; i < n; i++) cout << q[i];
return 0;
}
第二种写法:先交换后归位
#include<iostream>
using namespace std;
const int N = 1e6;
int n, q[N];
void quick_sort(int l, int r)
{
if (l >= r) return;
int i = l, j = r, key = q[l];
while (i!=j)
{
while (q[j] >= key && i < j) j--;
while (q[i] <= key && i < j) i++;
if(i<j) swap(q[i],q[j]);
}
//基准数和相遇点交换
q[l]=q[i];
q[i]=key;
quick_sort(l, j - 1);
quick_sort(j + 1, r);
}
int main()
{
scanf("%d",&n);
for (int i = 0; i < n; i++) scanf("%d",&q[i]);
quick_sort(0, n - 1);
for (int i = 0; i < n; i++) printf("%d ",q[i]);
return 0;
}
更新~
无敌至尊版快排诞生了
枢轴随便选且不会造成无限划分
void quick_sort3(int q[],int l,int r)
{
if(l>=r)return;
int i=l-1,j=r+1,key=q[l+r>>1];
while(i<j)
{
do i++;while(q[i]<key);
do j--;while(q[j]>key);
if(i<j) swap(q[i],q[j]);
}
//更新过的代码
quick_sort3(q,l,j-(j==r));
quick_sort3(q,j+1,r);
}
ps:普通版-边走边交换的还不怎么特清晰(待补充)
鉴于特殊版快排可以随机选枢轴 但是有特殊情况会造成无限划分,普通版呢又不能随机选枢轴;此问题有待研究…
但是取中值效率应该蛮高的~