介绍:
RadixSort
另外一种非比较排序算法,基于计数排序(或者说桶排序)进行排序,也可以达到O(n)的复杂度。
思路:
计数排序==>桶排序达到O(n)的情况,要求桶中只有一个元素,或者只有相同的元素。这种如果数据的值跨度过大,或者过于离散,所需要的很多,因为中间的空的数字也会占桶的,导致空间浪费很多。
基数排序很好了解决了这个问题,原始的思路不好推演,我们使用网上其它文章的扑克牌例子:
假设想将扑克牌按照红桃、黑桃、梅花、方块的花色顺序、1~K的点数进行排序。
可以先将牌分成四堆,红桃、黑桃、梅花、方块堆,每一堆里面再按照1~K排序,最终从头红色堆开始拿,就排完序了。
先拆分成大的块,让大块整体有序;逐级递减,继续拆分大块成中快,让中块有序,直到最后剩下单独一位的时候,再排一次序,排序结束。
另外一种想法,不知道是怎么想到的,,,我是想不到,但是确实能实现排序。就是先按最小的排,然后将数组合起来,这样最后一位是有序的。然后按照次高一级排序,这样两级都最小的肯定排在最前的。然后再合并,按照高一级排序,直到最高级,合并数组,完成排序。
从最高级开始排序的,叫做MSD(Most Significant Digit 最大有效数位,就是最高位);从最低级开始排序的,叫做LSD(Least Significant Digit 最低位),两种思路的写法很不相同。
Java代码实现:
// 最高数位,我们再用基数排序的时候,一般这个是已知的,这里我们就直接眼睛看了
private static int topDigit;
private static int resultIndex = 0;
private static int[] result;
public static void main(String[] args) {
int[] arr = new int[]{22,136,11, 124,235,16,268};
// 最高数位
topDigit = 2;
result = new int[arr.length];
// 在方法中不太方便组织结果数组,设置结果数组为全局变量
radixSortMsd(arr, topDigit, arr.length);
System.out.println(Arrays.toString(result));
// 重新乱序数组,使用Lsd方式排序
arr = new int[]{22,136,11, 124,235,16,268};
// lsd可以直接组织出结果数组
radixSortLsd(arr, 0);
System.out.println(Arrays.toString(arr));
// 重新乱序数组,使用Lsd方式排序
arr = new int[]{22,136,11, 124,235,16,268};
radixSortMsdInplace(arr, arr, topDigit, arr.length, 0);
// lsd可以直接组织出结果数组
System.out.println(Arrays.toString(arr));
}
/**
* 从高位排序
* @param arr
* @param currentDigit
* @param length
*/
private static void radixSortMsd(int[] arr, int currentDigit, int length){
// 桶,只需要10位数的桶
// 桶是一个二维数组,长度为本次的最长长度
int[][] buckets = new int[10][length];
// 使用一个辅助计数数组,用来标记桶中的数组中的有效数据的个数
int[] bucketsSize = new int[10];
// 循环arr,取arr[i]当前位的值,作为桶的下标,并计数
for(int i = 0; i < length; i++){
int bucketIndex = getCurrentIndexValue(arr[i], currentDigit);
buckets[bucketIndex][bucketsSize[bucketIndex]] = arr[i];
// 计数器+1
bucketsSize[bucketIndex]++;
}
// 继续递归,直到位置为0
for(int i = 0; i< buckets.length;i++){
int actualSize = bucketsSize[i];
// 不论何时,只要actualSize == 0,都可以跳过
if(actualSize <= 0){
continue;
}
// 如果数组中只有一个元素,即使目前仍然在高位,也没必要继续递归下去了
// 将数据放入新数组,直接进去下一次循环
int[] bucket = buckets[i];
if(actualSize == 1){
result[resultIndex] = bucket[0];
resultIndex++;
continue;
}
// 如果数组有多个元素,顺序输出这多个元素
currentDigit--;
if(currentDigit < 0){
// 将数组中的元素顺次输出
for(int j = 0; j < actualSize;j++){
result[resultIndex] = bucket[j];
resultIndex++;
}
return;
}
// 否则进入下一次递归
radixSortMsd(buckets[i], currentDigit, bucketsSize[i]);
}
}
/**
* 从高位排序
* @param arr
* @param currentDigit
* @param length
* @param startIndex 开始index
*/
private static void radixSortMsdInplace(int[]result, int[] arr, int currentDigit, int length, int startIndex){
// 桶,只需要10位数的桶
// 桶是一个二维数组,长度为本次的最长长度
int[][] buckets = new int[10][length];
// 使用一个辅助计数数组,用来标记桶中的数组中的有效数据的个数
int[] bucketsSize = new int[10];
// 循环arr,取arr[i]当前位的值,作为桶的下标,并计数
for(int i = 0; i < length; i++){
int bucketIndex = getCurrentIndexValue(arr[i], currentDigit);
buckets[bucketIndex][bucketsSize[bucketIndex]] = arr[i];
// 计数器+1
bucketsSize[bucketIndex]++;
}
// 继续递归,直到位置为0
for(int i = 0; i< buckets.length;i++){
int actualSize = bucketsSize[i];
// 不论何时,只要actualSize == 0,都可以跳过
if(actualSize <= 0){
continue;
}
// 如果数组中只有一个元素,即使目前仍然在高位,也没必要继续递归下去了
// 将数据放入新数组,直接进去下一次循环
int[] bucket = buckets[i];
if(actualSize == 1){
result[startIndex] = bucket[0];
startIndex++;
continue;
}
// 如果数组有多个元素,顺序输出这多个元素
currentDigit--;
if(currentDigit < 0){
// 将数组中的元素顺次输出
for(int j = 0; j < actualSize;j++){
result[startIndex] = bucket[j];
startIndex++;
}
return;
}
// 否则进入下一次递归
radixSortMsdInplace(result, buckets[i], currentDigit, bucketsSize[i],startIndex);
// 这一步很关键,startIndex仍然是上一次递归进去前的startIndex
// 这一步要将进入递归内的数计算在内
startIndex += actualSize;
}
}
/**
* 获取当前位置的值
* @param value
* @param index
* @return
*/
private static int getCurrentIndexValue(int value, int index){
String s = String.valueOf(value);
// 假设value是100,数是3,取0
// 假设value是100,数是2,取1
// 如果s的长度比index还小,说明没到这个位数
if(s.length() - 1 < index){
return 0;
}
// 返回从左边计算的index位置的值
return Integer.valueOf(String.valueOf(s.charAt(s.length() - 1 - index)));
}
/**
* 从低位开始排序
* @param arr
* @param currentDigit
*/
private static void radixSortLsd(int[] arr, int currentDigit){
int length = arr.length;
// 桶,只需要10位数的桶
// 桶是一个二维数组,长度为本次的最长长度
int[][] buckets = new int[10][length];
// 使用一个辅助计数数组,用来标记桶中的数组中的有效数据的个数
int[] bucketsSize = new int[10];
// 循环arr,取arr[i]当前位的值,作为桶的下标,并计数
for(int i = 0; i < length; i++){
int bucketIndex = getCurrentIndexValue(arr[i], currentDigit);
buckets[bucketIndex][bucketsSize[bucketIndex]] = arr[i];
// 计数器+1
bucketsSize[bucketIndex]++;
}
// 将数据合起来
int newIndex = 0;
for(int i = 0; i < buckets.length;i++){
//
int bucketSize = bucketsSize[i];
if(bucketSize <= 0){
continue;
}
int[] bucket = buckets[i];
for(int j = 0; j < bucketSize;j++){
arr[newIndex] = buckets[i][j];
newIndex++;
}
}
currentDigit++;
// 如果已经推进过了最高位
if(currentDigit > topDigit){
return;
}else{
radixSortLsd(arr, currentDigit);
}
}
MSD代码解释。。先写的代码(*/ω\*):
由于MSD思路直接,正向,所以我们先看MSD
1、理论上应该找到最高位先,但是这种情况一般数据特征会提前知道,所以我们直接用眼睛看,把高位定下来,当然也可以轮询一遍数组。。。
2、从最高级开始排序,使用桶排序进行排序。
3、遍历所有的桶
1)如果桶是空的,continue
2)如果桶中有一个,即使现在是最高级,也没有必要递归了,这一个分块的排序已经结束,直接输出,或者保存到结果数组中
3)如果桶中有多个,判断当前是否最低级,如果是最低级,可以输出结果,或将结果保存到结果数组中,当前分块排序结束。如果不是最低级,还需要递归进行排序,直到最低数级。
4、需要一个辅助的数组,和原数组长度相同,并且由于是递归,最简单的写法是将结果数组和结果数组的index作为全局变量。
5、另一种写法是,我们不需要辅助数组,就在原数组插入值,当然,桶的辅助数组必不可少。这么考虑,递归的时候,原数组已经没卵用了,已经被拆成一个个块放入桶中了,我们可以直接放入原数组中。
需要将index带着走,每次桶中有数据进入到排序尽头的时候,需要将index加上去。
LSD代码解释:
1、从0开始递归,桶排序,然后合起来。然后数位+1,继续排序,合并,一直到跨国最高位,合并,排序结束
2、没了,这种代码简单,所以很多例子喜欢用这种。。。
复杂度:
基于计数排序,所以是O(n+k),需要桶数组的辅助空间,但是由于被拆成了位数,桶的空间大大降低,并且每次用完都可以释放,不用一次性占用很大的空间。
稳定性:
基于计数排序,具有稳定性。