基础算法11-排序之计数排序、桶排序、基数排序
在之前我们介绍的都是比较排序算法,在结果中各元素的次序都基于输入元素间的比较。而任何比较排序算法在最坏情况下都要用 O(NlgN) 此比较来排序。而非基于比较的排序,如计数排序,桶排序,和在此基础上的基数排序,则可以突破O(NlogN)时间下限。但要注意的是,非基于比较的排序算法的使用都是有条件限制的,例如元素的大小限制,相反,基于比较的排序则没有这种限制(在一定范围内)。但并非因为有条件限制就会使非基于比较的排序算法变得无用,对于特定场合有着特殊的性质数据,非基于比较的排序算法则能够非常巧妙地解决。
计数排序
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
1 | //maxVal为传入的数组的最大值 |
桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort
)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序.
- 找出待排序数组中的最大值
max
、最小值min
- 我们使用动态数组
ArrayList
作为桶,桶里放的元素也用ArrayList
存储。桶的数量为(max-min)/arr.length+1
- 遍历数组
arr
,计算每个元素arr[i]
放的桶 - 每个桶各自排序
- 遍历桶数组,把排序好的元素放进输出数组
1 | private static int[] bucketSort(int[] arr){ |
基数排序
基数排序(Radix Sort
)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。
通过基数排序对数组{53, 3, 542, 748, 14, 214, 154, 63, 616},它的示意图如下:
在上图中,首先将所有待比较数值统一为统一位数长度,接着从最低位开始,依次进行排序。
- 按照个位数进行排序。
- 按照十位数进行排序。
- 按照百位数进行排序。
排序后,数列就变成了一个有序序列。
在理解了基本的思想之后,下面以一个简单的例子辅助理解程序思想。
首先我们有以下这个数组:
1 | int[] arrays = {6, 4322, 432, 344, 55 }; |
现在我们有10个桶子,每个桶子下能装载arrays.length个数字
1 | int[][] buckets = new int[arrays.length][10]; |
效果如下:
第一趟分配与回收:将数组的每个个位数进行分配到不同的桶子上
分配完之后,我们按照顺序来进行回收:得到的结果应该是这样子的:{4322,432,344,55,6}
第二趟分配与回收:将数组的每个十位数进行分配到不同的桶子上(像6这样的数,往前边补0)
分配完之后,我们按照顺序来进行回收:得到的结果应该是这样子的:{6,4322,432,344,55}
第三趟分配与回收:将数组的每个百位数进行分配到不同的桶子上(像6、55这样的数,往前边补0)
分配完之后,我们按照顺序来进行回收:得到的结果应该是这样子的:{6,55,4322,344,432}
第四趟分配与回收:将数组的每个百位数进行分配到不同的桶子上(像6、55,344,432这样的数,往前边补0)
分配完之后,我们按照顺序来进行回收:得到的结果应该是这样子的:{6,55,344,432,4322}
理解了上面,代码也就非常容易理解了:
获取这个数组的最大值,这里用递归来实现一下:
1 | private static int getMax(int[] arr,int L,int R){ |
基数排序:
1 | public static int[] radixSort(int[] arr) { |
基数排序要理解起来并不困难,不过值得注意的是:基数排序对有负数和0的数列难以进行排序
- 因此,往往有0和负数的数组一般我们都不用基数来进行排序
基数排序的要点就两个:
- 分配:按照元素的大小来放入不同的桶子里
- 回收:将桶子里的元素按桶子顺序重新放到数组中
- 重复…两个步骤