排序之基数排序
下面介绍一种比较“特别”的排序算法,前面讨论的几种算法均是基于关键字之间的比较来实现的,而基数排序则是通过“分配”和“收集”过程来实现排序的,不需要进行关键字间的比较,是一种借助多关键字排序的思想对单关键字排序的方法。其实通俗的来讲,基数排序的思想类似于我们对数字进行比较大小的思路,逐位的来进行比较。基数排序的关键就在于按位进行“分配”和“收集”,接下来我们详细讨论。
一般元素a[i]的关键字a[i].key是由d位数字组成,即k(d-1)k(d-2)….k(0),每一个数字表示关键字的一位,其中k(d-1)为最高位,k(0)为最低位,每一位的值都在0<=k(i)<r范围内,其中,r称为基数(也就是进制数)。例如,对于十进制r为10,对于二进制r为2。我们采用最低优先方式,过程为:我们先对最低位进行排序,在此基础上,然后再对次高位进行排序,依此类推,由低位到高位,每趟都是根据关键字的一位并在前一趟的基础上对所有元素排序,直至最高位。
在这里我们采用线性表的链式存储结构(单链表)来对程序进行描述,假设线性表由节点a0,a1,……,a(n-1)构成,每个节点的关键字由d位组成,其中每一位的数字为k(0<=k<r),r为基数,在排序过程中需要使用r个链队Q0,Q1…..Q(r-1)(所以空间复杂度为O(r)),排序过程如下:
对i=0,1,….,,d-1,依次做一次“分配”和“收集”。
分配过程:开始时,把Q0,Q1,….,Q(r-1)各个链队置为空,然后依次考察线性表中的每一个节点a(i)(i=0,1,….n-1),如果a(i)的关键字为k,就把a(i)插入到Q(k)链表中,采用尾插法。
收集过程:将Q0,Q1,….Q(r-1)各个队列中的节点依次首尾相接,得到新的节点序列,从而形成新的线性表。具体过程下面给出代码来解释,在给出代码前,先举例说明一下“分配”和“收集”过程:
待排序序列: 23 42 45 36 78 66 80 84
按个位数字进行分配 → 收集 → 按十位数字进行分配 → 收集(序列有序 )
typedef struct node
{
chardata[MAXD];//MAXD为最大的关键字位数
structnode *next;
}RecType;
void RadixSort (RecType * &p, int r,int d)
//实现基数排序,p为待排序序列单链表指针,r为基数,d为关键字位数
{
RecType*head[MAXR], *tail[MAXR], *t; //定义各链队的首位指针
inti,j,k;
for(i=d-1; i>=0; i--)//从低位到高位循环进行“分配”和“收集”
{
for(j=0;j<r; j++)//初始化各链队首位指针
head[j]= tail[j] = NULL;
while(p!= NULL) //找到第k个链队
{
k= p->data[i] - '0';
if(head[k]= NULL)//第k个链队首尾为空时,队头队尾均指向*p
{
head[k]= p;
tail[k]= p;
}
else
{
tail[k]->next= p;//第k个链队非空时,尾插法插入*p
tail[k]= p;
}
p= p->next;
}
//分配完成,下面开始收集
p=NULL;//重新用p来收集所有节点
for(j=0;j<r; j++) //收集:对于每一个链队循环
{
if(head[j] != NULL) //若第j个链队是第一个非空链队
{
if(p== NULL)
{
p= head[j];
t= tail[j];
}
else //若第j个链队是其他非空链队
{
t->next= head[j];
t= tail[j];
}
}
t->next= NULL;
//最后一个节点的next域置NULL,至此一次分配收集过程结束
}
}
}
我们不难分析出,在基数排序过程中,共进行了d遍的分配和收集,每一遍分配和收集的时间为O(n+r),所以基数排序的时间复杂度为O(d(n+r))。这也就说明了,当待排序序列的位数过大或者各个数字的位数差距较大时,不适宜使用基数排序。