浅谈基数排序

来源:互联网 发布:网络招生哪家强 编辑:程序博客网 时间:2024/04/30 11:42

排序算法有许多,时间复杂度在平方级的有冒泡排序,选择排序,插入排序,复杂度在对数级的有堆排序,归并排序,快速排序。

以上排序算法都是基于比较的,已经证明此类算法最快也就是O(N*(logN))

有一些排序算法不需要元素比较,时间复杂度可以达到线性级O(N),比如箱子排序和基数排序。


基数排序是基于箱子排序的,因此先谈谈箱子排序。

比如,给定整数数组a = {1, 2, 3, 2, 3, 3}。

可以看到数组元素的范围在[1,3]区间,且是离散的,即其取值在有限集合{1,2,3}中。

故此我们可以给定3个箱子分别装入这些数。

bin[1]:1;

bin[2]:2,2;

bin[3]:3,3,3;

然后把这三个箱子连起来,就得到1,2,2, 3, 3, 3。

箱子排序有多种实现,比如可以用链表作为箱子。

此处用数组来实现,代码如下:

//箱子排序#include <cstdio>#include <cstdlib>#include <ctime>#define N 20int a[N];template <class T>void BinSort(T a[],int n,int range){int *b = new int[range+1];T *t = new T[n];int i,temp,sum = 0;//将箱子置空for(i = 0;i <= range;i++)b[i] = 0;//统计每个箱子该有多大for(i = 0;i < n;i++)b[a[i]]++;//根据箱子大小,计算每个非空箱子的元素该放的位置(放在t[]中)for(i = 0;i <= range;i++){if(b[i] != 0){temp = b[i];b[i] = sum;sum += temp;}}//根据a[i]找到所在的箱子//再根据b[x](0<=x<n)确定a[i]在t[]中的位置for(i = 0;i < n;i++)t[ b[a[i]]++ ] = a[i];//把值赋回数组afor(i = 0;i < n;i++)a[i] = t[i];delete[] b;delete[] t;}int main(){int i;srand(time(0));for(i = 0;i < N;i++)a[i] = rand()%101;//freopen("data.txt","w",stdout);for(i = 0;i < N;i++)printf("%-8d",a[i]);printf("\n");BinSort(a,N,100);for(i = 0;i < N;i++)printf("%-8d",a[i]);printf("\n");return 0;}

箱子排序有一些局限,对一定范围内的正整数排序。以上代码中,范围是[0,100](可以用于百分制的排名,如果分数都是整数的话),所以箱子不用很多。

正是由于箱子排序的局限,是得它应用范围很小。


基数排序是基于箱子排序的,它在一定程度上改进了箱子排序。

比如对10个三位数排序,如果用箱子排序,需要900个箱子(因为这些数的范围在[100,999]之间),这显然是不划算的。

十进制数中,有高位有低位,同时3位数,如果一个数的百位大于另一个数,那它就比另一个数大,百位相等,十位大这较大,以此类推。

于是,我们可以将这些三位数拆分,然后逐位比较。那先比较低位还是高位呢?这就衍生出两种策略,最后都可以完成比较。

此处用的是先比较低位。

举例如下(升序):

给定数组int a={123,231,312,132,213,321}

先比较个位,得231,321,312,132,123,213;

再比较十位,得312,213,321,123, 231,132;

最后比较百位,得123,132,213,231,312,321。

为什么这样子呢?这得益于箱子排序树稳定排序,百度百科中对稳定排序的定义是这样的:

待排序的记录序列中可能存在两个或两个以上关键字相等的记录。排序前的序列中Ri领先于Rj(即i<j).若在排序后的序列中Ri仍然领先于Rj,则称所用的方法是稳定的。

比如上面,在比较十位完成后,123在132前面,然后到比较百位时,两数百位相等,123依然在132前面。

稳定排序在一些场景有应用,比如成绩排名,总分优先,总分相等,再比较某一科目(记得我们高一时是政治,晕!)。

基数排序也是稳定排序。

如此,比较三位数时,只要30个箱子就行了,比较4位数也只需要40个箱子。

其实把一位数和二位数看成三位数,跟三位数比较也行,就当做前面补零嘛,比如把7看作007。


基数排序比较麻烦的是箱子的范围(也就是“基数”)的确定。

比如对1000个六位数排序(范围[0,10^6-1],

如果基数是1000,那就需要进行两次箱子排序(1000^2 = 10^6]),

每次排序需要1000次初始化箱子,1000次分配箱子节点,1000次搜集箱子,也就需要执行2*(1000+1000+1000) = 6000次操作;

如果基数是100,那就需要进行3次箱子排序(100^3 = 10^6]),每次排序需要100次初始化箱子,1000次分配箱子节点,100次搜集箱子,

也就需要执行3*(100+1000+100) = 3600 次操作;

以此类推,如果基数是10,那就需要执行6*(10+1000+100)=6120次操作。

可以看到,基数的选取影响到算法的时间和空间复杂度。

可是,总不同每次用基数排序都去估算数据范围,指定基数大小吧——我想着正是基数排序没怎么被采用的原因。

在STL中,sort函数用的是快排,stable_sort用的是归并,这类算法的优点就是通用性强,只要元素可以比较,就能够排序。


有幸在某次上网时看到一个比较巧妙的基数排序的实现,该实现用256(即2^8)作为基,受其启发,得基数排序算法实现如下:

//基数排序#include <string.h>#include <cstdio>#include <cstdlib>#include <ctime>#define N 20double a[N];template <class T>void rsort(T *src, T *tmp, int n, int bit){int i,t,sum = 0;int cnt[256] = { 0 };for(i = 0;i < n;i++)cnt[(src[i] >> bit) & 0xFF]++;for(i = 0;i < 256;i++){t = cnt[i];cnt[i] = sum;sum += t;}for(i = 0;i < n;i++)tmp[cnt[(src[i] >> bit) & 0xFF]++] = src[i];}template <class T>void rsort(T *src,int n){T* tmp = new T[n];int size = sizeof(T);for(int i = 0;i < size;i++){if(i & 1)rsort(tmp, src, n, i<<3);elsersort(src, tmp, n, i<<3);}if( (size & 1) != 0)memcpy(src, tmp, n*size);delete[] tmp;}int main(){int i;int t;srand(time(0));for(i = 0;i < N;i++)a[i] = double(rand()) / double(rand()) * double(rand());for(i = 0;i < N;i++)printf("%-16.2lf",a[i]);printf("\n\n");rsort((long long *)a,N);for(i = 0;i < N;i++)printf("%-16.2lf",a[i]);printf("\n");return 0;}

不得不说,指定浮点数标准的人实在是太牛逼了。由于IEEE 754浮点标准使用的是“偏移指数(biased exponent)”,使得浮点数与整数(序)兼容:就同样的位模式来说,大的浮点数对应于大的整数,从而即使没有浮点硬件,也可以使用整数运算,来比较两个浮点数,实现排序。在

在上面的实现中,把浮点数组地址强转成整数数组地址,用同样的过程实现位运算,得出的排序结果也是正确的。

上面的rsort(第二个实现),只要调用得当,可以实现char, short, int, long long, float, double的排序,当然,是正数的前提下,如果包含负数,那需要做些处理。









原创粉丝点击