本页目录

排序算法总结与代码实现

汇总&前言

以下表格中讨论的时空复杂度都是每种算法未加优化、最朴素的实现。
是否稳定一栏,空白默认为稳定。

算法名称平均时间最好情况最坏情况辅助空间是否稳定

基于
比较

冒泡排序

选择排序

插入排序

希尔排序

归并排序

快速排序

堆排序

不基于
比较

计数排序

桶排序/插入

桶排序/归并

基数排序

注:

希尔排序的时间复杂度与增量序列有关,表格给出的只是一组较为常见的数据。

计数排序中的为数据最大值与最小值之差。

桶排序中的为桶的数量,对于最好情况,元素在桶中均匀分布且当近似等于元素数量时,桶排序时间复杂度是线性的;对于最坏情况,大多数元素都分布在一个桶里,此时桶排序时间复杂度取决于桶内排序算法。如果桶内排序使用稳定排序算法,则桶排序也是稳定的。

基数排序中的为数据的位数,当较小时,基数排序时间复杂度是线性的。

本文会给出所有排序算法(从小到大升序排列)的代码实现,代码可以直接提交到洛谷 - P1177【模板】排序,其中的sortArray函数可以提交到力扣 - 912.排序数组

注意两个网站的数据和环境都不一样,二者的时间横向比较没有可比性。

冒泡排序

每次把最大的元素冒泡到最后一位。

#include <iostream>
#include <vector>
using namespace std;
vector<int> sortArray(vector<int> &nums)
{
    for(int end=nums.size()-1;end>0;end--)
    {
        for(int i=0;i<end;i++)
        {
            if(nums[i+1]<nums[i])
            {
                int tmp=nums[i];
                nums[i]=nums[i+1];
                nums[i+1]=tmp;
            }
        }
    }
    return nums;
}
int main()
{
    ios::sync_with_stdio(false);
    int n;
    cin>>n;
    vector<int> nums(n);
    for(int i=0;i<n;i++)
    {
        cin>>nums[i];
    }
    sortArray(nums);
    for(int i=0;i<n;i++)
    {
        cout<<nums[i]<<' ';
    }
    return 0;
}

测试结果:洛谷通过1/5;力扣通过10/21

优化

如果一次冒泡过程中没有发生交换,说明数组已经有序,可以提前结束排序。这样最好情况下时间复杂度是

选择排序

每次从未排序序列中找出最小的元素,放到已排序序列的末尾。

#include <iostream>
#include <vector>
using namespace std;
vector<int> sortArray(vector<int> &nums)
{
    for(int i=0;i<nums.size();i++)
    {
        int min_id=i;
        for(int j=i+1;j<nums.size();j++)
        {
            if(nums[j]<nums[min_id])
            {
                min_id=j;
            }
        }
        int tmp=nums[i];
        nums[i]=nums[min_id];
        nums[min_id]=tmp;
    }
    return nums;
}
int main()
{
    ios::sync_with_stdio(false);
    int n;
    cin>>n;
    vector<int> nums(n);
    for(int i=0;i<n;i++)
    {
        cin>>nums[i];
    }
    sortArray(nums);
    for(int i=0;i<n;i++)
    {
        cout<<nums[i]<<' ';
    }
    return 0;
}

测试结果:洛谷通过1/5;力扣通过11/21

插入排序

每次把未排序序列的第一个元素,插入到已排序序列中。

#include <iostream>
#include <vector>
using namespace std;
vector<int> sortArray(vector<int> &nums)
{
    for(int i=0;i<nums.size();i++)
    {
        int j,k=nums[i];//未排序序列第一个元素
        for(j=i-1;j>=0&&nums[j]>k;j--)
        {
            nums[j+1]=nums[j];//大于k的数都右移一位
        }
        nums[j+1]=k;
    }
    return nums;
}
int main()
{
    ios::sync_with_stdio(false);
    int n;
    cin>>n;
    vector<int> nums(n);
    for(int i=0;i<n;i++)
    {
        cin>>nums[i];
    }
    sortArray(nums);
    for(int i=0;i<n;i++)
    {
        cout<<nums[i]<<' ';
    }
    return 0;
}

测试结果:洛谷通过3/5;力扣通过16/21

在数组基本有序的情况下,插入排序的效率是非常高的。

优化

可以使用二分查找优化插入排序的查找过程,不过此时平均时间复杂度仍然是,因为平均情况下仍然会涉及到元素的移动。此优化只是常数优化。

希尔排序

希尔排序每次将相隔gap的元素分为一组,对每一组进行插入排序,然后逐渐减半gap的值,直到gap=1

#include <iostream>
#include <vector>
using namespace std;
vector<int> sortArray(vector<int> &nums)
{
    for(int gap=nums.size()/2;gap>0;gap>>=1)
    {
        for(int i=gap;i<nums.size();i++)
        {
            int j,k=nums[i];
            for(j=i-gap;j>=0&&nums[j]>k;j-=gap)
            {
                nums[j+gap]=nums[j];//大于k的数都右移gap位
            }
            nums[j+gap]=k;
        }
    }
    return nums;
}
int main()
{
    ios::sync_with_stdio(false);
    int n;
    cin>>n;
    vector<int> nums(n);
    for(int i=0;i<n;i++)
    {
        cin>>nums[i];
    }
    sortArray(nums);
    for(int i=0;i<n;i++)
    {
        cout<<nums[i]<<' ';
    }
    return 0;
}

测试结果:洛谷AC/73ms;力扣AC/115ms

归并排序

归并排序是一种分治算法,将数组分为两部分,分别排序;两个有序数组可以在时间内合并为一个有序数组。

#include <iostream>
#include <vector>
using namespace std;
void mergeSort(vector<int> &nums,vector<int> &aux,int l,int r)//排序[l,r]
{
    if(l>=r) return;
    int m=l+(r-l)/2;
    mergeSort(nums,aux,l,m);
    mergeSort(nums,aux,m+1,r);
    //合并[l,m]和[m+1,r]
    int i=l,j=m+1;
    for(int k=l;k<=r;k++)//先在辅助空间中构造出合并后的序列
    {
        if(i>m)                 aux[k]=nums[j],j++;
        else if(j>r)            aux[k]=nums[i],i++;
        else if(nums[i]>nums[j])aux[k]=nums[j],j++;
        else                    aux[k]=nums[i],i++;
    }
    for(int k=l;k<=r;k++)//在辅助空间合并后复制给nums
    {
        nums[k]=aux[k];
    }
    return;
}
vector<int> sortArray(vector<int> &nums)
{
    vector<int> aux=nums;//辅助空间
    mergeSort(nums,aux,0,nums.size()-1);
    return nums;
}
int main()
{
    ios::sync_with_stdio(false);
    int n;
    cin>>n;
    vector<int> nums(n);
    for(int i=0;i<n;i++)
    {
        cin>>nums[i];
    }
    sortArray(nums);
    for(int i=0;i<n;i++)
    {
        cout<<nums[i]<<' ';
    }
    return 0;
}

测试结果:洛谷AC/72ms;力扣AC/130ms

快速排序

快速排序也是一种分治算法,每次选择一个基准元素,将小于基准的元素放在基准的左边,大于基准的元素放在基准的右边。

#include <iostream>
#include <vector>
using namespace std;
void quickSort(vector<int> &nums,int l,int r)
{
    if(l>=r) return;
    int lp=l,rp=l,tmp;//lp左侧的全部元素都小于主元
    for(;rp<r;rp++)
    {
        if(nums[rp]<nums[r])//选择nums[r]作为主元
        {
            tmp=nums[rp],nums[rp]=nums[lp],nums[lp]=tmp;
            lp++;
        }
    }
    tmp=nums[lp],nums[lp]=nums[r],nums[r]=tmp;
    quickSort(nums,l,lp-1);
    quickSort(nums,lp+1,r);
    return;
}
vector<int> sortArray(vector<int> &nums)
{
    quickSort(nums,0,nums.size()-1);
    return nums;
}
int main()
{
    ios::sync_with_stdio(false);
    int n;
    cin>>n;
    vector<int> nums(n);
    for(int i=0;i<n;i++)
    {
        cin>>nums[i];
    }
    sortArray(nums);
    for(int i=0;i<n;i++)
    {
        cout<<nums[i]<<' ';
    }
    return 0;
}

测试结果:洛谷通过2/5;力扣通过11/21

没有优化的快排被基本有序的数组和元素全部相同的数组卡掉了。

堆排序

堆排序是一种选择排序。利用最大堆,每次读取堆顶(最大元素),然后以时间代价删除堆顶。

#include <iostream>
#include <vector>
using namespace std;
//如果越界返回下标0,nums[0]会被赋值成负无穷作为哨兵节点
int left(int i,int heapsize){return i*2<=heapsize?i*2:0;}
int right(int i,int heapsize){return i*2+1<=heapsize?i*2+1:0;}
void swap(vector<int> &nums,int i,int j)
{
    int tmp=nums[i];
    nums[i]=nums[j];
    nums[j]=tmp;
    return;
}
//调用maxheapify时,i的左右子树都应该为最大堆
void maxheapify(vector<int> &nums,int heapsize,int i)
{
    int l=left(i,heapsize),r=right(i,heapsize);
    int rt=nums[i],lchild=nums[l],rchild=nums[r];
    if(rt>=lchild&&rt>=rchild) return;//根节点已经最大,返回
    else if(lchild>rchild)
    {
        swap(nums,l,i);
        maxheapify(nums,heapsize,l);
    }
    else
    {
        swap(nums,r,i);
        maxheapify(nums,heapsize,r);
    }
    return;
}
vector<int> sortArray(vector<int> &nums)
{
    int heapsize=nums.size();//堆中元素从1开始编号
    nums.emplace(nums.begin(),-2147483647);//nums[0]作为哨兵
    //建堆
    //heapsize>>1是堆的第一个非叶子节点,从它开始向前依次调用一次maxheapify
    for(int i=heapsize>>1;i>0;i--)
    {
        maxheapify(nums,heapsize,i);
    }
    //排序
    while(heapsize)
    {
        swap(nums,1,heapsize);//将最大的元素换到最后
        heapsize--;
        maxheapify(nums,heapsize,1);
    }
    nums.erase(nums.begin());//移除哨兵
    return nums;
}
int main()
{
    ios::sync_with_stdio(false);
    int n;
    cin>>n;
    vector<int> nums(n);
    for(int i=0;i<n;i++)
    {
        cin>>nums[i];
    }
    sortArray(nums);
    for(int i=0;i<n;i++)
    {
        cout<<nums[i]<<' ';
    }
    return 0;
}

测试结果:洛谷AC/87ms;力扣AC/136ms

计数排序

计数排序是一种非比较排序,统计每个元素出现的次数,然后依次输出。

#include <iostream>
#include <vector>
using namespace std;
vector<int> sortArray(vector<int> &nums)
{
    int mx=-2147483647,mi=-mx;
    for(auto x:nums)
    {
        if(x>mx) mx=x;
        if(x<mi) mi=x;
    }
    vector<int> aux(mx-mi+1);
    for(auto x:nums)
    {
        aux[x-mi]++;
    }
    int n=0;
    for(int i=0;i<aux.size();i++)
    {
        for(int j=0;j<aux[i];j++)
        {
            nums[n]=i+mi;
            n++;
        }
    }
    return nums;
}
int main()
{
    ios::sync_with_stdio(false);
    int n;
    cin>>n;
    vector<int> nums(n);
    for(int i=0;i<n;i++)
    {
        cin>>nums[i];
    }
    sortArray(nums);
    for(int i=0;i<n;i++)
    {
        cout<<nums[i]<<' ';
    }
    return 0;
}

测试结果:洛谷通过3/5;力扣AC/76ms

洛谷有两组数据极差较大,因此有两个测试点MLE;力扣数据范围小因此可以通过。计数排序时间效率是非常快的。

关于空间复杂度

在对整数排序的例子中,显然只需要的额外复杂度;不过考虑对结构体数组按键进行排序,此时就不能简单的用一个整数表示“该键出现了多少次”,需要额外的空间存储其他信息,比如结构体的地址,此时整体空间复杂度是的。

桶排序

桶排序是计数排序的推广,将元素分到不同的桶中,然后对每个桶进行排序(这里可以用插入、快排、归并等等),最后依次输出。

下面的代码实现就用了最朴素的插入排序:

#include <iostream>
#include <vector>
using namespace std;
vector<int> sortBucket(vector<int> &nums)
{
    for(int i=0;i<nums.size();i++)
    {
        int j,k=nums[i];//未排序序列第一个元素
        for(j=i-1;j>=0&&nums[j]>k;j--)
        {
            nums[j+1]=nums[j];//大于k的数都右移一位
        }
        nums[j+1]=k;
    }
    return nums;
}
vector<int> sortArray(vector<int> &nums)
{
    int k=100;//桶的大小
    int mx=nums[0],mi=nums[0];
    for(auto x:nums)
    {
        if(x>mx) mx=x;
        if(x<mi) mi=x;
    }
    vector<vector<int>> buckets((mx-mi)/k+1);
    for(auto x:nums)
    {
        buckets[(x-mi)/k].emplace_back(x);
    }
    for(int i=0;i<buckets.size();i++)
    {
        sortBucket(buckets[i]);
    }
    nums.clear();
    for(auto bucket:buckets)
    {
        nums.insert(nums.end(),bucket.begin(),bucket.end());
    }
    return nums;
}
int main()
{
    ios::sync_with_stdio(false);
    int n;
    cin>>n;
    vector<int> nums(n);
    for(int i=0;i<n;i++)
    {
        cin>>nums[i];
    }
    sortArray(nums);
    for(int i=0;i<n;i++)
    {
        cout<<nums[i]<<' ';
    }
    return 0;
}

测试结果:洛谷AC/402ms;力扣AC/83ms

注:可能因为STL容器常数较大,在洛谷只是初始化buckets就花费了较长时间(前两个元素范围较大的测试点约100ms)。

基数排序

基数排序从低位到高位依次对每一位进行计数排序,最后得到有序序列。

#include <iostream>
#include <vector>
using namespace std;
vector<int> sortArray(vector<int> &nums)
{
    int mx=nums[0],mi=nums[0];
    for(auto x:nums)
    {
        if(x>mx) mx=x;
        if(x<mi) mi=x;
    }
    for(int i=0;i<nums.size();i++) nums[i]-=mi;//减掉最小值,这样可以排序负数
    mx-=mi;
    vector<vector<int>> dgt(10);//每趟排序对一位进行计数排序
    for(int exp=1;mx/exp>0;exp*=10)
    {
        for(auto x:nums)
        {
            dgt[(x/exp)%10].emplace_back(x);
        }
        nums.clear();
        for(int i=0;i<10;i++)
        {
            nums.insert(nums.end(),dgt[i].begin(),dgt[i].end());
            dgt[i].clear();
        }
    }
    for(int i=0;i<nums.size();i++) nums[i]+=mi;//还原
    return nums;
}
int main()
{
    ios::sync_with_stdio(false);
    int n;
    cin>>n;
    vector<int> nums(n);
    for(int i=0;i<n;i++)
    {
        cin>>nums[i];
    }
    sortArray(nums);
    for(int i=0;i<n;i++)
    {
        cout<<nums[i]<<' ';
    }
    return 0;
}

测试结果:洛谷AC/67ms;力扣AC/83ms