洛谷 P1177:【模板】排序 ← 基数排序实现
【题目来源】
https://www.luogu.com.cn/problem/P1177
【题目描述】
将读入的 N 个数从小到大排序后输出。
【输入格式】
第一行为一个正整数 N。
第二行包含 N 个空格隔开的正整数 ai,为你需要进行排序的数。
【输出格式】
将给定的 N 个数从小到大输出,数之间空格隔开,行末换行且无空格。
【输入样例】
5
4 2 4 5 1
【输出样例】
1 2 4 4 5
【说明/提示】
对于 20% 的数据,有 1≤N≤10^3;
对于 100% 的数据,有 1≤N≤10^5,1≤ai≤10^9。
【算法分析】
● 基数排序(Radix Sort)是一种非比较型整数排序算法,通过逐位分配和收集元素实现排序。基数排序仅适用于整数或可转换为数字的字符串的排序。其核心原理是将整数按位数切割,从最低位(LSD)或最高位(MSD)开始依次排序,利用桶排序或计数排序作为子过程。基数排序是倍增法实现后缀数组(Suffix Array)的核心排序工具,其高效的 O(n) 时间复杂度直接决定了后缀数组整体算法的时间复杂度。
● 基数排序的步骤
(一)预处理阶段:计算数组中元素的最大位数,确定基数排序的轮数。
int maxbit(int n,int a[]) {int imax=0;for(int i=1; i<=n; i++) {int cnt=0, t=a[i];while(t!=0) {t=t/10;cnt++;}imax=max(imax,cnt);}return imax;
}
(二)按位排序阶段(从最低位到最高位)
(1)统计频次:遍历数组 a[],统计当前轮按位排序时当前位上每个数字(0-9)的出现次数,存储于频次数组 freq[] 中。
for(int i=0; i<=9; i++) freq[i]=0;
for(int i=1; i<=n; i++) {int x=(a[i]/div)%10;freq[x]++;
}
(2)计算前缀和:对频次数组 freq[] 做前缀和 freq[i]+=freq[i-1],i∈[1,9]。其中,freq[0] 的值为上步所得。此时,若有 freq[i]=x,则表示在按位排序的当前轮执行后,数组 a[] 中的各元素在当前位小于等于 i 的数有 x 个。在基数排序中,计算前缀和的主要目的是为了快速确定数组 a[] 的元素在按位排序的当前轮执行后,它们在所得的有序序列中的位次。
for(int i=1; i<=9; i++) {freq[i]+=freq[i-1];
}
(3)分配位次:逆序遍历数组 a[],依据元素 a[i] 的当前轮当前位元素的频次,将元素 a[i] 放入临时数组 rk[] 的对应位置。逆序遍历数组 a[],是为了保持基数排序的稳定性。为了应对数组 a[] 中,多个元素在按位排序的当前轮的当前位均为相同的 t,则需在给一个 t 安置后,执行 freq[t]--,为下一个相同的 t 预留正确的位次。
for(int i=n; i>=1; i--) {int t=(a[i]/div)%10;rk[freq[t]]=a[i];freq[t]--;
}
(4)更新数组:将当前轮按位排序结果 rk[] 覆盖原数组 a[],为下轮排序做准备。
for(int i=1; i<=n; i++) {a[i]=rk[i];
}
【基数排序算法实例】
假设我们有一个待排序的数组:a=[712, 303, 4, 18, 89, 999, 70, 26]。
在预处理阶段,通过计算可得数组 a[] 中元素的最大位数为 3,也即基数排序的轮数。
下面各步骤,以基数排序的第一轮,也即以数组 a[] 中各元素按位排序时的个位为例,进行算法陈述。
步骤1:统计频次
首先创建一个频次数组 freq(长度为 10,对应数字 0~9),统计数字 0~9 出现的次数。显然,对于给定的数组 a=[712, 303, 4, 18, 89, 999, 70, 26],其频率数组 freq=[1, 0, 1, 1, 1, 0, 1, 0, 1, 2]。这个频率数组在基数排序的下一步中会转换为前缀和数组,快速确定数组 a[] 的元素在按位排序的当前轮执行后,它们在所得的有序序列中的位次,目的是实现稳定排序。
步骤2:计算前缀和
执行 for(int i=1; i<=9; i++) freq[i]+=freq[i-1]; 后,频次数组 freq 的值变为 [1, 1, 2, 3, 4, 4, 5, 5, 6, 8]。此时,频次数组 freq 的语义更新为前缀和数组 freq,其含义如下所示。
freq[0] = 1:表示所有小于等于 0 的数字有 1 个
freq[1] = 1:表示所有小于等于 1 的数字有 1 个
freq[2] = 2:表示所有小于等于 2 的数字有 2 个
freq[3] = 3:表示所有小于等于 3 的数字有 3 个
……
freq[8] = 6:表示所有小于等于 8 的数字有 6 个
freq[9] = 8:表示所有小于等于 9 的数字有 8 个
步骤3:分配位次
逆序遍历数组 a[],依据元素 a[i] 的当前轮当前位元素的频次,将元素 a[i] 放入临时数组 rk[] 的对应位置。
i=8:a[8]=26 → t=(26/1)%10=6 → rk[freq[6]]=rk[5]=26 → freq[6]减为4
i=7:a[7]=70 → t=(70/1)%10=0 → rk[freq[0]]=rk[1]=70 → freq[0]减为0
i=6:a[6]=999 → t=(999/1)%10=9 → rk[freq[9]]=rk[8]=999 → freq[9]减为7
i=5:a[5]=89 → t=(89/1)%10=9 → rk[freq[9]]=rk[7]=89 → freq[9]减为6
i=4:a[4]=18 → t=(18/1)%10=8 → rk[freq[8]]=rk[6]=18 → freq[8]减为5
i=3:a[3]=4 → t=(4/1)%10=4 → rk[freq[4]]=rk[4]=4 → freq[4]减为3
i=2:a[2]=303 → t=(303/1)%10=3 → rk[freq[3]]=rk[3]=303 → freq[3]减为2
i=1:a[1]=712 → t=(712/1)%10=2 → rk[freq[2]]=rk[2]=712 → freq[2]减为1
步骤4:更新数组
将当前轮按位排序结果 rk[] 覆盖原数组 a[],为下轮排序做准备。
此时 a[1]~a[8] 的值为 rk[1]~rk[8] 的值:70, 712, 303, 4, 26, 18, 89, 999。
显然,70, 712, 303, 4, 26, 18, 89, 999 是按个位从小到大排序后的结果,之后将以此数据为基础进行下一轮(十位)的按位排序。
【算法代码: 基数排序求解洛谷P1177的代码】
#include <bits/stdc++.h>
using namespace std;const int maxn=1e5+5;
int a[maxn];
int rk[maxn],freq[maxn];int maxbit(int n,int a[]) {int imax=0;for(int i=1; i<=n; i++) {int cnt=0, t=a[i];while(t!=0) {t=t/10;cnt++;}imax=max(imax,cnt);}return imax;
}void Radix_Sort(int n,int a[]) {int cnt=maxbit(n,a);int div=1;while(cnt--) {for(int i=0; i<=9; i++) {freq[i]=0;}for(int i=1; i<=n; i++) {int t=(a[i]/div)%10;freq[t]++;}for(int i=1; i<=9; i++) {freq[i]+=freq[i-1];}for(int i=n; i>=1; i--) {int t=(a[i]/div)%10;rk[freq[t]]=a[i];freq[t]--;}for(int i=1; i<=n; i++) {a[i]=rk[i];}div=div*10;}
}int main() {int n;cin>>n;for(int i=1; i<=n; i++) {cin>>a[i];}Radix_Sort(n,a);for(int i=1; i<=n; i++) {cout<<a[i]<<" ";}return 0;
}/*
in:
5
12345 12 123 1 12out:
1 12 12 123 12345
*/
【参考文献】
https://www.cnblogs.com/pdpdzaa/p/17522307.html
https://www.cnblogs.com/pdpdzaa/p/17532682.html
https://zhuanlan.zhihu.com/p/655379377
https://www.cnblogs.com/000zwx000/p/12330710.html
