Java排序算法之基数排序
算法概述
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。本文基于正整数来讲解。
算法思想
基数排序的主要思路是,将所有待比较数值(注意,必须是正整数)统一为同样的数位长度,数位较短的数前面补零. 然后, 从最低位开始, 依次进行一次稳定排序(我们常用上一篇blog介绍的计数排序算法, 因为每个位可能的取值范围是固定的从0到9).这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列.
基数排序可以采用两种方式:
LSD(Least Significant Digital):从待排序元素的最右边开始计算(如果是数字类型,即从最低位个位开始)。
MSD(Most Significant Digital):从待排序元素的最左边开始计算(如果是数字类型,即从最高位开始)。
我们以LSD方式为例,从数组R[1..n]中每个元素的最低位开始处理,假设基数为radix,如果是十进制,则radix=10。基本过程如下所示:
- 计算R中最大的元素,求得位数最大的元素,最大位数记为distance;
- 对每一位round<=distance,计算R[i] % radix即可得到;
- 将上面计算得到的余数作为bucket编号,每个bucket中可能存放多个数组R的元素;
- 按照bucket编号的顺序,收集bucket中元素,就地替换数组R中元素;
- 重复2~4,最终数组R中的元素为有序。
补充知识点
顺便介绍下计数排序和桶排序,便于理解基数排序。
计数排序
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量内存。计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
算法的步骤如下:
1. 找出待排序的数组中最大和最小的元素
2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
桶排序
桶排序假设序列由一个随机过程产生,该过程将元素均匀而独立地分布在区间[0,1)上。我们把区间[0,1)划分成n个相同大小的子区间,称为桶。将n个记录分布到各个桶中去。如果有多于一个记录分到同一个桶中,需要进行桶内排序。最后依次把各个桶中的记录列出来记得到有序序列。
桶排序的平均时间复杂度为线性的O(N+C),其中C为桶内快排的时间复杂度。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。 当然桶排序的空间复杂度 为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。
算法源码
参考拾毅者的源码
/**
* 基数排序
* @author Administrator
*
*/
public class RadixSort {
public static void main(String[] args) {
Integer[] array = new Integer[] { 1200, 292, 121, 72, 233, 44, 12 ,8};
radixSort(array, 10, 4);
System.out.println("排序后的数组:");
print(array);
}
/*
* array 代表待排序数组
* radix 代表基数 。十进制为10
* d 代表排序元素的位数 ,待排序数组 中最大的数的位数
*/
public static void radixSort(Integer []array, int radix, int d){
// 临时数组
Integer[] tempArray = new Integer[array.length];
// count用于记录待排序元素的信息,用来表示该位是i的数的个数
Integer[] count = new Integer[radix];
int rate = 1;
for (int i = 0; i < d; i++) {
//重置count数组,开始统计下一个关键字
Arrays.fill(count, 0);
//将array中的元素完全复制到tempArray数组中
System.arraycopy(array, 0, tempArray, 0, array.length);
//计算每个待排序数据的子关键字
for (int j = 0; j < array.length; j++) {
int subKey = (tempArray[j] / rate) % radix;
count[subKey]++;
}
//统计count数组的前j位(包含j)共有多少个数
for (int j = 1; j < radix; j++) {
count[j] = count[j] + count[j - 1];
}
//按子关键字对指定的数据进行排序 ,因为开始是从前往后放,现在从后忘前读取,保证基数排序的稳定性
for (int m = array.length - 1; m >= 0; m--) {
int subKey = (tempArray[m] / rate) % radix;
array[--count[subKey]] = tempArray[m]; //插入到第--count[subKey]位,因为数组下标从0开始
}
rate *= radix;//前进一位
System.out.print("第" + (i+1) + "次:");
print(array);
}
}
//输出数组===============
public static void print(Integer[] array) {
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + "\t");
}
System.out.println();
}
}
测试效果:
第1次:1200 121 292 72 12 233 44 8
第2次:1200 8 12 121 233 44 72 292
第3次:8 12 44 72 121 1200 233 292
第4次:8 12 44 72 121 233 292 1200
排序后的数组:
8 12 44 72 121 233 292 1200
算法分析
时间复杂度
设待排序的数组R[1..n],数组中最大的数是d位数,基数为r(如基数为10,即10进制,最大有10种可能,即最多需要10个桶来映射数组元素)。处理一位数,需要将数组元素映射到r个桶中,映射完成后还需要收集,相当于遍历数组一遍,最多元素书为n,则时间复杂度为O(n+r)。所以,总的时间复杂度为O(d*(n+r))。
空间复杂度
设待排序的数组R[1..n],数组中最大的数是d位数,基数为r。基数排序过程中,用到一个计数器数组,长度为r,还用到一个r*n的二位数组来做为桶,所以空间复杂度为O(r*n)。
排序稳定性
通过上面的排序过程,我们可以看到,每一轮映射和收集操作,都保持从左到右的顺序进行,如果出现相同的元素,则保持他们在原始数组中的顺序。 可见,基数排序是一种稳定的排序。
总结
LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好,MSD的方式恰与LSD相反,是由高位数为基底开始进行分配,其他的演算方式则都相同。