快速排序

  • 正如它的名字一样,快速排序实在实践中最快的已知排序算法,它的平均运行时间是O(N log
    N)。快速排序的基本思想就是选取一个枢纽元,将所有小于它的元素都放在它的左边,所有大于它的元素都放在它的右边。此时此元素位于它的最终位置。然后对左边和右边的元素执行相同的操作。
    就其对S这个数组的具体操作步骤来说,可以分为下列简单的四步:

1.如果S中的元素个数是0或1个,则返回(0个或1个元素可是为有序)
2.去S中的任意一个元素v,此元素为枢纽元
3.将S中除了v的元素分为两部分,S1与S2,S1的所有元素均小于等于v,S2的所有元素均大于等于v
4.对S1和S2继续执行快速排序
由于第3步中分割的情况会由于v的选取方法不同而不同。不难看出,一种比较好的情况是S1和S2的元素个数差不多。
其中,一种没有经过充分考虑的选择是将数组的第一个元素当做枢纽元。如果输入时随机的,那么这么做是可以接受的。但如果数组是预排序或者反序,那么这样的枢纽元就会导致劣质的分割。不难看出,元素不是被分到S1就是被分到S2,这样快速排序所花费的时间将是二次的。
首先,我们还是选取第一个元素当做枢纽元,我们这里还有一个问题没有解决,就是如何将元素划分到S1和S2, 虽然选取第一个元素是种糟糕的选择,但这也是一种很常见的方案,而且只要稍作修改便可以避免糟糕的情况。
这里,我不得不使用图示的办法,如何操作很难用文字解释清楚,然而画图总是很麻烦的。。。

  • 我们让i指向第二个元素,j指向最后一个元素。首先将i右移,直到指向第一个比枢纽元大的数,只有j左移,直到指向第一个比枢纽元小的数,此时i和j指向的两个元素的位置交换。继续以上操作,直到i=j或者i>j(此时不交换元素)。这时不难发现,j右边的元素均大于枢纽元,而j所指向的元素小于枢纽元,此时交换数组第一个元素和j所指向的元素。然后对j左边的元素继续进行该操作,对j右边的也是如此(注意不包括j,j指向的位置就是元素最终的位置)。
    下面给一个程序例程:
static inline void swap(int *a,int *b)
{
    int tmp;
    tmp=*b;
    *b=*a;
    *a=tmp;
}
void quick(int a[],int L,int R)
{
    int i,j;
    int pivot;
    if(L<R)
    {
        pivot=a[L];
        i=L+1;j=R;
        for( ; ; )
        {
            for(;a[i]<pivot&&i<=R;i++){}
            for(;a[j]>pivot;j--){}
            if(i<j)
                swap(&a[i],&a[j]);
            else
                break;
        }
        swap(&a[L],&a[j]);
        quick(a,L,j-1);
        quick(a,j+1,R);
    }
}
void QuickSort(int a[],int lenth)
{
    quick(a,0,lenth-1);
}

这里有个值得注意的地方,就是i最后可能会出现越界。不难想象,如果枢纽元是最大值的话,那么i肯定会越界。所以这里还要限定i的范围。而j却不需要,因为j一旦指向最左边的枢纽元便会停下,而且j之后也不会在左移,此时i和j必定交错。

  • 事实上,枢纽元最好的选择便是数组的中值,因为这样S1和S2的元素总是一样的。然而实际操作中,我们不可能去寻找数组的中值,这显然会使快速排序的速度放慢。另一种做法是随机选取枢纽元。而随机数的生成显然是很昂贵的。
    一般的做法我们是选取数组最左端,最右端,中间三个数的中值作为枢纽元。这种做法可以消除预排序这种坏情况。这里给出例程:
static inline void swap(int *a,int *b)
{
    int tmp;
    tmp=*b;
    *b=*a;
    *a=tmp;
}
int Media(int a[],int L,int R)
{
    int Center;
    Center=(L+R)/2;
    if(a[L]>a[Center])
        swap(&a[L],&a[Center]);
    if(a[L]>a[R])
        swap(&a[L],&a[R]);
    if(a[Center]>a[R])
        swap(&a[Center],&a[R]);
    swap(&a[L],&a[Center]);
    return a[L];
}
void quick(int a[],int L,int R)
{
    int i,j;
    int pivot;
    if(L<R)
    {
        pivot=Media(a,L,R);
        i=L+1;j=R;
        for( ; ; )
        {
            for(;a[i]<pivot;i++){}
            for(;a[j]>pivot;j--){}
            if(i<j)
                swap(&a[i],&a[j]);
            else
                break;
        }
        swap(&a[L],&a[j]);
        quick(a,L,j-1);
        quick(a,j+1,R);
    }
}
void QuickSort(int a[],int lenth)
{
    quick(a,0,lenth-1);
}

这里使用了Media函数。它的做法便是找出左端,右端,中间这三个数中的中值,并把它放在数组的第一个位置。正因为如此做,我们前一个例程只需稍作修改即可。而且不难看出,Media函数将最大值放在了数组的最右端,这时可以不对i进行越界判断,因为i指向最大值时必定会停下。

  • 之前讨论过插入排序,事实上,对于小数组(一般长度不超过20个元素),快速排序不如插入排序好。而且,由于这里的快速排序是递归的,而调用函数的开销是很大的。通常的解决方案是对于小的数组,我们使用插入排序。使用这种策略大约可以节省15%的时间。一种好的截止范围是数组元素个数等于10的时候。这种做法也避免了一些有害的情况。在使用Media时,元素只有一个或两个的情况。这里再给一个例程:
void InsertSort(int arr[],int lenth)
{
    int p,i;
    int tmp;
    for(p=1;p<lenth;p++)
    {
        tmp=arr[p];
        for(i=p;i>0&&arr[i-1]>tmp;i--)
            arr[i]=arr[i-1];
        arr[i]=tmp;
    }
}
static inline void swap(int *a,int *b)
{
    int tmp;
    tmp=*b;
    *b=*a;
    *a=tmp;
}
int Media(int a[],int L,int R)
{
    int Center;
    Center=(L+R)/2;
    if(a[L]>a[Center])
        swap(&a[L],&a[Center]);
    if(a[L]>a[R])
        swap(&a[L],&a[R]);
    if(a[Center]>a[R])
        swap(&a[Center],&a[R]);
    swap(&a[L],&a[Center]);
    return a[L];
}
#define Cutoff (10)
void quick(int a[],int L,int R)
{
    int i,j;
    int pivot;
    if(L+Cutoff<=R)
    {
        pivot=Media(a,L,R);
        i=L+1;j=R;
        for( ; ; )
        {
            for(;a[i]<pivot;i++){}
            for(;a[j]>pivot;j--){}
            if(i<j)
                swap(&a[i],&a[j]);
            else
                break;
        }
        swap(&a[L],&a[j]);
        quick(a,L,j-1);
        quick(a,j+1,R);
    }
    else
        InsertSort(a+L,R-L+1);
}
void QuickSort(int a[],int lenth)
{
    quick(a,0,lenth-1);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值