桶排序:
在桶排序思想下的排序都是不基于比较的排序,运用范围有限,需要样本的数据状况满足桶的划分。下面介绍桶排序思想下的两个排序
(1)计数排序 (2)基数排序
时间复杂度
O(N)
计数排序:
之前介绍的所有排序算法如插入排序,堆排序等都是基于比较的排序,除了这些排序算法外,还有不基于比较的排序:
比如将公司员工的年龄进行排序,考虑到实际年龄的范围可以开辟一个大小为100的数组,将所有员工的年龄进行统计并添加到数组中,比如数组第0个位置为年龄为1员工的个数,依次类推。
最后根据数组上每个位置的值(即频数)将年龄全部展开,就可以得到一个排好序的年龄数组。 上述这种排序叫计数排序,
代码实现:
public class CountSort {
public static void countSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
int[] bucket = new int[max + 1];
for (int i = 0; i < arr.length; i++) {
bucket[arr[i]]++;
}
int i = 0;
for (int j = 0; j < bucket.length; j++) {
while (bucket[j]-- > 0) {
arr[i++] = j;
}
}
}
}
基数排序:
假设需要排序的数组为[17,13,25,100,72]:
1.确定所有数中最大数的位数为几位,该数组中最大数为100,即为3位。将数组中所有数进行填充到3位,得到[017,013,025,100,072]
2.所有数字中数字类别为0~9,所以需要0-9这10个桶。使用队列作为桶的数据结构,这样桶在输出数字时是先进先出的(可以是栈,数组等其他结构)
3.根据填充后的数组中不同位数的大小将数字放入桶中:
3.1 先看个位数,可以得到[7,3,5,0,2],根据个位数值将填充数字分别放入这8个桶中:
0号:[100] 1号:[] 2号:[072] 3号:[013]
4号:[] 5号:[025] 6号:[] 7号:[017]
将桶中数字按桶的顺序依次返回得到新数组[100,072,013,025,017]
3.2 再看十位数,可以得到[0,7,1,2,1],根据十位数值将填充数字分别放入这8个桶中:
0号:[100] 1号:[013,017] 2号:[025] 3号:[]
4号:[] 5号:[] 6号:[] 7号:[072]
将桶中数字按桶的顺序依次返回得到新数组[100,013,017,025,072]
3.3 最后看百位数,可以得到[1,0,0,0,0],根据百位数值将填充数字分别放入这8个桶中:
0号:[013,017,025,072] 1号:[100] 2号:[] 3号:[]
4号:[] 5号:[] 6号:[] 7号:[]
将桶中数字按桶的顺序依次返回得到新数组[013,017,025,072,100]代码实现和分析过程是有一定区别的,假设数组arr=[13,21,11,52,62]:
1.创建一个count数组(大小为10)用于记录每个个位数对应的数字个数,所以count数组为[0,2,2,1,0,0,0,0,0,0]
2.计算当前count数组的不同位置值的前缀和并填入count数组中,[0,2,4,5,5,5,5,5,5,5]。其中,2=0+2,4=0+2+2,5=0+2+2+1,以此类推。此时count数组的含义是与1中的不一样,比如4表示的是原数组中个位数<=2的数字个数有4个
3.此时从右往左遍历原数组,即先看62,它的个位数是2,所以排序后放置的位置是bucket数组(辅助数组,用于暂时储存当前排序的数)的第3位上
为什么?这个3=4-1,4为count在个位数为2上的值,因为数组是从0开始的,所以将62放置在第3位置上(即个位数为2的数字还有3个,要放在第0、1、2位置上),这样正好符合原数组中个位数<=2的数字个数有4个,从右开始遍历正好符合进桶的顺序是先进先出。此时将count数组中个位数为2上的值需要减1,注意count其他位置上的数不需要改变。最终bucket数组为[(21,11),(52,62),(13)]。每一个括号内的数可以视为一个桶中的数
4.将bucket数组中的数重新赋值给arr数组,再对其十进制、百进制位…继续上述1~3的操作
代码实现:
public class RadixSort {
// 只针对非负值
public static void radixSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
radixSort(arr, 0, arr.length - 1, maxbits(arr));
}
// arr[L...R]排序
public static void radixSort(int[] arr, int L, int R, int digit) {
// radix设置为10,因为数字的范围(即桶的个数)为0~9
final int radix = 10;
int i = 0, j = 0;
int[] bucket = new int[R - L + 1];
// 根据十进制位的大小决定进出桶几次
for (int d = 1; d <= digit; d++) {
// count表示一个前缀和数组
// count[0]:当前位(d位)是(0)的数字有多少个
// count[1]:当前位(d位)是(0和1)的数字有多少个
// count[i]:当前位(d位)是(0~i)的数字有多少个
int[] count = new int[radix];
// 先计算不同桶需要放置数的个数
for (i = L; i < R; i++) {
j = getDigit(arr[i], d);
count[j]++;
}
// 再计算前缀和
for (i = 1; i < radix; i++) {
count[i] = count[i] + count[i - 1];
}
// 将原数组从右到左重新放置到bucket数组(一个辅助数组)中
for (i = R; i >= L; i--) {
j = getDigit(arr[i], d);
bucket[count[j] - 1] = arr[i];
count[j]--;
}
// 将bucket数组重新放置到原数组中
for (i = L, j = 0; i <= R; i++, j++) {
arr[i] = bucket[j];
}
}
}
// 获取数字x在第d位(从右向左)十进制上的值(可以视为实现了对数组中每一个数的填充)
public static int getDigit(int x, int d) {
return ((x / ((int) Math.pow(10, d - 1))) % 10);
}
// 计算数组中最大值有几个十进制位
public static int maxbits(int[] arr) {
int max = Integer.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;
}
}
tips:
相较于计数排序,基数排序对于数据范围的要求就降低了很多,就算是亿级别的数需要桶的数量还是可控的 。但是对于数据类型就有一定要求了,必须有进制的数据对象才能使用该方法进行排序,再次验证不基于比较的排序是基于数据状况的排序