这道题用传统快排(如下所示)的结果就是最后三个点TLE:

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

void quickSort(int a[], int left, int right)
{
    if (left >= right)
        return;
    int i = left, j = right;
    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]);  //i和j相遇则与枢轴元素交换,否则a[i]与a[j]交换
    }
    quickSort(a, left, i-1);
    quickSort(a, j+1, right);
}

因为快排对于一些特殊的情况(例如序列原本就有序、有大量重复元素等等)会进行很多完全不必要的操作,耗费大量时间。为此,我们基于上述普通快速排序算法一步步进行优化。

一、随机化

如果永远取第一个元素作为枢轴的话,在数组已经有序的情况下每次划分都将得到最坏的结果,时间复杂度退化为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);
}

为了追求更快的速度,建议用scanf()和printf()来进行输入输出。经过这几步优化,最后快排终于也能顺利通过了!

完整AC代码如下:

#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;
}

洛谷 P1177 【模板】快速排序的更多相关文章

  1. 洛谷P3373 [模板]线段树 2(区间增减.乘 区间求和)

    To 洛谷.3373 [模板]线段树2 题目描述 如题,已知一个数列,你需要进行下面两种操作: 1.将某区间每一个数加上x 2.将某区间每一个数乘上x 3.求出某区间每一个数的和 输入输出格式 输入格 ...

  2. 洛谷P3375 [模板]KMP字符串匹配

    To 洛谷.3375 KMP字符串匹配 题目描述 如题,给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置. 为了减少骗分的情况,接下来还要输出子串的前缀数组next.如果 ...

  3. LCT总结——概念篇+洛谷P3690[模板]Link Cut Tree(动态树)(LCT,Splay)

    为了优化体验(其实是强迫症),蒟蒻把总结拆成了两篇,方便不同学习阶段的Dalao们切换. LCT总结--应用篇戳这里 概念.性质简述 首先介绍一下链剖分的概念(感谢laofu的讲课) 链剖分,是指一类 ...

  4. 【AC自动机】洛谷三道模板题

    [题目链接] https://www.luogu.org/problem/P3808 [题意] 给定n个模式串和1个文本串,求有多少个模式串在文本串里出现过. [题解] 不再介绍基础知识了,就是裸的模 ...

  5. 洛谷-P5357-【模板】AC自动机(二次加强版)

    题目传送门 -------------------------------------- 过年在家无聊补一下这周做的几道AC自动机的模板题 sol:AC自动机,还是要解决跳fail边产生的重复访问,但 ...

  6. 洛谷.1919.[模板]A*B Problem升级版(FFT)

    题目链接:洛谷.BZOJ2179 //将乘数拆成 a0*10^n + a1*10^(n-1) + ... + a_n-1的形式 //可以发现多项式乘法就模拟了竖式乘法 所以用FFT即可 注意处理进位 ...

  7. 洛谷.3803.[模板]多项式乘法(FFT)

    题目链接:洛谷.LOJ. FFT相关:快速傅里叶变换(FFT)详解.FFT总结.从多项式乘法到快速傅里叶变换. 5.4 又看了一遍,这个也不错. 2019.3.7 叕看了一遍,推荐这个. #inclu ...

  8. 洛谷.3803.[模板]多项式乘法(NTT)

    题目链接:洛谷.LOJ. 为什么和那些差那么多啊.. 在这里记一下原根 Definition 阶 若\(a,p\)互质,且\(p>1\),我们称使\(a^n\equiv 1\ (mod\ p)\ ...

  9. 洛谷P3385 [模板]负环 [SPFA]

    题目传送门 题目描述 暴力枚举/SPFA/Bellman-ford/奇怪的贪心/超神搜索 输入输出格式 输入格式: 第一行一个正整数T表示数据组数,对于每组数据: 第一行两个正整数N M,表示图有N个 ...

  10. [洛谷P3806] [模板] 点分治1

    洛谷 P3806 传送门 这个点分治都不用减掉子树里的了,直接搞就行了. 注意第63行 if(qu[k]>=buf[j]) 不能不写,也不能写成>. 因为这个WA了半天...... 如果m ...

随机推荐

  1. .NET CORE上传文件到码云仓库【搭建自己的图床】

    .NET CORE上传文件到码云仓库[搭建自己的图床] 先建一个公共仓库(随意提交一个README文件或者.gitignore文件保证master分支的存在),然后到gitee的个人设置页面找到[私人 ...

  2. R语言矩阵

    矩阵是元素布置成二维矩形布局的R对象. 它们包含相同原子类型的元素. R创建矩阵的语法: matrix(data, nrow, ncol, byrow, dimnames) 参数说明: data - ...

  3. 【bfs基础】①

    bfs,即广度优先搜索,主要通过队列(queue)进行操作. 稍微解释一下,队列是一种基础数据结构,其形态类似于一支长长的队伍,大概如下: 在C++中,队列的头文件定义为:#include<qu ...

  4. C++内存泄漏及检测工具详解

    #include "stdafx.h" #ifdef _DEBUG #define DEBUG_CLIENTBLOCK new( _CLIENT_BLOCK, __FILE__, ...

  5. YuniKorn 介绍

    一.YuniKorn 简介 YuniKorn 是一种轻量级的通用资源调度程序,适用于容器编排系统.它的创建是为了一方面在大规模,多租户环境中有效地实现各种工作负载的细粒度资源共享,另一方面可以动态地创 ...

  6. FC游戏修改教程(hack)小白文。

    FC(NES)红白机Family Computer(简称FAMICOM)(或Nintendo Entertainment System)是任天堂公司发行的第一代家用游戏机. 修改FC游戏需要的工具有 ...

  7. Excel导出功能超时解决方案 -- 异步处理

    背景 有运营同学反馈,最近导出excel会出现超时的情况,初步判断是数据增长太快,数据量太大,请求时间超过了设置的超时时间 尝试 有考虑直接更改该请求的超时时长,可是治标不治本 网上搜索发现,有很多人 ...

  8. Excel催化剂开源第1波-自定义函数的源代码全公开

    Excel催化剂插件从2018年1月1日开始运营,到今天刚好一周年,在过去一年时间里,感谢社区里的许多友人们的关心和鼓励,得以坚持下来,并收获一定的用户量和粉丝数和少量的经济收入回报和个人知名度的提升 ...

  9. C#3.0新增功能02 匿名类型

    连载目录    [已更新最新开发文章,点击查看详细] 匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型. 类型名由编译器生成,并且不能在源代码级使用. 每 ...

  10. 关于Object.defineProperty 的基础知识

    Object.defineProperty 这个方法大家耳熟能详,可以对 对象的属性进行添加或修改的操作.即可以进行  数据劫持 .vue就是通过这个方法来劫持数据的. 平时我们创建对象的时候,一般通 ...