其实对于c++选手来说,快速排序即使没有掌握,也可以使用STL库中的sort函数。但仔细学习一下快速排序还是很有必要的,不光是由此演化的快速选择算法,快排的种种优化也可以锻炼能力。
快排的思想很简单,就是选取序列中任意一个元素为基准,将序列划分成两部分,序列左边都比基准小,右边都比基准大,也就是找到了基准元素的正确位置,这就完成了一趟快速排序,再递归处理左右两个子序列就可以了。
快速排序是冒泡排序的改进,每次移动都是跳跃式的,平均复杂度是O(nlogn)的,而最坏情况下,快排将退化成冒泡排序,复杂度为O(n^2)。
说起来,快排有三种常见的实现方式,分别是左右指针法、挖坑法、前后指针法。这三种方法都可以,在此使用挖坑法,其他两种可以自行了解一下。
挖坑法一般会选取序列左右端点为基准,先将坑放在基准元素处,然后从两端不断挖新坑,填旧坑,最终找到基准元素应该在的坑,填上。
1 void qsort(int l,int r) { 2 if(l>=r) return; 3 int x=a[l],i=l,j=r; 4 while(i<j) { 5 while(a[j]>=x&&i<j) --j; 6 a[i]=a[j]; 7 while(a[i]<=x&&i<j) ++i; 8 a[j]=a[i]; 9 } 10 a[i]=x; 11 qsort(l,i-1); 12 qsort(j+1,r); 13 }
再来说说快排的优化,为什么需要优化呢?快排的平均效率很高,主要是为了针对一些极端情况,比如序列原先有序、有大量重复元素等。
优化的方法主要有三个:1、三数取中法 2、小区间优化 3、针对重复元素的优化。
基准元素如果选的太大或太小,就会影响快排的效率,随机选取基数,或是使用三数取中法都是可以尽量避免的。三数取中法就是选取序列开头结尾及中间位置上的三个数中第二大的数。
其实,挖坑法会选取端点元素为基准,也可以认为是将次大的元素放到序列端点上。
1 void move_mid(int l,int r) { 2 int m=l+(r-l)/2; 3 if(a[l]>a[m]) swap(a[l],a[m]); 4 if(a[m]>a[r]) swap(a[m],a[r]); 5 if(a[l]<a[m]) swap(a[l],a[m]); 6 } 7 8 move_mid(l,r); //快排中加入此语句
小区间优化是指当要排序的序列长度较小时,快速排序的效率甚至不如插入排序,因此当区间长度较小时,可以用插排代替快排。
1 void isort(int l,int r) { //插入排序 2 for(int i=l+1,j;i<=r;++i) { 3 int x=a[i]; 4 for(j=i-1;a[j]>x&&j>0;--j) a[j+1]=a[j]; 5 a[j+1]=x; 6 } 7 } 8 9 if(r-l+1<10) { //快排中加入此语句 10 isort(l,r); 11 return; 12 }
如果序列里有大量重复元素,快排的效率会很低,优化方法是,按照基准划分左右序列时,将重复元素放到序列两端,划分结束后,将重复元素放到基准元素周围。
1 void qsort(int l,int r) { 2 if(l>=r) return; 3 int x=a[l],i=l,j=r,first=l,last=r,llen=0,rlen=0; 4 while(i<j) { 5 while(a[j]>=x&&i<j) { //先将与基准元素相同的元素放到序列两端 6 if(a[j]==x) { 7 swap(a[j],a[last--]); 8 ++rlen; 9 } 10 --j; 11 } 12 a[i]=a[j]; 13 while(a[i]<=x&&i<j) { 14 if(a[i]==x) { 15 swap(a[i],a[first++]); 16 ++llen; 17 } 18 ++i; 19 } 20 a[j]=a[i]; 21 } 22 a[i]=x; 23 int p=l,q=i-1; //将重复元素放到基准元素周围 24 while(p<first&&a[q]!=x) swap(a[p++],a[q--]); 25 p=j+1,q=r; 26 while(q>last&&a[p]!=x) swap(a[p++],a[q--]); 27 qsort(l,i-1-llen); //只需排序重复元素两边的子序列 28 qsort(j+1+rlen,r); 29 }
这三种优化一起使用,效率会很高,接近c++ STL中的sort函数。
1 void move_mid(int l,int r) { //三数取中法 2 int m=l+(r-l)/2; 3 if(a[l]>a[m]) swap(a[l],a[m]); 4 if(a[m]>a[r]) swap(a[m],a[r]); 5 if(a[l]<a[m]) swap(a[l],a[m]); 6 } 7 void isort(int l,int r) { //插入排序 8 for(int i=l+1,j;i<=r;++i) { 9 int x=a[i]; 10 for(j=i-1;a[j]>x&&j>0;--j) a[j+1]=a[j]; 11 a[j+1]=x; 12 } 13 } 14 void qsort(int l,int r) { 15 if(l>=r) return; 16 if(r-l+1<10) { //小区间优化 17 isort(l,r); 18 return; 19 } 20 move_mid(l,r); 21 int x=a[l],i=l,j=r,first=l,last=r,llen=0,rlen=0; 22 while(i<j) { 23 while(a[j]>=x&&i<j) { //先将与基准元素相同的元素放到序列两端 24 if(a[j]==x) { 25 swap(a[j],a[last--]); 26 ++rlen; 27 } 28 --j; 29 } 30 a[i]=a[j]; 31 while(a[i]<=x&&i<j) { 32 if(a[i]==x) { 33 swap(a[i],a[first++]); 34 ++llen; 35 } 36 ++i; 37 } 38 a[j]=a[i]; 39 } 40 a[i]=x; 41 int p=l,q=i-1; //将重复元素放到基准元素周围 42 while(p<first&&a[q]!=x) swap(a[p++],a[q--]); 43 p=j+1,q=r; 44 while(q>last&&a[p]!=x) swap(a[p++],a[q--]); 45 qsort(l,i-1-llen); //只需排序重复元素两边的子序列 46 qsort(j+1+rlen,r); 47 }
呃呃,目前来说,快速排序对我还是很玄学的。。。理论上快排的各种实现方式效果应该是一样的,但洛谷上面的快排模板题对于某些不加优化实现方式是会超时的,在这里放一下简洁的未加优化却可以A掉某些其他实现形式A不掉的模板题的代码。
1 void quicksort(int l, int r) { 2 if (l == r) return; 3 int mid = a[l + (r - l) / 2], i = l, j = r; 4 while (i <= j) { 5 while (a[i] < mid) ++i; 6 while (a[j] > mid) --j; 7 if (i <= j) { 8 swap(a[i], a[j]); 9 ++i,--j; 10 } 11 }; 12 if (i < r) quicksort(i, r); 13 if (j > l) quicksort(l, j); 14 }