基数排序是一种基于多关键词的非比较排序算法。基数排序是一种用在老式穿卡机上的算法。一张卡片有80列,每列可在12个位置中的任一处穿孔。排序器可被机械地”程序化”以检查每一迭卡片中的某一列, 再根据穿孔的位置将它们分放12个盒子里。这样,操作员就可逐个地把它们收集起来。其中第一个位置穿孔的放在最上面,第二个位置穿孔的其次,等等。
算法原理(以正整数为例)
基数排序有**LSD(Least Significant Dight)和MSD(Most Significant Dight)**两种。MSD是从最主要关键词开始排序,而LSD是从最次关键词开始排序。由于MSD需要额外的空间,一般用到的是LSD方法,本文讲的都是LSD方法。
排序的要求是对任意两个数,a大于b,当且仅当从两数的第某位开始a大于b,也就是说前面的位权重大于后面的,我们先保证最后一位是有序的,这就保证了仅当最后一位不同的数是有序的,然后根据次末位进行排序,保证了后两位不同的数是有序的(注意:这里说的排序并不是一般意义上的排序方法)。
算法过程
首先,我们要有十个桶,和一个大桶(桶里的数根据先到先出的顺序,是一个队列,下文不作说明了)
第一步,把数组里的数依次放到桶里;
第二步,把倒数第i位(i从1开始)为k的数放到第k个桶里;
第三步,从第一个桶开始,把所有数放到大桶里;
第四步,重复第二、三步,直到所有数中的最大数倒数第i位是0;
排序完成
算法演示
例如对{41, 467, 334, 500, 169, 724, 478, 358, 962, 464}
进行基数排序。
1、–> {500}, {41}, {962}, {334, 464}, {467}, {478, 358}, {169}
2、–> {500, 41, 962, 334, 464, 467, 478, 358, 169}
3、–> {500}, {724}, {334}, {41}, {358}, {962, 467, 169}, {478}
4、–> {500, 724, 334, 41, 358, 962, 467, 169, 478}
5、–> {041}, {169}, {334, 358}, {464, 467, 478}, {500}, {724}, {962}
6、–> {041, 169, 334, 358, 464, 467, 478, 500, 724, 962}
时间复杂度
基数排序的时间复杂度与最大数的位数K、数的总个数N有关,一般来说是O(KN),这是正整数的情况。推广到一般情况,如果一位有B种可能性,最大的是M,那么时间复杂度是O(logB(M)*N)。值得一提的是,基数排序过程中会涉及到指针操作等等,速度稍微有点慢。如果考虑和比较排序进行对照,基数排序的形式复杂度虽然不一定更小,但由于不进行比较,因此其基本操作的代价较小,而且在适当选择的B之下,K一般不大于log_N_,所以基数排序一般要快过基于比较的排序,比如快速排序。
速度实测
写完基数排序代码之后,我很有兴致的测了一下各种排序算法处理10万规模的正整数的时间,结果发现对于同一个数据,基数排序用了0.2s,快速排序0.1s,归并排序0.4s,而插入排序竟然用了16s!(估计是代码写的太烂)(除了快速排序是STL里的,其他都是自己写的。)
排序算法(N=100000) | 时间(s) |
---|---|
基数排序(Redix-sort) | 0.1 |
快速排序(Quick-sort) | 0.2 |
插入排序(Insert-sort) | 0.4 |
归并排序(Merge-sort) | 16? |
算法稳定性
显然,根据LILO(先进先出)的规则,基数排序是稳定的排序。
示例代码(C++)
1 | /** |
代码说明
- 代码仅能处理正整数
- 如果待排序的数中含有负数,建议分开处理(原因见负数取模、负数除法的问题)
- 进行其他(比如a-z的字符串),需要增大桶的数量。
- 使用了链表和大量指针是为了减少空间复杂度和方便说明,平时写的时候可以利用STL中的队列
详细代码请点击这里查看。