快速排序
主要特点
1)普通快速排序最差时间复杂度为o(n^2)
2)期望时间复杂度为o(nlgn)
3)在o(nlgn)中蕴含的常量比较小
4)就地排序,不需要辅助数组空间
主要操作
1.数组划分
在快速排序算法中,最为关键的就是选取一个基值,将数组分为大于基值以及小于基值两部分,并返回基值所以在位置以利用于递归划分。
下面先介绍Lomuto partitioning方法。
对数组a,设需要划分的其中一段为a[p]~a[r],我们期待的结果是得到一个q,其中p<=q<=r,使得a[p]~a[q-1]<=a[q]<=a[q+1]~a[r],这个时候原先的一段数组被分成了三部分。
首先,设基值为这段数组的最后一个元素a[r],我们希望最后得到的结果是a[r]现在对应的值在算法结束后可以排在比他大和小的两部分的中间爱。
然后令i=p-1; j=p,当发现有a[j]>x时,j继续前进,不需要任何移动。当发现a[j]<=x时,我们需要将这个元素放到小于基值的一边,于是将i自加1,并交换此时a[i],与a[j]的元素,然后j自加1。这个时候i指向的是比基值小的那段数据的最后一个元素,j指向的是第一个还没有判断的剩余元素。
上面一步不断循环直到j指向了r,此时只剩下r没有和基值判断了,而a[r]本来就是基值,而除了a[r]以外,a[p]~a[i]是小于基值的部分,a[i+1]~a[r-1]是大于基值的部分,所以此时只需交换a[i+1]和a[r]即可。
由于对数组a从头到尾扫描一次就可以得到结果,因此这一部分算法复杂度为o(n)
2.递归排序
在首先对整个数组进行了划分后,我们将数组分成了两部分,一部分比基值小,一部分比基值大,并且我们知道了基值所在的位置,设为q,因此只需对划分出来的两部分进行递归排序即可。
在最坏的情况下,每次划分出的一部分大小为0,一部分为n-1,这种情况下这种递归划分要进行n次,时间复杂度为o(n^2)
在稍微好一点的情况下,每次划分一部分大小为十分之一,一部分大小为十分之九,这种情况下有递归公式:
T (n ) ≤ T (9n /10) + T (n /10) + (n )= O (n lg n )
在平均的情况下,当最好和最坏的情况依次发生时,最坏的情况对渐进时间上的影响不大,因此时间复杂度为o(nlgn)
3.随机化
随机化处理在需要划分的区域中随机选择一个作为划分基准,这样可以使划分得到更好的平均情况性能。
其他讨论
1.hoare划分
这个划分主要原理和上面的划分原理差不多,不一样的地方在于这个划分将基准元素也分到某一个组中,得到是两个分组。具体如下:
对于数组a,区间为p~r的一段,设基准为区域的首个元素x=a[p],i,j初始分别指向区域的两头,i和j不断靠近,当遇到a[j]<x,a[i]>x时,将两个元素进行交换,这时i指向的是比基准小的最后一个元素,j指向的是比基准大的元素。不断循环直至i==j循环结束,此时以i或j分界将数组划分为两部分。
这个算法也是扫描一次数组即可完成,因此时间复杂度为o(n)
2.stooge排序
stooge排序的原理是将数组分为三部分,首先对数组的前2/3进行排序,然后对数组的后2/3进行排序,最后再对数组的前2/3进行排序。递归公式为T(N)=3T(2n/3)+o(1),根据主定理,得到时间复杂度为o(n^log(3/2,3))~=o(n^2.7)
详见http://ufownl.blog.163.com/blog/static/1250122200861072445895/
3.消除尾递归
由于quicksort第二次递归调用不是必须的,因此可以用循环代替,取消尾递归,具体实现见下面代码quicksort2
4.三数取中
为了更好地得到基准元素,可以使用随机选取三个元素,然后取中数的方式进行优化,具体实现见下面代码three_randomized_partition
5.区间模糊排序
对n个区间[ai,bi],进行排序,使得存在一个ci属于[ai,bi],满足c1<=c2<=……<=cn,使用快速排序进行改造实现。
参考:http://cnicwsh.iteye.com/blog/504394,实现代码见最后。
问题描述:
考虑这样的一种排序问题,即无法准确地知道待排序的各个数字到底是多少。对于其中的每个数字,我们只知道它落在实轴上的某个区间内。亦即,给定的是n个形如[a(i), b(i)]的闭区间(这里小括后起下标的作用,后同),其中a(i) <= b(i)。算法的目标是对这些区间进行模糊排序(fuzzy-sort),亦即,产生各区间的一个排列<i(1), i(2), ..., i(n)>,使得存在一个c(j)属于
区间[a(i(j)), b(i(j))],满足c(1) <= c(2) <= c(3) <= ... <= c(n)。
a) 为n个区间的模糊排序设计一个算法。你的算法应该具有算法的一般结构,它可以快速排序左部端点(即各a(i)),也要能充分利用重叠区间来改善运行时间。(随着各区间重叠得越来越多,对各区间进行模糊排序的问题会变得越来越容易。你的算法应能充分利用这种重叠。)
b) 证明:在一般情况下,你的算法的期望运行时间为Θ(nlgn),但当所有的区间都重叠时,期望的运行时间为Θ(n)(亦即,当存在一个值x,使得对所有的i,都有x∈[a(i), b(i)])。你的算法不应显式地检查这种情况,而是应随着重叠量的增加,性能自然地有所改善。
算法思想:
利用快速排序算法的思想,对区间序列进行划分。但此时的主元区间元素是一个区间元素集合中所有区间元素的公共区间(交集),
即是说该集合中的所有区间元素都是“相等的”或者说“任意序都是有序的”。初始时,算法任选一个区间元素作为主元(同快速排序)。
算法运行过程中,如果某个区间元素严格小于主元,则将其放到序列左边;如果其严格大于主元,则将其放到序列右边;否则,说明该区间元素与主元相交,则更新主元区间大小为该相交的区间部分,并将该区间元素加入到与主元“相等”的区间元素集合中。处理完当前序列后,再递归处理左边和右边的子序列(分治思想嘛,同快速排序)。
在程序进行的过程中,将不断排除掉重叠的部分,如果所有部分都会重叠,则在第一次partition的时候已经完成,用时为o(n)
普通快速排序代码如下:
#include<stdio.h>
#include<algorithm>
#include<stdlib.h>
#include<time.h>
using namespace std;
int a[100];
int b[100];
int c[100];
void stooge_sort(int *a,int i,int j){
if(a[i]>a[j])
swap(a[i],a[j]);
if((i+1)>=j)
return;
int k=(j-i+1)/3;
stooge_sort(a,i,j-k);;
stooge_sort(a,i+k,j);
stooge_sort(a,i,j-k);
}
int partion(int *a, int p, int r){
int x=a[r];
int i=p-1;
for(int j=p; j<r; j++){
if(a[j]<=x){
i++;
swap(a[i],a[j]);
}
}
swap(a[i+1],a[r]);
return i+1;
}
void quicksort(int *a, int p, int r){
if(p<r){
int q=partion(a,p,r);
quicksort(a,p,q-1);
quicksort(a,q+1,r);
}
}
void quicksort2(int *a, int p, int r){
while(p<r){
int q=partion(a,p,r);
if(q-p<r-q){
quicksort2(a,p,q-1);
p=q+1;
}
else{
quicksort2(a,q+1,r);
r=q-1;
}
}
}
int moare_partion(int *a,int p,int r){
int x=a[p];
int i=p-1;
int j=r+1;
while(1){
do{
j--;
}while(a[j]<x);
do{
i++;
}while(a[i]>x);
if(i<j)
swap(a[i],a[j]);
else
return j;
}
}
void moare_quicksort(int *a, int p, int r){
if(p<r){
int q=moare_partion(a,p,r);
moare_quicksort(a,p,q);
moare_quicksort(a,q+1,r);
}
}
int randomized_partition(int *a, int p, int r){
srand(time(NULL));
//i~[p,r]
int i=p+rand()%(r-p+1);
swap(a[i],a[r]);
return partion(a,p,r);
}
void randomized_quicksort(int*a, int p, int r){
if(p<r){
int q=randomized_partition(a,p,r);
randomized_quicksort(a,p,q-1);
randomized_quicksort(a,q+1,r);
}
}
int three_randomized_partition(int *a, int p, int r){
srand(time(NULL));
//i~[p,r]
int i=p+rand()%(r-p+1);
int j=p+rand()%(r-p+1);
int k=p+rand()%(r-p+1);
if(j<=k&&k<=i||i<=k&&k<=j)
i=k;
if(k<=j&&j<=i||i<=j&&j<=k)
i=j;
swap(a[i],a[r]);
return partion(a,p,r);
}
void three_randomized_quicksort(int *a, int p, int r){
if(p<r){
int q=three_randomized_partition(a,p,r);
three_randomized_quicksort(a,p,q-1);
three_randomized_quicksort(a,q+1,r);
}
}
int main(){
int len;
scanf("%d",&len);
for(int i=1; i<=len; i++){
scanf("%d",&a[i]);
}
// quicksort2(a,1,len);
// moare_quicksort(a,1,len);
// stooge_sort(a,1,len);
// three_randomized_quicksort(a,1,len);
randomized_quicksort(a,1,len);
for(int j=1; j<=len; j++){
if(j!=len)
printf("%d ",a[j]);
else
printf("%d\n",a[j]);
}
return 0;
}
区间模糊快速排序代码:
#include<stdio.h>
using namespace std;
class Array{
public:
int begin;
int end;
Array(){}
Array(int a, int b){
if(a<=b){
this->begin=a;
this->end=b;
}
else{
this->begin=0;
this->end=0;
}
}
};
Array swap(Array*a, int x, int y){
Array tmp;
tmp=a[x];
a[x]=a[y];
a[y]=tmp;
}
//将数组分为三段
//一段是小于基准主元部分,这一部分数组下标从p~i-1
//二段是与基准主元重叠的部分,这一部分数组下标是从i~j
//三段是大于基准主元的部分,这一部分数组下标从j+1~r
//在过程中使用k指示下一个需要判断的数组元素
//递归将返回两个整数i,j,用于标志下一次递归要开始的地方
//使用Array类型作为返回仅仅是为了存储i,j俩个数,标志与基准重叠部分,不代表一个线段
Array partition(Array *a, int p, int r){
Array x=a[p];
int i=p;
int k=p+1;
int j=r;
while(k<=j){
//当前区域小于基准主元区域
if(a[k].end<x.begin){
swap(a,i,k);
i++;
k++;
}
//当前区域大于基准主元区域
else if(a[k].begin>x.end){
swap(a,j,k);
j--;
}
//当前区域于基准主元区域有重叠
else{
x.begin=a[k].begin>x.begin?a[k].begin:x.begin;
x.end=a[k].end<x.end?a[k].end:x.end;
k++;
}
}
return Array(i-1,j+1);
}
void quicksort(Array *a, int p, int r){
if(p<r){
Array q=partition(a,p,r);
quicksort(a,p,q.begin);
quicksort(a,q.end,r);
}
}
int main(){
Array a[6]={Array(2,4),Array(0,1),Array(5,6),Array(3,3),Array(1,5),Array(8,9)};
quicksort(a,0,5);
for(int i=0; i<6; i++){
printf("[%d,%d] ",a[i].begin,a[i].end);
}
printf("\n");
}