排序

基于比较的排序

冒泡排序

  • 基本思想:两个相邻元素相比,前一个比后一个大就交换,直到将最大的元素交换到末尾。重复进行n-1次这样的交换将可以把所有的元素排好。
  • 时间复杂度\(O(n^2)\)
  • 稳定性:稳定
  • 原地排序。
  • 实现

void bubble_sort(int l[], int n) {
    for(int i=0;i<n;i++) {
        for(int j=0;j<n-1-i;j++) {
            if(l[j]>l[j+1]) {
                int t=l[j];l[j]=l[j+1];l[j+1]=t;
            }
        }
    }
}

插入排序

  • 基本思想:把待排序的数组的首个元素插入到一个已经排好序的有序序列中的适当位置,得到一个新的有序序列和大小减少一的未排序数列,直到未排序数列大小为0就完成。
  • 时间复杂度\(O(n^2)\),越接近有序,时间效率越高。
  • 稳定性:稳定
  • 原地排序。
  • 实现

void insert_sort(int l[],int n) {
    for(int i=1;i<n;i++) {
        int t=l[i];
        for(int j=i-1;j>=0;j--) {
            if(l[j]>t) {
                l[j+1]=l[j];
            } else break;
        }
        l[j+1]=t;
    }
}

希尔排序

  • 希尔排序(英语:Shell sort),也称为缩小增量排序法,是 插入排序 的一种改进版本。
  • 排序对不相邻的记录进行比较和移动:
    1. 将待排序序列分为若干子序列(每个子序列的元素在原始数组中间距相同);
    2. 对这些子序列进行插入排序;
    3. 减小每个子序列中元素之间的间距,重复上述过程直至间距减少为1.
  • 稳定性:不稳定
  • 时间复杂度与间距数列有关
void shellsort(vector<int>& v,int n) {
    int h=n;
    while(h>=1) {
        for(int i=h;i<n;i++) {
            for(int j=i;j>=h && v[j] < v[j-h];j-=h) {
                int t = v[j];
                v[j] = v[j-h];
                v[j-h] = t;
            }
        }
        h/=2;
    }
}

选择排序

  • 基本思想:从待排序的数据元素中选出最小的一个元素,存放在序列的起始位置。重复此过程至数组排序完毕。
  • 时间复杂度\(O(n^2)\)
  • 稳定性:不稳定
  • 原地排序。
  • 实现
void select_sort(int l[],int n) {
    for(int i=0;i<n;i++) {
        int m=l[i];int mi=i;
        for(int j=i;j<n;j++) {
            if(l[j]<m) {
                m=l[j];mi=j;
            }
        }
        int t=l[i];
        l[i]=l[mi];
        l[mi] = t;
    }
}

归并排序

  • 基本思想:将未排序数组不断分成两个未排序数组,将这两个数组排好序后,再合并起来得到一个新的已排序数组。
  • 时间复杂度:\(O(nlogn)\)
  • 稳定性:稳定
  • 非原地排序

//使用双指针递归实现
void mergesort(int p[],int l,int r) {
    if(l>=r) return;
    int mid=(l+r)>>1;
    mergesort(p,l,mid);
    mergesort(p,mid+1,r);
    int ll[r-l+1];
    int i=l,j=mid+1;int pos=0;
    while(i<=mid && j<=r) {
        if(p[i]<p[j]) ll[pos++]=p[i++];
        else ll[pos++]=p[j++];
    }

    while(i<=mid) ll[pos++]=p[i++];
    while(j<=r) ll[pos++]=p[j++];
    int k=0;
    for(int g=l;g<=r;g++) {
        p[g]=ll[k++];
    }
}

快速排序

  • 基本思想:每轮排序选取一个基准值分区,该基准值左边的元素均小于基准值,右边的元素均大于基准值,一直重复分区操作,直到排完序。
  • 分区:运用双指针的的思想,左指针从左边开始扫描,扫描到不符合条件的停下,右指针从右边开始扫面,不符合条件就停下,都停下后交换,交换后继续扫描重复该过程,直到右指针到左指针的左边。
  • 时间复杂度:\(O(nlog n)\)
  • 稳定性:不稳定
  • 原地排序
void quicksort(int p[],int l, int r) {
    if(l>=r) return;
    int t = p[l+r>>1];
    int i=l-1,j=r+1;
    while(i<j) {
        while(p[++i]<t);
        while(p[--j]>t);
        if(i<j) {
            int tmp=p[i];
            p[i]=p[j];
            p[j]=tmp;
        }
    }
    quicksort(p,l,j);
    quicksort(p,j+1,r);
}

堆排序

  • 利用最大/最小二叉堆数据结构进行排序,
  • 时间复杂度\(O(n \space log n)\)
  • 简单实现:将数组元素全部加入堆中,逐个弹出即可,空间复杂度\(O(n)\)
  • 就地实现:
    • 将原数组当成未构建的最大二叉堆(完全二叉树的线性表示),从后往前构建二叉堆后,逐步将堆顶弹出与数组末尾元素交换,然后减少堆的大小,维护二叉堆性质,直至二叉堆大小为一。空间复杂度\(O(1)\)
  • 堆排序是不稳定的排序算法
// 就地堆排序实现
#include<bits/stdc++.h>
using namespace std;
int n;
vector<int> v;

// 以k为根节点构建最大堆
void heapify(int size,int k)
{
    int largest = k;
    int left = 2*k+1;
    int right = 2*k+2;
    if(left < size && v[left] > v[largest]) {
        largest = left;
    }
    if(right < size && v[right] > v[largest]) {
        largest = right;
    }
    
    if(largest != k) {
        int t = v[k];
        v[k] = v[largest];
        v[largest] = t;
        heapify(size,largest);
    }
}

int main() {
    cin>>n;
    v.resize(n);
    for(int i=0;i<n;i++) cin>>v[i];
    // 从后(最后一个非叶子节点)往前构建最大堆
    for(int i=(v.size()-1)/2;i>=0;i--) {
        heapify(n,i);
    }
    // 输出最大堆
    for(int i=0;i<n;i++) cout<<v[i]<<" ";
    cout<<"\n";
    // 进行堆的弹出,交换,维护
    int size = n;
    while(size>1) {
        int t = v[size-1];
        v[size-1] = v[0];
        v[0] = t;
        heapify(--size,0);
    }
    // 输出最后结果
    for(int i=0;i<n;i++) cout<<v[i]<<" ";
    return 0;
}

基于比较排序的边界问题

对于任意一个\(N\)个数字组成的排列,一共有 \(N!\) 种情况,而每次比较需要比较两个数,得到两种不同的结果,这意味着对应的决策树每个节点最多有两个子节点,于是对应的决策树一共有 \(log_2(N!)\) 层,所以排序至少要经历\(log_2(N!)\)次才能得出答案,故任何比较算法的渐进下界为 \(log(N!)=\Theta(NlogN)\)

线性时间排序

桶排序

  • 基本思想:设置一定数量的桶,每个桶存储一定区间内的元素,(各个桶对应的区间递增),将数组的元素存放到对应的桶中,对每个桶分别排序,依次将每个桶中的元素放到新数组末尾即可。
  • 时间复杂度:\(O(n+k)\)\(k\)为桶的个数
  • 稳定性:稳定
  • 例子:洛谷P1059(桶排序)
#include<bits/stdc++.h>
using namespace std;

int b[1001];
int main() {
    int m;cin>>m;
    for(int i=0;i<m;i++) {
        int a;cin>>a;
        if(!b[a]) b[a]=1;
    }

    int c=0;vector<int> v;
    for(int i=0;i<1001;i++){
        if(b[i]){
            c++;v.push_back(i);
        }
    }
    cout<<c<<endl;
    for(int i:v){cout<<i<<" ";}
}

计数排序(Counting Sort)

  • 基本思想:扫描一遍数组,记录数组中每个元素出现的次数,\(cnt[i]\)。对 \(cnt\) 数组求前缀和,则 \(cnt[i]\) 表示原数组中小于等于 \(i\) 的元素数量。 则对于原数组的元素\(a[i]\) 在排序后数组的位置为 \(cnt[a[i]]\)
  • 从后往前将原数组的元素放到新数组中,放入后 cnt[a[i]]-=1
  • 从后往前的原因:保证排序的稳定性
  • 时间复杂度:\(O(n+k)\)\(k\) 为数组元素取值范围
void counting_sort(vector<int>& a) {
    int mxa=0;
    int sz=a.size();
    for(int &i:a) {
        mxa = max(mxa,i);
    }
    vector<int> cnt(mxa+1,0);
    vector<int> ans(n);
    for(int &i:a) {
        cnt[i]++;
    }
    for(int i=1;i<cnt.size();i++) {
        cnt[i]+=cnt[i-1];
    }
    for(int i=sz-1;i>=0;i--) {
        ans[cnt[a[i]]-1]=a[i];
        cnt[a[i]]--;
    }
    a=ans;   
}

基数排序(Radix Sort)

  • 基本思想:将数组中整数的每一位数从小到大排序,即可得到排序数组。
  • 时间复杂度:\(O(nk)\)\(k\)为最长位数。
  • 稳定性:稳定
void radixsort(int p[],int n) {
    int maxm=p[0];
    for(int i=0;i<n;i++) {
        maxm = p[i]>maxm?p[i]:maxm;
    }
    int md=0;
    while(maxm>0){
        md++;maxm/=10;
    }
    int dev=1; vector<vector<int>> mcount(10);
    for(int i=0;i<md;i++) {
        for(int j=0;j<n;j++) {
            int radix = (p[j]/dev)%10;
            mcount[radix].push_back(p[j]);
        }
        int pos=0;
        for(int k=0;k<10;k++) {
            for(int l=0;l<mcount[k].size();l++){
                p[pos++]=mcount[k][l];
            }
            //记得清空
            mcount[k].clear();
        }
        //记得更新分割数
        dev*=10;
    }
}
posted @ 2026-01-07 17:14  NightRainLone  阅读(3)  评论(0)    收藏  举报