基数排序(Radix Sorting),又称 桶排序(bucket sorting)是之前的各类排序方法完全不同的一中排序方法,在之前的排序方法中,主要是通过元素之间的比较和移动两种操作来实现排序的。基数排序不需要进行元素之间的比较,而是根据关键字的每个位上的有效数字的值,借助“分配”和 “收集”两种操作来进行排序的一中内部排序方法。
在具体介绍基数排序算法前,首先先介绍两个两个关键词:单关键字和多关键字。序列中任一记录的关键字均有 d 个分量 ki0 ki1 …… kid-1 构成,若 d 个分量中每个分量都是一个独立的关键字,则文件是多关键字的(如扑克牌有两个关键字:点数和花色、汉子的笔画和拼音);否则是文件时单关键字的(如数值和字符串)。在多关键字的排序中,每个关键字都能决定记录的大小,如扑克牌的花色黑桃比花色方片大;在花色相同的情况下,在比较点数的大小,如红桃 9 比红桃 8 大。 kij (0<= j < d) 是关键字中的其中一位(如字符串,十进制整数等)。多关键字中的每个关键字的取值范围一般不同,如扑克牌的花色取值只有 4 种,而点数则有 13 种。对于单关键字序排列可以利用多关键字排序的方法,只是单关键字的每位一般取值范围相同。
在介绍一下基数的概念。设单关键字的每个分量的取值范围均为 C0 <= kj<= Crd-1 (0 <= j <=d),则每个记录中分量的可能取值的个数 rd 称为基数。基数的选择和关键字的分解因关键字的类型而异:
(1)若关键字是十进制整数,则按个、十等位进行分解,基数 rd = 10, C0 = 0,C9 = 9, d 为最长整数的位数;
(2)若关键字是小写的因为字符串,则 rd = 26,C0 = 'a' ,C25 = 'z', d为字符串的最大长度。
(3)在扑克牌花色和点数的排序中,花色的基数 rd1 = 4 ,而点数的 rd2 = 13, d 同时使用扑克牌的副数。
基数排序时一种借助于过关键字排序的思想,将单关键字按基数分成“多关键字”进行排序的方法。比如字符串 "abcd" "acsc" "dwsc" "rews" 就可以把每个字符看成一个关键字,另外还有整数 425 、321、235、432也可以将每个位上的数字作为一个关键字。
一般情况下,假定有一包含 n 个对象的序列 { V0 , V1 , …… ,Vn-1 } ,且每个对象 Vi 中包含 d 个关键字 ( ki1 ,ki2 , …… ,kid ), 如果对于序列中任意两个对象 Vi 和 Vj (0 <= i < j <= n - 1 ) 都满足: ( ki1 ,ki2 , …… ,kid ) < ( kj1 ,kj2 , …… ,kjd ) ,则称序列对关键字 ( k1 ,k2 , …… ,kd ) 有序。其中,k1 称为最高位关键字,k2 称为次高位关键字, kd 称为 最低位关键字。
基数排序方法有两种:最高位优先法(MSD:Most Significant Digit First)和最低位优先法(LSD:Least Significant Digit First)。
最高位优先法,简称MSD法:即先按 k1 排序分组,同一组中记录的关键字 k1 相等,在对各组按 k2 分成子组,之后对其他的关键字继续这样的排序分组,直到按最次位关键字 kd 对各子组排序后。再将各组连接起来,得到一个有序序列。
最低位优先法,简称LSD法: 即先从 kd 开始排序,在对 kd-1 进行排序,一次重复,直到对 k1 排序后便得到一个有序序列。
现在已下面的序列为例简述一下 MSD 方法和 LSD方法: ead, bed, dad, add, bee, abc, dbe, dae, cda, eba, ccd 共 n = 11 个字符串
上面的每个字符串每个字符串包含 3 个字符,因此 d = 3, 这些字符的取值 为 { a, b, c, d, e} 共 5 种取值, rd = 5
【注:这是针对这个例子而言 rd = 5,字符串的 rd 一般为 26 】
MSD 方法的排序过程如下:
第一个字母排序: 将第一个字母相同的元素放在同一个队列,我们就可以得到:
第一个字母 元素
a add abc
b bed bee
c cda ccd
d dad dbe dae
e ead eda
第二个字母排序:将上述队列中元素,第二个字母相同的元素放在同一个队列中,我们可以得到
第一个字母 第二个字母 元素
a b abc
a d add
b e bed bee
c c ccd
c d cda
d a dad dae
d b dbe
e a ead
e b eba
第三个字母排序:将上述队里中的元素,第三个字母相同的元素放在同一个队列中,我们可以得到
第一个字母 第二个字母 第三个字母 元素
a b c abc
a d d add
b e d bed
b e e bee
c c d ccd
c d a cda
d a d dad
d a e dae
d b e dbe
e a d ead
e b a eba
由于每个栏中只有一个元素,故从上到下连接起来就得到有序队列: abc, add, bed, bee, ccd, cda, dad, dae, dbe, ead, eba
使用LSD排序过程如下:
首先根据第三个字母排序,字母相同的放在一起,得到序列: cda, eba, abc, ead, bed, dad, add, ccd, bee, dbe, dae
然后对得到的序列根据第二个字母排序得到:ead, dad, dae, eba, abc, dbe, ccd, cda, add, bed, bee
最后得到的序列根据第一个字母排列得到: abc, add, bed, bee, ccd, cda, dad, dae, dbe, ead, eba
我们可以看出使用LSD排序方法排序结果和 MSD排序方法是一样的,只是排序过程中元素交换次序有些区别。MSD 是对在上一次分配好的队列(或初始序列)中对元素进行分配排序,每一次分配的元素越来越少,总数不变,但是分配次数增加;LSD是对上一次分配得到的序列,收集后再进行分配排序,每一次分配元素和次数均相同。
上面讲得是关于基数排序的基本思路和方法,下面我们以另外一个例子讲一下基数排序的实现过程。
我们以数值为例,先假定每个数值 只有两位,因此数值包括 d = 2 个分量, 每个数值的取值范围是 0 ~ 9 ,共 rd = 10 种,如数值:
73, 22, 93, 43, 55, 14, 28, 65, 39,81
首先根据个位数的数值,对每个数值查询最末位的值,将它们分配到编号0 ~ 9 的队列中
个位数 数值
0
1 81
2 22
3 43 93 73
4 14
5 65 55
6
7
8 28
9 39
将上面的队里的数值重新串起来,如果是链式队列的话,这个过程将会非常简单,只要把指针连接起来就好了。我们得到新的序列:
81, 22, 43, 93, 73, 14, 65, 55, 28, 39
接着对十位进行一次分配,我们可以得到下面的结果:
十位数 数值
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
我们将这些队列中的数值重新串起来,得到序列: 14, 22, 28, 39, 43, 55, 65, 73, 81, 93
这个时候整个序列已经排序完毕,如果排序的对象有三位数以上,则继续进行以上的动作直至最高位为止。
LSD的基数排序使用与位数小的数列,如果位数多的话,使用MSD的效率会比较好,MSD的方式恰好 与LSD相反,由最高位为基底进行分配其他的演算方式都相同。
对于记录的长度不同的序列,通过在记录前面加上相应数据类型最低位来进行处理,如数值类列前加0,字符串类型前加空字符。
参考代码:(这是以LSD方式实现的)
1 #include <stdio.h> 2 3 #define MAX_NUM 80 4 5 int main(int argc, char* argv[]) 6 { 7 int data[MAX_NUM]; // 存储数据 8 int temp[10][MAX_NUM]; // 队列表 9 int order[10]={ 0}; // 用于记录队列的信息 10 11 int n; // 待排序个数 12 int i,j,k; // 用于排序 13 int d; // 用于表示待排序输的位数 14 int lsd; // 用于记录某位的数值 15 16 k = 0; 17 d = 1; 18 19 printf("输入需要排序的个数(<=80),待排序数(<10000): "); 20 scanf("%d",&n); 21 22 if( n > MAX_NUM ) n = MAX_NUM; 23 24 for(i = 0; i < n;i++) 25 { 26 scanf("%d",&data[i]); 27 data[i] %= 10000; 28 } 29 30 31 while(d <= 10000) 32 { 33 for(i = 0; i < n; i++) 34 { 35 lsd = (data[i]/d)%10; 36 temp[lsd][order[lsd]] = data[i]; 37 order[lsd]++; 38 } 39 40 printf(" 重新排列:"); 41 42 for(i = 0; i < 10; i++ ) 43 { 44 if(order[i]!=0) 45 { 46 for(j = 0; j < order[i]; j++ ) 47 { 48 data[k] = temp[i][j]; 49 printf("%d ",data[k]); 50 k++; 51 } 52 } 53 order[i] = 0; 54 } 55 56 d *= 10; 57 k = 0; 58 } 59 60 printf(" 排序后:"); 61 for(i = 0; i <n; i++) 62 printf("%d ",data[i]); 63 printf(" "); 64 65 66 67 return 0; 68 }
代码运行结果以下面的数值列为例: 125 11 22 34 15 44 76 66 100 8 14 20 2 5 1 共 15 个元素
运行截图:
基数排序算法的效率和稳定性
对于 n 个记录,执行一次分配和收集的时间为O(n+r)。如果关键字有 d 位,如果关键字有 d 位,则要执行 d 遍。 所以总的运算时间为 O(d(n+r))。可见不同的基数 r 所用时间是不同的。当 r 或 d 较小时,这种算法较为节省时间。上面讨论的是顺序表的基数排序,这个算法同样也是用与链式的记录排序,只是要求额外附加一下队列的头、尾指针。所以附加的存储量为 2r 个存储单元。待排序的记录以链表形式存储的,相对于顺序表,只是额外增加了 n 个指针域的空间。
基数排序在分配过程中,对于相同关键字的记录而言保持原有顺序进行分配,故基数排序时稳定的排序方法。
注:主要参考彭军、向毅主编的 《数据结构与算法》