数据结构:分配排序

插入类、交换类、选择类和归并类排序方法,都是在比较关键字大小的基础上实现排序的,而分配类排序不需要比较关键字的大小。

分配排序,是指通过对待排序记录进行若干趟分配与收集实现排序,是一种借助多关键字排序思想对单关键字进行排序的方法。基数排序(Radix Sorting)是最典型的分配类排序。

一、多关键字排序思想

先看一个具体例子。已知一副扑克牌,52张牌面的次序关系为

1
2
3
4
2<♣3<...<♣A<
2<♦3<...<♦A<
2<♥3<...<♥A<
2<♠3<...<♠A

即,每张牌有两个关键字:花色(♣<♦<♥<♠)和面值(2<3<…<A),且花色的地位高于面值。在比较任意两张牌面的大小时,必须先比较花色,若花色相同,则再比较面值。由此,将扑克牌整理成上述次序关系时,有以下两种排序法。

最高位优先法:先按不同花色分成有次序的4堆,每堆牌均具有相同的花色,然后分别对每堆牌按面值大小整理有序。

最低位优先法:先按不同面值分成13堆,然后将这13堆牌按照面值的次序收集到一起(将13堆牌自小至大叠在一起,3在2之上,4在3之上,…,最上面的是4张A);再重新对这些牌按不同花色分成4堆,最后将这4堆牌按花色的次序再收集到一起(♣在最下面,♠在最上面)。此时,同样可以得到一副,满足上述次序关系的牌。该方法是一种分配与收集交替进行的方法,其过程如下图所示。

img

二、链式基数排序

2.1、算法思想

基数排序的思想类似于上述最低位优先法的洗牌过程,是借助分配和收集操作,对单逻辑关键字进行排序的一种内部排序方法。

有的逻辑关键字可以看成是由若干个关键字复合而成的。例如,若关键字是数值,且其值都在0K9990\le K \le 999范围内,则可把每位上的十进制数字看成一个关键字,即可认为K由3个关键字K0,K1,K2(K^0,K^1,K^2)组成,其中k0k^0是百位数,k1k^1是十位数,k2k^2是个位数;若关键字K是由5个字母组成的单词,则可看成是由5个关键字K0,K1,K2,K3,K4(K^0,K^1,K^2,K^3,K^4)组成,其中kj1k^{j-1}是(自左至右的)第j个字母。这样分解而得的每个关键字kjk^j都在相同的范围内(对数字来说,是0kj90 \le k^j \le 9;对于字母来说,是AkjZ'A' \le k^j \le 'Z'),故可以按照分配和收集的方法进行排序。

假设记录的逻辑关键字由d个关键字组成,每个关键字可能取rd个值。只要从最低数位关键字起,按关键字的不同值将序列中记录分配到rd个队列中后再收集之,如此重复d次完成排序。按这种方法实现排序称之为基数排序 ,其中“基”指的是rd的取值范围,在上述两种关键字的情况下,rd的取值范围分别为10和26。

2.1.1、示例

接下来,来看一个具体例子。

假设待排序记录的关键字都是数值,且值都在0K9990\le K \le 999范围内。首先,以链表存储n个待排序记录,链表头指针指向第一个记录,如图下所示。然后,通过三趟分配和收集操作来完成排序。

image-20240614085921467

第一趟分配对最低数位关键字(个位数)进行,改变记录的指针值,将链表中的记录分配到10个链表中,每个链表中的记录关键字的个位数相等,如下图(a)所示,其中f[i]和e[i]分别为第i个链表的头指针和尾指针;第一趟收集是改变所有非空链表的表尾记录的指针域,令其指向下一个非空链表的表头记录,重新将10个链表中的记录链成一个链表,如下图(b)所示。

image-20240617083702416

image-20240617084134474

第二趟分配和第二趟收集是对十位数进行的,其过程和个位数相同。分配和收集结果分别如下图(a)和下图(b)所示。

image-20240617085531504

image-20240617085833018

第三趟分配和第三趟收集是对百位数进行的,过程同上。分配和收集结果分别如下图(a)和下图(b)所示。至此排序完毕。

image-20240617090928540

image-20240617091152988

2.1.2、存储结构

基数排序在具体实现时,一般采用链式存储结构。这里,算法实现时采用静态链表(即用数组描述的链表),以便于更有效地存储和重排记录。相关数据类型的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 记录的逻辑关键字最多由多少个关键字组成(这里假设最多由8个关键字组成)
#define MAXNUM_KEY 8

// 关键字基数的取值范围(这里假设是十进制整数)
#define RADIX 10

// 静态链表的空间大小
#define MAX_SPACE 10000

// 静态链表的结点类型
typedef struct
{
// 关键字
KeyType keys[MAXNUM_KEY];
// 其他数据项
InfoType otheritems;
int next;
}SLCell;

// 静态链表类型
typedef struct
{
// 静态链表的可利用空间,r[0]为头结点
SLCell r[MAX_SPACE];
// 静态链表的当前长度
int recnum;
// 记录的当前关键字个数
int keynum;
}SLList;

// 数组类型
typedef int ArrType[RADIX]

2.2、算法描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 静态链表L的r域中的记录已按(keys[0], keys[1],..., keys[i-1])有序
// 按第i个关键字keys[i]建立RADIX个子表,keys[i]相同的记录存放在同一个子表中
// f[0..RADIX-1]和e[0..RADIX-1]分别指向各子表中第一个和最后一个记录
void Distribute(SLCell &r, int i, ArrType &f, ArrType &e)
{
// 初始化各子表为空表
for(j=0; j<RADIX; ++j) f[j]=0;

for(p=r[0].next; p; p=r[p].next)
{
// ord函数根据记录的第i个关键字,将记录映射到某个子表(即求记录在子表中的位置)
j=ord(r[p].keys[i]);

// 将p所指的结点插入到第j个子表中
if(!f[j]) f[j]=p;
else r[e[j]].next=p;
e[j]=p;
}
}

// 按第i个关键字keys[i]自小到大地将f[0..RADIX-1]所指各子表依次链接成一个链表
// e[0..RADIX-1]为各子表的尾指针
void Collect(SLCell &r, int i, ArrType f, ArrType e)
{
// 找第一个非空子表,succ函数是求后继函数
for(j=0; !f[j]; j=succ(j));

// 静态链表的头节点r[0]指向第一个非空子表中第一个结点
r[0].next=f[j];
// t指向最后一个非空子表中的最后一个结点
t=e[j];

while(j<RADIX)
{
// 找下一个非空子表
for(j=succ(j); j<RADIX-1&&!f[j]; j=succ(j));
// 链接两个非空子表
if(f[j]) { r[t].next=f[j]; t=e[j]; }
}
// 将最后一个结点的指针域置空
r[t].next=0;
}

// 对L(采用静态链表表示的顺序表)做基数排序,使得L成为按关键字自小到大的有序静态链表
void RadixSort(SLList &L)
{
// 将L改造为静态链表,r[0]为头结点
for(i=0;i<L.recnum;++i) L.r[i].next=i+1;
L.r[L.recnum].next=0;

// 按最低位优先法,依次对各个关键字进行分配和收集
for(i=0;i<L.keynum;++i) {
// 第i趟分配
Distribute(L.r, i, f, e);
// 第i趟收集
Collect(L.r, i, f, e);
}
}

2.3、算法分析

从时间上来看。对n个记录(假设每个记录含d个关键字,每个关键字的取值范围为rd个值)进行链式基数排序时,每一趟分配的时间复杂度为O(n),每一趟收集的时间复杂度为O(rd),整个排序需进行d趟分配和收集,所以时间复杂度为O(d (n +rd ))。

从空间上来看。所需辅助空间为2rd个队列指针,另外由于用链表做存储结构,则相对于其他以顺序结构存储记录的排序方法而言,还增加了n个指针域的空间,所以空间复杂度为O(n+rd )。

2.4、算法特点

基数排序算法的特点为:

  • 稳定排序。
  • 可用于链式结构,也可用于顺序结构。
  • 时间复杂度可以突破基于关键字比较类方法的下界O(nlog2n)O(nlog_{2}{n}),达到O(n)。
  • 基数排序使用条件有严格的要求:需要知道各级关键字的主次关系和各级关键字的取值范围。

三、总结

img

四、参考

《数据结构(C语言版 第2版)》

《数据结构 自考02331》


数据结构:分配排序
https://kuberxy.github.io/2024/05/26/数据结构27:分配排序/
作者
Mr.x
发布于
2024年5月26日
许可协议