一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第22天,点击查看活动详情。
之前所有的排序其实都和比较有关,都是一种基于比较的排序,而桶排序不是。
计数排序
我们来看一个例子,例如给你一个arr,已经告诉你这个是员工年龄了,让你去排序,这个时候我们可以根据实际情况 - 年龄区间一般在0-200且都为正整数,来生成一个新的数组,长度为201,然后我们遍历原数组,例如遍历到33,那么我们就给新数组index为33的地方++....完成遍历之后就得到了频次表了,然后平铺开就称完成了排序,复杂度为O(N)
上述这种方式如果比较的数字范围很大其实就不行了,必须考虑实际数据状况,所以这种基于数据状况进行的排序没有基于比较(你告诉怎么比大小)的排序通用性强,需要定制化比较高且应用场景固定,但是针对具体场景时间复杂度一般比较好
基数排序
基数排序的总体思路是首先判断出最大数字的位数,例如最大数字为100,那么最大位是百位,然后给所有数字补上缺失位数:
接着对于十进制数字来说,单一位上总共分为0-9个数字,我们配置10个桶对应上去(下面只是画不下了):
然后针对个位数字,十位数字,百位数字按照队列顺序放到对应桶子里面去,然后从左往右依次倒出来:
第一轮保证个位顺序排好了,第二轮是十位:
第三轮是百位数字:
其实就是按照优先级进行个位,十位,百位进行排序,高位数字晚排序,优先级高
上面这个方法比计数排序要好,要通用一些。只要是涉及进制的数字都可以排序。另外如果有负数可以考虑两个解决方案,首先是遍历求位数时顺带找到最小值(负数),所有数减去最小值,然后得到结果后再加回去就行了,或者新增一位代表符号,也进行排序!
但是如果数据状况没有进制就不行了。
代码实现与讲解
// 针对非负值
public static void radixSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
radixSort(arr,0,arr.length - 1, maxbits(arr));
}
public static int maxbits(int[] arr){
// 遍历找到最大值然后求位数
int max = Interger.MIN_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
}
int res = 0;
while(max != 0){
res++;
max /= 10;
}
return res;
}
// arr[L..R]排序
public static void radixSort(int[] arr, int L, int R, int digit){
final int radix = 10;
int i = 0,j = 0;
// 有多少个数字就准备多少辅助空间
int[] bucket = new int[R-L+1];
for(int d = 1; d < digit; d++){ // 有多少位就进出多少次
// 10个空间
// count[0] 当前位(d位)是0的数字有多少个
// count[1] 当前位是0和1的数字有多少个
// count[2] 当前位是0,1,2的数字有多少
...
int[] count = new int[radix]; // count[0...9]
// 取出对应位置数字 - count为词频结果
for(i = L; i <= R; i++){
j = getDigit(arr[i],d);
count[j]++;
}
// 处理成前缀和
for(i = 1;i <radix;i++){
count[i] = count[i - 1] + count[i];
}
for(i = R; i >= L; i--){ // 数组从右到左遍历 - 入bucket
j = getDigit(arr[i],d);
bucket[count[j] - 1] = arr[i];
count[j]--;
}
for(i = L,j=0;i <= R; i++){ // 出桶更新原数组
arr[i] = bucket[j];
}
}
}
这里重点分析一下radixSort内部入桶和出桶的过程,这里面代码实现的逻辑稍微有点绕。
首先我们准备一个count数组,作为0-9号桶记录对应位数的词频,但是注意记录的不是下标出现的次数,而是需要处理成前缀和的形式,这样方便后续出桶排序时候进行分片:
看上图:原先下标2的含义是2数字有多少个,现在是数字≤2的由多少个。有4个
接着我们声明一个和原先arr等长的help数组,然后就是确定位置了,分析顺序是原数组从右往左(先进先出):062这个数字,第一轮是针对个位,个位数字是2,对应前缀和为4,意思即为小于等于2的数字有4个,而062是最先出来的,所以应该放到第4个数字位置,即4-1=3位置,help[3]即为062,同时前缀和-- count[2] = 3,后续同理:
最后结束,基于count数组完成了分片,相当于完成了入桶和出桶过程!