前言
基数排序(radix sort)又称桶排序(bucket sort),相对于常见的比较排序,基数排序是一种分配式排序,即通过将所有数字分配到应在的位置最后再覆盖到原数组完成排序的过程。我在上一篇讲到的计数排序也属于这种排序模式,上一篇结尾处提到了计数排序的稳定性,即排序前和排序后相同的数字相对位置保持不变。今天我们要说的基数排序就要利用到排序稳定性这一点。
思考过程
我们回想一下我们小时候是怎么学习比较数字大小的?我们是先比位数,如果一个位数比另一个位数多,那这个数肯定更大。如果位数同样多,就按位数递减依次往下进行比较,哪个数在这一位上更大那就停止比较,得出这个在这个位上数更大的数字整体更大的结论。当然我们也可以从最小的位开始比较,这其实就对应了基数排序里的MSD(most significant digital)和LSD(least significant digital)两种排序方式。
想清楚了这一点之后,我们就要考虑如何存储每一位排序结果的问题了,首先既然作为分配式排序,联想计数排序,每一位排序时存储该次排序结果的数据结构应该至少是一个长度为10的数组(对应十进制该位0-9的数字)。同时可能存在以下情况:原数组中所有元素在该位上的数字都相同,那一维数组就没法满足我们的需要了,我们需要一个10*n(n为数组长度)的二维数组来存储每次位排序结果。熟悉计数排序结果的读者可能会好奇:为什么不能像计数排序一样,在每个位置只存储出现该数字的次数,而不存储具体的值,这样不就可以用一维数组了?这个我们不妨先思考一下,在对基数排序分析完之后再来看这个问题。
现在我们可以存储每次位排序的结果了,为了在下一位排序前用到这一位排序的结果,我们要将桶里排序的结果还原到原数组中去,然后继续对更改后的原数组执行前一步的位排序操作,如此循环,最后的结果就是数组内元素先按最高位排序,最高位相同的依次按下一位排序,依次递推。得到排序的结果数组。
算法过程
- 初始化:构造一个10*n的二维数组,一个长度为n的数组用于存储每次位排序时每个桶子里有多少个元素。
- 循环操作:从低位开始(我们采用LSD的方式),将所有元素对应该位的数字存到相应的桶子里去(对应二维数组的那一列)。然后将所有桶子里的元素按照桶子标号从小到大取出,对于同一个桶子里的元素,先放进去的先取出,后放进去的后取出(保证排序稳定性)。这样原数组就按该位排序完毕了,继续下一位操作,直到最高位排序完成。
下面给出一个实例帮助理解:
我们现有一个数组:73, 22, 93, 43, 55, 14, 28, 65, 39, 81
下面是排序过程(二维数组里每一列对应一个桶,因为桶空间没用完,因此没有将二维数组画全):
1.按个位排序
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
|
81 |
22 |
73 |
14 |
55 |
|
|
28 |
39 |
|
|
|
93 |
|
65 |
|
|
|
|
|
|
|
43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
按第一位排序后数组结果:
81,22,73,93,43,14,55,65,28,39
可以看到数组已经按个位排序了。
2根据个位排序结果按百位排序
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
|
14 |
22 |
39 |
43 |
55 |
65 |
73 |
81 |
93 |
|
|
28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
取出排序结果:
14,22,28,39,43,55,65,73,81,93
可以看到在个位排序的基础上,百位也排序完成(对于百位相同的数子,如22,28,因为个位已经排序,而取出时也保持了排序的稳定性,所以这两个数的位置前后是根据他们个位排序结果决定的)。因为原数组元素最高只有百位,原数组也完成了排序过程。
总结
我们现在来看看之前遗留的两个问题:为什么不能用一维数组,一定要用二维数组这样的类似桶的结构来存储中间位排序结果?其实之所以要写这个问题,是因为我觉得这个问题是理解基数排序的关键。基数排序本身原理很简单,但是实现中有两个问题需要考虑:1.怎么保留前一位的排序结果,这个问题用之前提到的排序稳定性可以解决。2.怎么关联该位排序结果和原数组元素,二维数组正是为了解决这个问题使用的办法。在计数排序里,虽然保留了所有相等的元素的相对位置,但是这些相等的元素在计数排序里实际是没有差别的,因此我们可以只保存数组里有多少个这样的元素即可。而基数排序里不同,有些元素虽然在某一位上相同,但是他们其他位上很可能不同,如果只保存该位上有多少个5或者多少个6,那关于元素其他位的信息就都丢弃了,这样也就没法对这些元素更高位进行排序了。
弄清基数排序的过程后,我们来看看这个算法的时间复杂度是多少?每次循环遍历数组将元素放在指定位置Θ(n),在从桶中取出数据Θ(n),循环d次(d是位数),时间复杂度就是Θ(r*n)
最后附上基数排序的Java实现:
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。