关于排序

· · 算法·理论

众所周知,排序在一些算法中出现,甚至直接影响代码的时间,今天我们来看看几种常见的排序。

时间复杂度为 O(n+m) 的算法。

1. 计数排序(不常用):

通过“票箱”排序,如下列代码(就是将对应数放进对应数组内,数组就是“票箱”):

#include<bits/stdc++.h>
using namespace std;
int n,m;
int main()
{
    cin>>n>>m;//n表示选票的个数,m表示候选人的个数
    int a[n+1]={};
    for(int i=1;i<=n;i++)
    {
        int tmp;
        cin>>tmp;
        a[tmp]++;
    }
    for(int i=1;i<=m;i++)
    {
        for(int j=1;j<=a[i];j++)
        {
            cout<<j<<" ";
        }
    }
    return 0;
}

在这里数组是“票箱”,每读入一个数对应的票箱就加一,然后按照数量输出。

这个排序的方法适合在数据较小时使用,时间复杂度为:O(n+m)

适合题目类型:

数据较小时可用,虽然时间复杂度还可以。

相对题目:

P1271 选举学生会

时间复杂度为 O(n^{2}) 的算法(一般来说不常用,容易 TLE)。

2. 选择排序:

如图:

选择排序思路

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

稳定性:

举个例子,序列 5 8 5 2 9,我们知道第一遍选择第 1 个元素 5 会和 2 交换,那么原序列中两个 5 的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。

排序代码:

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int n;
    cin>>n;
    int a[n+1];
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=i+1;j<=n;j++)
        {
            if(a[j]<a[i]) swap(a[i],a[j]);
        }
    }
    for(int i=1;i<=n;i++) cout<<a[i]<<" ";
    return 0;
}

每次交换后都会一部分有已经排序,不需要再改变,时间复杂度: O(n^{2})

适合: 数据较小题目,否则容易爆时间复杂度。

不适合题目(会TLE):

P1177 【模板】排序

参考文献:

选择排序(计算机、数学术语) - 百度百科

3. 冒泡排序

如图:

算法剖析:

假定序列中有 n 个数,要进行从小到大的排序。若参与排序的数组元素共有 n 个,则需要 n-1 轮排序。在第 i 轮排序中,从左端开始,相邻两数比较大小,若反序则将两者交换位置,直到比较第 n+1-i 个数为止。第 1 个数与第 2 个数比较,第 2 个数和第 3 个数比较,一直到第 n-i 个数与第 n+1-i 个数比较,一共处理 n-i 次。此时,第 n+1-i 个位置上的数已经有序,后续就不需要参加以后的排序。

实现方法:

(1)第 1 轮冒泡排序先从第 1 个数和第 2 个数开始比较,若第 1 个数大于第 2 个数,则需要交换两者的位置;否则保持不变。重复这一过程,直到处理完本轮数列中最后两个数。

(2)第 2 轮冒泡排序与第1轮冒泡排序进行相同的排序,使大的数交换到 n-2 的位置上。

(3)重复以上过程,共需经过 n-1 轮冒泡排序后,数据实现升序排序。 算法示例:

对于序列(26282411),采用非递减规则进行排序,排序过程如下所示。

(1) 比较相邻的元素。如果第一个比第二个大,就交换他们两个。

(2) 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。

(3) 针对所有的元素重复以上的步骤,除了最后一个。

(4) 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

算法分析:

冒泡排序总的平均时间复杂度为 O(n^2)

算法稳定性:

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

排序代码:

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int n;
    cin>>n;
    int a[n+1];
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<n;i++)
    {
        for(int j=1;j<=n-i;j++)
        {
            if(a[j]>a[j+1]) swap(a[j],a[j+1]);
        }
    }
    for(int i=1;i<=n;i++) cout<<a[i]<<" ";
    return 0;
}

时间复杂度 O(n^{2})

适合: 数据较小题目,否则容易爆时间复杂度。

不适合题目(会TLE):

P1177 【模板】排序

参考文献:

冒泡排序(计算机科学领域的排序算法)_百度百科

4. 插入排序:

如图:

算法介绍:

插入排序‌是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上只需用到 O(1) 的额外空间的排序,因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

插入排序的时间复杂度:

‌最好情况‌:当输入数组已经是有序的时候,插入排序只需要进行 n-1 次比较。

‌最坏情况‌:当输入数组是逆序的时候,需要进行 \frac{n(n-1)}{2} 次比较和交换。 ‌平均情况‌:插入排序的平均时间复杂度为 O(n^2)

插入排序的稳定性:

插入排序是稳定的排序算法。如果待排序的序列中存在两个或两个以上具有相同关键字的记录,由于相等元素在排序前后位置不变,所以记录的相对次序保持不变,因此插入排序是稳定的。

插入排序的应用场景:

插入排序适用于数据量较小且部分有序的数据集。对于大规模数据集,插入排序的效率较低。通常在数据规模大于 1000 时,不建议使用插入排序。

代码:

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int n;
    cin>>n;
    int a[n+10];
    for(int i=0;i<n;i++) cin>>a[i];
    for(int i=1;i<n;i++)
    {
        int now=a[i],j=0;
        for(int j=i-1;j>=0;j--)
        {
            if(a[j]>now) {
                a[j+1]=a[j];
                a[j]=now;
            }
            else break;
        }
}
    for(int i=0;i<n;i++) cout<<a[i]<<" ";
    return 0;
}

适合: 数据较小题目,否则容易爆时间复杂度。

不适合题目(会TLE):

P1177 【模板】排序

关于插入排序参考文献:

插入排序_百度百科

插入排序 - TianJiankun - 博客园

时间复杂度为 O(n\log n) 的算法(基本什么排序都适合)。

5. 快速排序:

结合选择排序和插入排序的优点。

代码:

#include<bits/stdc++.h>
using namespace std;
void _sort(int a[],int l, int r)
{
    int i=l,j=r,flag=a[(l+r)/2],tmp;
    do{
        while(a[i]<flag) i++;
        while(a[j]>flag) j--;
        if(i<=j){
            swap(a[i],a[j]);
            i++;j--;
        }
    }while(i<=j);
    if(l<j) _sort(a,l,j);
    if(i<r) _sort(a,i,r);
}
int main()

{
    int n;
    cin>>n;
    int a[n+10];
    for(int i=1;i<=n;i++) cin>>a[i];
    _sort(a,1,n);
    for(int i=1;i<=n;i++) cout<<a[i]<<" ";
    return 0;
}

适合题目:

P1177 【模板】排序

sort 函数与快速排序不同之处

相信很多人都会说:“与这个快排代码性质相同的函数叫:sort。”

其实,他们并不一样:

sort 和快速排序是两种不同的排序算法,它们在实现方式、稳定性、稳定性、时间复杂度、性能和适用场景上有所不同。‌

实现方式

‌快速排序‌:基于分治思想,选择一个基准元素(也叫哨兵数),将数组分割成两个子数组,左边的子数组所有元素小于基准值,右边的子数组所有元素大于基准值,然后递归地对这两个子数组进行排序‌。

稳定性 ‌快速排序‌:快速排序是不稳定的排序算法。在排序过程中,相等的元素可能会因为位置交换而改变原有的顺序,因此快速排序不能保证元素的稳定性‌。 $‌sort‌$:$C++$ 的 $sort$ 函数在默认情况下使用快速排序,但由于快速排序的不稳定性,$sort$函数在某些情况下会使用插入排序来保证稳定性。因此,$sort$ 函数在某些情况下是稳定的‌。 时间复杂度 ‌快速排序‌:快速排序的平均时间复杂度为 $O(n log n)$,但在最坏情况下(如数组已经有序)时间复杂度为 $O(n^2)$。快速排序的时间复杂度主要取决于基准元素的选择和递归深度‌。 ‌$sort‌$:$C++$ 的 $sort$ 函数通过混合使用不同的排序算法来优化性能。在大多数情况下,$sort$ 的时间复杂度也是 $O(n log n)$,但在某些特殊情况下可能会退化为 $O(n^2)‌$。 性能 ‌快速排序‌:平均时间复杂度为 $O(n log n)$,最坏情况下(如数组已经有序或近乎有序)时间复杂度为 $O(n^2)$,但通过合理选择基准元素可以避免最坏情况的发生‌。 ‌$sort‌$:平均时间复杂度为 $O(n log n)$,通过结合多种排序算法,避免了快速排序在最坏情况下的性能问题,整体性能更稳定‌。 适用场景 ‌快速排序‌:适用于数据量较大且不确定数据分布情况的场景,因为它在平均情况下的效率较高。 $‌sort‌$:适用于需要高性能排序且对性能、稳定性有较高要求的场景,特别是在处理大量数据时,能够自动选择最优的排序方法,提供更好的整体性能‌(因为 $sort$ 函数在必要时会使用插入排序来保证稳定性)。 综上所述,‌$sort$ 函数由于其自动优化和多种排序算法的结合,通常在大多数情况下提供更好的性能和稳定性‌。而快速排序则更适合于数据量较大但分布不确定的场景。在实际应用中,可以根据具体需求选择合适的排序算法。 关于 $sort$ 函数参考文献: [C++ sort函数详解](https://blog.csdn.net/VariatioZbw/article/details/125155432) [【手写快排与Sort】](https://blog.csdn.net/weixin_44853527/article/details/134128843) 相关排序题: [ 【深基9.例1】选举学生会](https://www.luogu.com.cn/problem/P1271) [ 【深基9.例4】求第 k 小的数](https://www.luogu.com.cn/problem/P1923) [ [NOIP2006 普及组] 明明的随机数](https://www.luogu.com.cn/problem/P1059) [ [NOIP2007 普及组] 奖学金](https://www.luogu.com.cn/problem/P1093) [ [USACO07DEC] Bookshelf B](https://www.luogu.com.cn/problem/P2676) [ 车厢重组](https://www.luogu.com.cn/problem/P1116) [生日](https://www.luogu.com.cn/problem/P1104) [ [NOIP1998 提高组] 拼数](https://www.luogu.com.cn/problem/P1012)