基数排序
已同步微信公众号【乐享Coding】,想要一起学习的可以加群,共同交流!
基本思想:
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。
从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
理论比较晦涩难懂,我们以具体例子进行图解:
本次举例的待排序数组为:[1, 52, 478, 12, 83, 7, 45, 333 ]
大家先可以看看排序的步骤,进一步对思路有个大致理解!
步骤一
准备是10个桶(0-9),取数组中每一个元素个位的值,分别装入对应的桶中。
根据动图可以看到桶应该是个二维数组
,既要有编号(0-9),又要存数据(0-arr.length)。
代码实现:
定义桶(二维数组):
int[][] bucket = new int[10][arr.length];
除了桶这个需要额外空间之外,我们还需要一个数组来记录每个桶中的元素个数,目的是方便最后取出。
int[] bucketElementCounts = new int[10]; //桶中元素个数
在此,我们还可以发现一个问题就是可能会存在空桶占用空间资源,所以基数排序典型的占用空间换取时间,文章末尾会对这种排序时间和空间复杂度的分析!
for (int j = 0; j < arr.length; j++) {
// 算法:取出个位数字
int digitOfElement = arr[j] / 1 % 10;
// 把当前遍历的数据放入指定的数组中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
// 记录数量
bucketElementCounts[digitOfElement]++;
}
在个位排序之后bucketElementCounts数组的应该为:
之后根据以下代码将桶中元素取出替换arr数组
int index = 0;
// 把各个桶中(10个桶)存放的数字取出来, 放入到arr中
for (int k = 0; k < bucketElementCounts.length; k++) {
// 如果这个桶中,有数据才取,没有数据就不取了
if (bucketElementCounts[k] != 0) {
// 循环取出元素
for (int l = 0; l < bucketElementCounts[k]; l++) {
// 取出元素
arr[index++] = bucket[k][l];
}
// 把这个桶的对应的记录的数据个数置为0,注意,桶本身数据(前面存的数据还在)
bucketElementCounts[k] = 0; //
}
上述排序后的新arr数组为
步骤二:第一次是找个位接下来就是找十位的数字,重复步骤一,在找出百位的数字,同样重复步骤一。最后arr数组就会按照升序排序结束。
在此只展示代码不同的地方,其余地方完全一样。
//算法:取出十位的数字
int digitOfElement = arr[j] / 10 % 10;
//算法:取出百位的数字
int digitOfElement = arr[j] / 100 % 10;
此时arr数组已经成了完全升序的数组了。
优化
最后我们总结以下可以优化的,此次排序一共进行了三轮,且每轮都有重复代码,因此这样是非常冗余的。
观察发现与最大元素的位数有关,联想简单选择排序我们可知只需找出最大值,最大值占几位,那么就需要循环几轮。
本例最大元素是478,共占三位,所以需要循环三轮,通过改进代码我们得到了最终代码:
public static void radixSort(int[] arr) {
// 假定arr[0] 是最大数
// 1. 通过遍历arr, 找到数组中真正最大值
// 2. 目的是确定要进行多少轮排序
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
// 计算最大数字是几位数
int maxLength = (max + "").length();
// 定义一个二维数组, 就是10个桶
// 1. 该二维数组有10个一维数组 0-9
// 2. 为了防止溢出,每个一维数组(桶),大小定为 arr.length
// 3. 很明确, 基数排序是空间换时间
int[][] bucket = new int[10][arr.length];
// 用于记录在每个桶中,实际存放了多少个数据,这样才能正确的取出
int[] bucketElementCounts = new int[10];
// 根据最大长度的数决定比较的次数
// 1. 大循环的次数就是 最大数有多少位,前面分析过
// 2. n = 1, n *= 10 是为了每轮循环排序时,分别求出各个元素的 个位,十位,百位,千位 ...
// 就是一个小算法
// 3. 这个基础排序,完全可以使用 冒泡分步写代码来完成,比较简单!!
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
// 把每一个数字分别计算本轮循环的位数的值,比如第1轮是个位...
for (int j = 0; j < arr.length; j++) {
// 计算
int digitOfElement = arr[j] / n % 10;
// 把当前遍历的数据放入指定的数组中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
// 记录数量
bucketElementCounts[digitOfElement]++;
}
// 记录取的元素需要放的位置
int index = 0;
// 把各个桶中(10个桶)存放的数字取出来, 放入到arr中
for (int k = 0; k < bucketElementCounts.length; k++) {
// 如果这个桶中,有数据才取,没有数据就不取了
if (bucketElementCounts[k] != 0) {
// 循环取出元素
for (int l = 0; l < bucketElementCounts[k]; l++) {
// 取出元素
arr[index++] = bucket[k][l];
}
// 把这个桶的对应的记录的数据个数置为0,注意,桶本身数据(前面存的数据还在)
bucketElementCounts[k] = 0; //
}
}
}
}
时间和空间复杂度分析
空间复杂度:
- 最坏情况下:O(m*n) m代表“桶”的个数,一般是10。
时间复杂度:O(m+n)
- m是保存桶的元素个数数组占的空间,n是保存桶的二维数组占的空间。
最后总结
1)基数排序是对传统桶排序的扩展,速度很快.
2)基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError 。
3)基数排序时稳定的(值相等的元素排序后前后顺序不变)。
4)有负数的数组,我们不用基数排序来进行排序, 如果要支持负数,需要添加绝对值并反转。