排序10:基数排序

       基数排序是一种与之前所介绍的排序算法截然不同的排序方法。前面所介绍的排序方法中,都有着共同的一种操作:元素间的比较。而基数排序则不是基于元素间的比较来驱动整个排序过程的,后文将会详细展开介绍。另外,基数排序需要做一些准备工作,以序列中最大者为准,我们需要让其它元素的位数与最大者的位数一致,比它少的则要在数值前补0。 这步的目的在于让序列全体元素的位数一致。 我们很快会发现,基数排序过程需要操作元素的 数位。
       现在,我们以全新的序列:278、109、63、930、589、184、505、269、8、83为例。开始时,找到该序列最大值930,其是3位数,对序列内剩下的元素,若数位小于3,则全部在其前头补0直到有3位,得:278、109、063、930、589、184、505、008、083。接着,我们发现,所有元素均为10进制数,也就是说元素的每个数位均由0~9这10种数字构成。我们创建10个队列,每个队列刚好对应表示1个十进制数位值,则分别有队列0、队列1、队列2、……、队列9。
       现在可以正式开始处理了。首先,我们依次看序列所有元素的最低位,并按最低位的数字令该元素进入相应的队列当中。序列头个元素为278,其最低位是8,278进入队列8(开头队列为空,则所进入元素将成为队列头)。对于109、63、930,则分别进入队列9、队列3、队列0。对于589,其必然进入队列9,由于队列9已有1个元素109,109也为当前的队尾,则589紧跟其后,成为新的队尾。对于剩余的元素都是同样的处理,则第1趟处理后,所有队列的情况为:队列0:930;队列1:空;队列2:空;队列3:063、083;队列4:184;队列5:505;队列6:空;队列7:空;队列8:278、008;队列9:109、589、269。我们从队列0开始一直到队列9,把前一个队列的队尾与后一个队列的队头链接起来,队列0的队头除外,其作为链接结果的头,队列9的队尾也除外,其作为链接结果的尾。从头到尾遍历这个链接结果,则得到第1趟处理后的序列:930、063、083、184、505、278、008、109、589、269。不难看出,该序列已经按元素最低位有序了。
       然后,我们将清空全部队列,对第1趟处理后得到的序列,按照每个元素的中间位,完全效仿上文所介绍的步骤处理。则第2趟处理后的各队列如下:队列0:505、008、109;队列1:空;队列2:空;队列3:930;队列4:空;队列5:空;队列6:063、269;队列7:278;队列8:083、184、589;队列9:空。按同样方法把所有队列链接起来后遍历,得第2趟处理后的序列:505、008、109、930、063、269、278、083、184、589。这个序列显然按元素的中间位有序,读者或许在这里会提出个问题:第1趟处理后,序列按元素最低位有序,第2趟处理后,序列按元素中间位有序。这两者是相互独立没关系的吗?这两者究竟有何意义?细心观察可发现,第2趟处理的结果似乎破坏了第1趟处理的结果,让人觉得不知道是什么目的,比如说:第1趟处理后的505必然排在063之后,而第2趟处理必有505在063之前,这似乎让人觉得不可思议。然而,由于第2趟处理是在第1趟处理所得的序列上进行的,第1趟处理后,序列已经按元素的最低位有序。这点对于中间位相同的元素也不例外,那么显然,第2趟处理后的序列,尽管拥有不同中间位的元素将不再与第1趟处理后的顺序一致,但拥有相同中间位的元素将仍然保留第1趟处理后的顺序。比如505、008这两者,第1趟处理后,505必然在008前,由于两者中间位相同,第2趟处理后,505与008都在队列0,且505仍在008之前。由此,第2趟处理结果将有:序列按元素的中间位有序,与此同时,中间位相同的元素按最低位有序。
       最后,我们再次清空全部队列,对第2趟排序后的序列,按每个元素的最高位,完全效仿上文所介绍的步骤同样处理。此刻将有:队列0:008、063、083;队列1:109、184;队列2:269、278;队列3:空;队列4:空;队列5:505、589;队列6:空;队列7:空;队列8:空;队列9:930。同样链接全部队列并遍历,得第3趟处理后序列:008、063、083、109、184、269、278、505、589、930。按照同样的分析,可知:第3趟排序后的序列按元素最高位有序,最高位相同的元素将会按中间位有序,中间位也相同者则会按最低位有序。这样的结果不正好是我们所期待的最终有序序列吗?所以说,至此,基数排序完毕,最后别忘了,可以把所有元素的前导0清除掉了。
       从上面的介绍看来,基数排序明显是稳定排序。另外,基数排序的原理使得它只限于处理非负整数构成的序列。代码如下(注:代码中用到的队列,实际上可以是直接使用STL里头提供的队列,此处本人只是因为强迫症,而去实现了自己的队列版本而已,可忽略): 
#include <cmath>

struct ListNode
{
	int data;
    ListNode * next;
};

class Queue
{
	private:
		ListNode * head;
		ListNode * tail;
		
	public:
    	Queue()
        {
        	head=tail=0;
        }
        
        bool isEmpty()
        {
        	return (head==0)&&(tail==0);
        }
        
        void enter(int element)
        {
        	ListNode * listNode=new ListNode;
            listNode->data=element;
            listNode->next=0;
            
            if(isEmpty())
				head=tail=listNode;
            else
            {
            	tail->next=listNode;
                tail=listNode;
            }
        }
        
        int get()
        {
        	return head->data;
        }
        
        void depart()
        {
        	ListNode * temp=head;
        	
            if(head==tail)
				head=tail=0;
            else
				head=temp->next;

            delete temp;
        }
                
		~Queue()
        {
        	while(!isEmpty())
				depart();
        }
};

int getMax(int list[],int length)
{
	int max=0;
	for(int i=1;i<length;++i)
	{
		if(list[i]>list[max])
			max=i;
	}
	
	return list[max];
}

int getNumOfDigits(int number,int radix)
{
	int count=0;
	
	while(number/=radix)
		++count;
		
	return ++count; //上面的循环条件导致最高位没被算进去。 
}

void radixSort(int list[],int length)
{
	int radix=10; //要处理不同进制非负整数,可修改这里。
	int max=getMax(list,length);
	int numOfDigits=getNumOfDigits(max,radix);
	Queue * queues=new Queue [radix];
		
	for(int i=1;i<=numOfDigits;++i)
	{
		for(int j=0;j<length;++j)
		{
			/*
			  此步目的在于获得各趟排序中,所取元素的相应数位。先让元素除以基数的相应次幂,再让结果对基数取余。
			  以10进制的元素为例,基数为10,其除以10的0次则为1,不变;除以10的1次则去掉了个位
			  (使用整数除法可去掉小数点后的部分,只保留结果的整数部分),从而让之前的十位变为现在的个位;
			  除以10的2次则去掉了个位、十位,从而让之前的百位变为现在的个位;如此类推……
			  除得的结果再对10求余,便能把结果的个位取出,该个位便是原先的百位、十位、个位神马的…… 
			*/ 
			int digit=(list[j]/(int)pow(radix,i-1))%radix;
			queues[digit].enter(list[j]); 
		}
		
		int index=0;
		for(int j=0;j<radix;++j)
		{
			while(!queues[j].isEmpty())
			{
				list[index++]=queues[j].get();
				queues[j].depart();
			}
		}
	}
	
	delete [] queues;
}
       设序列元素个数为n,元素均为 r 进制数,且元素被统一定为d位。显然,基数排序的时间复杂度分析不存在最好最坏情况之分 。d决定了基数排序的趟数,每一趟处理中,需要扫描整个序列,以令元素按位进入相应队列,故这里会进行n次操作。然后,顺次地让所有队列头尾相接,队列数由 r 决定,以更新原序列。这步需要进行r 次操作。所以每趟处理共需要进行n+r 次操作,则基数排序的过程总共需要(n+r)d 次处理。因此,基数排序的时间复杂度为O((n+r)d)。由于基数排序需要使用队列作为辅助存储空间,队列的数量、大小恰好与 r、n有关,上面提到过 r 即为队列数,而所有队列所用空间的大小主要由n线性决定。故基数排序的空间复杂度为O(n+r)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值