1. MSD(Most Significant Digit First)排序准则
对记录按关键字组(K0, K1, K2, … , Kd-1)进行排序时,先按K0排序,使K0相同的记录排列在一起,然后对K0相同的每个子序列按K1排序,使K0相同的每个子序列进一步细分为K1相同的若干个更小的子序列,如此重复,直至Kd-1排序结束。基于MSD准则的排序等效于按照关键字组比较大小的定义直接对关键字组比较大小来实现排序,即只需要定义如下关键字组比较大小的函数即可实现MSD准则排序。
一个例子(比较时间的大小)
定义时间的结构体:
typedef struct
{
int hour,
min,
sec;
} Time;
依据MSD排序准则对时间进行比较,默认采用24小时时间制,一天从00:00:00开始,从23:59:59结束。越“晚”的时间越“大”,越“早”的时间越“小”。例如下午三点三分三秒是要大于凌晨三点三份三秒的,即15:03:03>03:03:03。比较原理就是MSD准则,从最高位开始,依次向最低位比较。
源代码
#include <iostream>
using namespace std;
typedef struct
{
int hour,
min,
sec;
} Time;
int compareTime(Time &a, Time &b);
int main()
{
Time a = {5, 4, 4}, b = {4, 4, 4}; //初始化,也可键入
int result = compareTime(a, b);
cout << result << endl; //输出结果
system("pause");
return 0;
}
int compareTime(Time &a, Time &b)
{ //a=b返回0,a>b返回1,a<b返回-1
//两个关键字组,d=3
int aK[3] = {a.hour, a.min, a.sec}, bK[3] = {b.hour, b.min, b.sec};
int i;
//依据MSD排序准则,从最高位开始比较
for (i = 0; i < 3; i++)
if (aK[i] != bK[i])
break;
//i>=3说明每一位都相同,"正常结束"循环
if (i >= 3)
return 0;
//"异常结束"循环再比较使得循环结束的这一位的大小来判断a,b的大小
if (aK[i] > bK[i])
return 1;
else
return -1;
}
2. LSD(Least Significant Digit First)排序准则
各关键字位按由低位到高位的次序排序。这需要通过“分配”和“收集”两种操作来实现。对于d元关键字组,需要进行d趟分配和收集操作。LSD准则实现的排序常称为基数排序。基数排序非常适合采用链式存储结构。为方便操作,每趟分配得到的每个分组用带头、尾指针的链式队列来实现存储。分配时,序列中的每个结点按连接次序依次摘下并连接到分组关键字相同的分组(队列)的队尾(这样可以确保同一队列中记录的连接先后次序与它们在原序列中的先后次序相同)。当序列中的所有结点摘完后,则一趟分配操作结束。收集时,只需将所有分组(队列)按分组关键字有序的次序首尾相连即生成新的序列。其实我们可以发现,基数排序是箱子排序的"拓展",理解箱子排序有助于我们理解基数排序。
2.1基数排序的手工求法
以下面这个三位十进制序列为例子,求其基数排序的结果:{278, 109, 063, 930, 589, 184, 505, 269, 008, 083}
第一趟:按照“个位”进行分配和收集
第二趟:按照“十位”进行分配和收集
注意:第二趟的输入为第一趟收集的结果
第三趟:按照“百位”进行分配和收集
最终结果为:{008,063,083,109,184,269,278,505,589,930}
2.2基数排序的算法实现
以三位四进制数的算法实现为例。首先我们定义结构体:
typedef struct
{ //关键字序列以及指针域(静态链表)
int K[3];
int next; //值为其下一个结点的元素下标
} elemTp;
其中整型数组K[d],(在这个例子中d=3),代表每一个元素的“位数”,对应三位四进制中的三位;整数next代表指针,它的值是该结点的下一个结点的下标。设每一个关键字的每一位的可能的取值为整型数组rd[]
,我们可以很容易的知道三位四进制中的四进制是什么意思,所以数组rd中的值为0,1,2,3,故我们用其来初始化数组rd并保持其不变(定义在所有函数之外)int rd[4] = {0, 1, 2, 3};
依据箱子排序的原理,显然我们需要知道每一个关键字需要插入的队列序号,于是我们定义了一个函数sp
int sp(int K)
{ //定义sp函数:返回待排序关键字应该插入的队列数
int j;
for (j = 0; j < 4; j++)
if (K == rd[j])
break;
return j;
}
然后我们就可以开始进行排序了,其基本思想还是“分配”和“收集”,通过每一个关键字的每一位与rd数组进行匹配,寻找到其应该插入的队列序号,然后收集起来;再分配,再收集;直至关键字序列有序。
源代码
//三位四进制基数排序
#include <iostream>
using namespace std;
int rd[4] = {0, 1, 2, 3};
typedef struct
{ //关键字序列以及指针域(静态链表)
int K[3];
int next; //值为其下一个结点的元素下标
} elemTp;
int radixSort(elemTp R[], int n);
int main()
{
elemTp R[6] = {{1, 2, 3}, {1, 2, 2}, {1, 2, 1}, {1, 1, 3}, {1, 1, 2}, {1, 1, 1}};
for (int h = radixSort(R, 6); h != -1; h = R[h].next)
{
for (int i = 0; i < 3; i++)
cout << R[h].K[i];
cout << " ";
}
cout << endl;
system("pause");
return 0;
}
int sp(int K)
{ //定义sp函数:返回待排序关键字应该插入的队列数
int j;
for (j = 0; j < 4; j++)
if (K == rd[j])
break;
return j;
}
int radixSort(elemTp R[], int n)
{ //基数排序,算法结束后返回静态链表头结点下标
//各结点next域初始化
int i;
//n个结点,共有n-1个指针
for (i = 0; i < n - 1; i++)
R[i].next = i + 1;
int h = 0, p = 0;
R[n - 1].next = -1; //h为头指针, -1表示链表结束
//依据LSD排序准则
for (i = 3 - 1; i >= 0; i--)
{ //第i趟分配
int *head = new int[4], *rear = new int[4];
int k;
//初始化队头指针
for (k = 0; k < 4; k++)
head[k] = -1;
while (h != -1)
{
k = sp(R[h].K[i]); //R[h]应插入到第k个队列中
if (head[k] == -1)
head[k] = h;
else
R[rear[k]].next = h;
rear[k] = h;
h = R[h].next;
}
//第i趟收集
//找到第一个非空队列,h为收集后的链表头,p跟踪当前链表尾结点
for (k = 0; k < 4; k++)
if (head[k] != -1)
break;
h = head[k];
p = rear[k];
//链接后续的非空队列
while (++k < 4)
if (head[k] != -1)
{
R[p].next = head[k];
p = rear[k];
}
//使链表结束
R[p].next = -1;
//释放队列头、尾指针内存
delete[] head;
delete[] rear;
} //end for
//返回链表头结点下标
return h;
}
2.3基数排序的算法复杂度分析
(1) 时间复杂度
第i趟分配:循环n次(n为记录总数); 第i趟收集:循环rd(i)次;共d趟分配和收集,故总循环次数为表示简单,记rd为rd(i)的最大值,于是T(n)=O(d(n+rd))
(2) 空间复杂度
辅助空间:O(rd)
参考资料
西南交通大学信息科学与技术学院软件工程系‐赵宏宇 数据结构A教学PPT 第9章