快排我接触的也比较多了,从之前NOIP的时候算法老师讲的版本,到之前数据结构课上学习的版本,到现在《算法导论》里讲的版本,我个人并不能不能区别它们的好坏,权且都写出来,以后再来区别。三种实现方式如下:
noip:
void qsort1(int *a,int l,int r)
{
int i,j,mid,temp;
i=l;j=r;mid=a[(i+j)/2];
while(i<=j)
{
while(a[i]<mid) i++;
while(a[j]>mid) j--;
if(i<=j)
{
temp=a[i];a[i]=a[j];a[j]=temp;
i++;j--;
}
}
if(l<j) qsort1(a,l,j);
if(i<r) qsort1(a,i,r);
}
思路即是选取中间数作为参考值,i,j分别从两端向另外一端枚举,然后从左边找到一个比它小的数,从右边找到一个比它大的数,互换位置,直到i,j位置互调,然后进行下一轮递归。
数据结构:
int partition(int *a,int l,int r)
{
int temp;
while(l<r)
{
while(l<r&&a[l]>=flag) l++;
temp=a[l];a[l]=a[r];a[r]=temp;
while(l<r&&a[r]<=flag) l++;
temp=a[l];a[l]=a[r];a[r]=temp;
}
return l;
}
void qsort2(int *a,int l,int r)
{
if(l<r)
{
int flag = partition(a,l,r);
qsort2(a,l,flag-1);
qsort2(a,flag+1,r);
}
}
这个实现就是遇到大的就往后移,遇到小的就往前移。
算法导论:
int partition(int *a,int l,int r)
{
int i,j,x,temp;
x=a[r];
i=l-1;
for(j=l;j<r;j++)
if(a[j]<x)
{
i++;
temp=a[i];a[i]=a[j];a[j]=temp;
}
temp=a[i+1];a[i+1]=a[r];a[r]=temp;
return i+1;
}
void qsort3(int *a,int l,int r)
{
if(l<r)
{
int flag = partition(a,l,r);
qsort3(a,l,flag-1);
qsort3(a,flag+1,r);
}
}
算法格式上来说,算法导论的版本与数据结构类似,但是思路上有些不同:算法导论是维护一个相邻的小队列,当发现队列右端的数比flag标签小时,就将它与队列中的一个数调换,这样可以轻易地将待排序数组分为四段:最左侧 l~i均小于flag,i+1~j均大于flag,j+1~r-1为未排序,r处即是flag标签。
po完了三种版本的代码,再来谈一下快速排序,快排顾名思义就是非常快的排序,它是一种原地排序算法(不需要辅助数组),不稳定的,同时它的期望值是较为理想的O(nlgn),但最坏情况下会达到O(n^2)。
下面讲述快排的几种优化方法。
第一点,怎么变稳定。这个应该不难,就是添加一个标号数组,记录未排序时各元素的下标(b[i]=i),排序时加入双关键字(先按照大小排,大小相同按照标号排)即可,这种方法对于快速排序的时间复杂度影响不是很大,因此有必要时可以这样修改以适应不同情况。
第二点,快速排序的最坏情况是O(N^2),即每次划分都划分成(1,n-1)这样的集合,这是我们无论如何也不想看到的情况。因此我们可以引入随机化快排。
对于第一种版本,只要对mid进行小操作即可(mid=a[(l+r)/2]改为mid=a[random(l,r)]) //此处存疑,因为看了算法导论之后才清楚认识到快排的最坏情况是什么,这个算法每次将集合划分为等长的两部分,按理说深度是lgn的,但之前好多情况过不了题目,后来使用随机化确实过掉了。
第二种版本我并不知道如何使用随机化-。-
第三种版本则是在x的选择上动手脚(x=a[r]改为x=random(l,r);swap(a[x],a[r]);x=a[r];)这样就从l,r之间随机选择了一个数作为x,而不是原本的a[r]。
第三点,跟归并排序的优化类似,就是在划分到足够小的集合时使用插入排序,这个在另一篇文章里已经说过了,应当将集合划分为lgn大小时使用插入排序,最好情况即此。