讨论基数排序之前,先来看桶式排序
桶式排序
桶式排序的原理是:利用待排序序列元素的值作为新序列的索引进行插入,完成新序列的建立后,只需对新序列进行遍历即可完成排序(具体遍历过程和新序列元素的值有关)
假设有N个整数的序列,元素的值的范围是0到M-1。现建立一个名为Count的桶序列,长度为M,并初始化为0。于是,Count有M个桶,开始时都是空的。对待排序序列进行遍历,当遍历至原序列元素Ai时,Count[Ai]增1。Count序列建立完毕后,对其进行遍历,遍历输出的不是Count序列元素的值,而是其索引,每个索引输出的次数由其对应元素的值决定,元素值为0则不输出
代码如下
void bucket_sort(int a[], int n, int m){ //n为待排序序列长度,其元素值取范为[0, m-1]
assert(a != NULL);
assert(n > 0);
assert(m > 0);
int * b = (int *)malloc(m*sizeof(int));
memset(b, 0, m*sizeof(int));
for(int i = 0; i < n; i++) //入桶
b[a[i]]++;
for(int i = 0, j = 0; i < n; i++){ //出桶
while(b[j] == 0) j++;
a[i] = j;
b[j]--;
}
}
桶式排序的局限性在于
- 待排序序列元素的值只能为整数
- 若待排序序列元素值的取值范围很大,则Count序列需占用很大空间,而实际上Count序列可能是很稀疏的
将数值的每一位看作一个关键字,则同样可对数值应用基数排序。设待排序序列长度为N,取值范围为0到M,选取基数为R,则该序列中所有元素的“位数”(基数R对应的“位”,当基数为10时就是一般意义上的“位”)均不超过P = ceil(lnM/lnR) (对数换底公式)。整个基数排序需要R个高度为N的桶,需要P趟桶排序。每次以元素值的某一“位”为关键字进行桶排序,顺序必须是先从最低“位”开始桶排序,最后对最高“位”桶排序。因此基数排序实际上是反复“入桶”-“出桶”的过程
对于”先低位,后高位“原则,作如下理解:低位的排序结果会影响两个高位相同元素的排序。譬如21和27,第一趟排序时,21所在桶位于27所在桶之前,因此第二趟排序时会先扫描到21。最终结果是,第二趟排序结束后21在27之前
根据上述分析,相比于桶式排序,基数排序有以下特性
- 桶是一个二维矩阵,大小为R×N
- 桶式排序的Count序列是一维列表,这是因为同一桶内的元素的值必定相同;而基数排序的同一桶内的元素只是针对该趟对应的“位”相同,元素值不一定相同,因此桶还需要一定的“高度”来记录不同的元素值,因此是二维矩阵
- 理论上基数R可以任意取值的,但R的选取决定了桶排序的趟数(时间复杂度)和桶的大小(空间复杂度),因此需要一定的平衡。另外,虽然基数为10符合我们的一般思路易于理解,但基数为10时“取位”操作必须通过pow函数和取模运算进行,开销较大。基于上述考虑,我们选取基数为16,故而可使用移位操作进行“取位”
void radix_sort(int a[], int n, int m){ //max: m
assert(a != NULL);
assert(n > 0);
assert(m > 0);
int p = (int)ceil(log(m)/log(16)); //p表示以16为基时,数组元素最大值m对应的最高位数
printf("p = %d\n", p);
int ** b = (int **)malloc(16*sizeof(int *)); //二维数组b[][]表示桶,桶有一定高度
for(int i = 0; i < 16; i++)
b[i] = (int *)malloc(n*sizeof(int));
int c[16]; //数组c[]记录桶的高度
int index;
int mask = 0x0f;
for(int k = 0; k < p; k++){
memset(c, 0, 16*sizeof(int));
for(int j = 0; j < n; j++){ //入桶
index = (a[j]>>(k*4))&mask; //index指示当前元素应在哪个桶
b[index][c[index]] = a[j];
c[index]++;
}
int s = 0;
for(int i = 0; i < 16; i++){ //出桶
for(int j = 0; j < c[i]; j++)
a[s++] = b[i][j];
}
}
for(int i = 0; i < 16; i++)
free(b[i]);
free(b);
}