快速排序

平时我们在排序的时候是直接用STL的,sort()一遍就行了,但是我们要掌握快速排序的精髓。

以下看到一个大神的讲解。

快速排序的基本思想是

1、先从数列中取出一个数作为基准数

2、分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边

3、再对左右区间重复第二步,直到各区间只有一个数

概括来说为 挖坑填数+分治法

下面举例来进行说明,主要有三个参数,i为区间的开始地址,j为区间的结束地址,X为当前的开始的值

第一步,i=0,j=9,X=21

0123456789
2132439854452346686

第二步,从j开始由,后向前找,找到比X小的第一个数a[7]=4,此时i=0,j=6,X=21 
进行替换

0123456789
4324398544523216686

第三步,由前往后找,找到比X大的第一个数a[1]=32,此时i=2,j=6,X=21

0123456789
4214398544523326686

第四步,从j=6开始由,由后向前找,找到比X小的第一个数a[0]=4,此时i=2,j=0,X=21,发现j<=i,所以第一回结束

可以发现21前面的数字都比21小,后面的数字都比21大 
接下来对两个子区间[0,0]和[2,9]重复上面的操作即可

下面直接给出过程,就步详细解说了

i=2,j=6,X=43

0123456789
4214398544523326686

i=4,j=6,X=43

0123456789
4213298544523436686

i=4,j=5,x=43

0123456789
4213243544523986686

i=5,j=5,x=43

0123456789
4213223434554986686

然后被分为了两个子区间[2,3]和[5,9]

….最后排序下去就是最终的答案

0123456789
4212332434554668698

总结:

1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。

2.j–由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。

3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。

4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。

以下是普通的快速排序的代码

void quick(int s[],int l,int r)
{
    if(l<r)
    {
        int i=l,j=r,x=s[l];
        while(i<j)
        {
            while(i<j&&x<=s[j])//从右向左找
                j--;
            if(i<j)
                s[i++]=s[j];//交换二者位置
            while(i<j&&x>s[i])//从左向右找
                i++;
            if(i<j)
                s[j--]=s[i];
        }
        s[i]=x;
        quick(s,l,i-1);//递归继续排序,直到不符合l<r的条件
        quick(s,i+1,r);
    }
}

 以上的代码在落谷的P1177上在后三个数据上会TLE这就涉及到几个问题

1.数据中有大量重复元素。2.检索到最后,区间的范围会变得很小

对此,我们可以进行集中优化。

一、随机化

如果永远取第一个元素作为枢轴的话,在数组已经有序的情况下每次划分都将得到最坏的结果,时间复杂度退化为O(n^2)。因为其中一个子序列每次都只比原序列少一个元素,该侧的递归深度将达到最大。
然而,我们可以通过随机选取枢轴元素来打破这种固定模式,这样每次都是最坏划分的概率就非常小了。实现起来只需要先将随机选中的元素和第一个元素交换一下位置作为枢轴元素,然后就可以接着用原来的方法进行排序了。

void quickSort(int a[], int left, int right)
{
    if (left >= right)
        return;
    **int i = left, j = right, pivot = rand() % (right - left + 1) + left;**
    **swap(a[left], a[pivot]);**
    while (i < j)
    {
        while (j > i && a[j] >= a[left])
            j--;
        while (i < j && a[i] <= a[left])
            i++;
        swap(a[i], (i == j) ? a[left] : a[j]);
    }
    quickSort(a, left, i-1);
    quickSort(a, j+1, right);
}

二、小区间插入排序

当序列长度分割到足够小后,继续使用快速排序递归分割的效率反而没有直接插入排序高。因此我们可以增加一个判断,当区间长度小于10以后改为使用插入排序。

void insertSort(int a[], int left, int right)
{
    for (int i = left + 1; i <= right; i++)
        for (int j = i; j > 0 && a[j] < a[j-1]; j--)
            swap(a[j], a[j-1]);
}

void quickSort(int a[], int left, int right)
{
    if (left >= right)
        return;
    **if (right - left + 1 < 10)
    {
        insertSort(a, left, right);
        return;
    }**
    int i = left, j = right, pivot = rand() % (right - left + 1) + left;
    swap(a[left], a[pivot]);
    while (i < j)
    {
        while (j > i && a[j] >= a[left])
            j--;
        while (i < j && a[i] <= a[left])
            i++;
        swap(a[i], (i == j) ? a[left] : a[j]);
    }
    quickSort(a, left, i-1);
    quickSort(a, j+1, right);
}

三、聚拢重复元素

完成了前两步优化后,代码成功AC了前四个点,但最后一个点还是TLE了。下载输入数据一看,竟然是100000个完全一样的数字……对于这种情况,如果还让程序傻傻地分割的确没有必要。于是我想出了一种聚拢重复元素的办法,专治这种变态的数据。
这种方法的主要思想是,在j向前扫描的过程中,每次遇到和枢轴元素相同的元素,就将其与前方第一个异于枢轴元素的元素交换位置,然后继续原本的工作。如果在i之前没有找到任何一个异于枢轴元素的元素,说明此时i与j之间已经全部都是与枢轴元素相同的重复元素了,这就把重复的元素都聚拢到了中间。这时我们再想办法把枢轴元素也加入到这个重复序列中,然后就不必继续向中间扫描了,直接以这个重复序列的两端作为分割线即可。同理,i向后扫描的过程中也可以运用这种思想。
思路应该还是比较好理解的,但是具体实现起来有些麻烦,有不少细节需要考虑到,为了解释方便我把它们都写在注释里。

void quickSort(int a[], int left, int right)
{
    if (left >= right)
        return;
    if (right - left + 1 < 10)
    {
        insertSort(a, left, right);
        return;
    }
    int i = left, j = right, k, flag = 0, pivot = rand() % (right - left + 1) + left;
    swap(a[left], a[pivot]);
    //到这以前都和原来一样,主要就是下面的两个子while循环里分别增加了一个大的if判断
    while (i < j)
    {
        while (j > i && a[j] >= a[left])
        {
            if (a[j] == a[left])  //如果当前扫描到的元素等于枢轴元素
            {
                for (k = j-1; k > i; k--)  //向前寻找第一个和枢轴元素不同的元素
                    if (a[k] != a[j])
                    {
                        swap(a[k], a[j]);  //如果找到了则交换,这样和枢轴元素相同的元素都往中间去了
                        break;
                    }
                if (k == i)  //如果k等于i,说明没找到,这时i和j之间都是重复元素了
                {
                    //我们想把枢轴元素也加进来,这时要分两种情况考虑
                    if (a[left] >= a[i])  //如果枢轴元素大等于a[i],则直接交换后大小关系不会出问题,而枢轴元素会接在重复序列的左端
                        swap(a[left], a[i]);
                    else  //否则操作要复杂一些,建议认真体会模拟一下
                    {
                        swap(a[i], a[j]);  //较大的a[i]应该先和a[j]交换到重复序列右端,a[j]接到左端
                        swap(a[left], a[i-1]);  //然后再让枢轴元素继续接到左端,而a[i-1]因为一定比枢轴元素小所以可以换到更左边
                        i--;  //调整左右分割线的位置
                        j--;
                    }
                    flag = 1;  //标记表明聚拢已完成
                    break;
                }
                else continue;  //如果找到了一个异于枢轴元素的元素完成了交换,那么继续向前扫描
            }
            j--;
        }
        if (flag) break;  //如果聚拢已完成,则直接跳出大循环进行分割,i无需再向后扫描
        while (i < j && a[i] <= a[left])  //i向后扫描的过程基本类似
        {
            if (a[i] == a[left] && i != left)  //增加i!=left条件以跳过枢轴元素本身
            {
                for (k = i+1; k < j; k++)
                {
                    if (a[k] != a[i])
                    {
                        swap(a[k], a[i]);
                        break;
                    }
                }
                if (k == j)
                {
                    //这里比j向前扫描对应的地方简单一些,因为a[j]一定小于枢轴元素,无需分情况讨论
                    swap(a[left], a[j]);
                    flag = 1;
                    break;
                }
                else continue;
            }
            i++;
        }
        if (flag) break;
        //这里以后也和原来一样
        swap(a[i], (i == j) ? a[left] : a[j]);
    }
    quickSort(a, left, i-1);
    quickSort(a, j+1, right);
}

 

最后完整代码

#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdlib>
using namespace std;

int n, a[100010];

void swap(int &a, int &b)
{
    int tmp = a;
    a = b;
    b = tmp;
}

void insertSort(int a[], int left, int right)
{
    for (int i = left + 1; i <= right; i++)
        for (int j = i; j > 0 && a[j] < a[j-1]; j--)
            swap(a[j], a[j-1]);
}

void quickSort(int a[], int left, int right)
{
    if (left >= right)
        return;
    if (right - left + 1 < 10)
    {
        insertSort(a, left, right);
        return;
    }
    int i = left, j = right, k, flag = 0, pivot = rand() % (right - left + 1) + left;
    swap(a[left], a[pivot]);
    while (i < j)
    {
        while (j > i && a[j] >= a[left])
        {
            if (a[j] == a[left])
            {
                for (k = j-1; k > i; k--)
                    if (a[k] != a[j])
                    {
                        swap(a[k], a[j]);
                        break;
                    }
                if (k == i)
                {
                    if (a[left] >= a[i])
                        swap(a[left], a[i]);
                    else
                    {
                        swap(a[i], a[j]);
                        swap(a[left], a[i-1]);
                        i--;
                        j--;
                    }
                    flag = 1;
                    break;
                }
                else continue;
            }
            j--;
        }
        if (flag) break;
        while (i < j && a[i] <= a[left])
        {
            if (a[i] == a[left] && i != left)
            {
                for (k = i+1; k < j; k++)
                {
                    if (a[k] != a[i])
                    {
                        swap(a[k], a[i]);
                        break;
                    }
                }
                if (k == j)
                {
                    swap(a[left], a[j]);
                    flag = 1;
                    break;
                }
                else continue;
            }
            i++;
        }
        if (flag) break;
        swap(a[i], (i == j) ? a[left] : a[j]);
    }
    quickSort(a, left, i-1);
    quickSort(a, j+1, right);
}

int main()
{
    srand((int)time(NULL));
    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        scanf("%d", &a[i]);
    quickSort(a, 0, n-1);
    for (int i = 0; i < n-1; i++)
        printf("%d ", a[i]);
    printf("%d\n", a[n-1]);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值