在算法导论中第八章专门列出了线性时间的排序算法,常规的基于比较的排序算法的上限是O(nlogn),本章介绍了三个算法,计数排序,基数排序,桶排序。
第一次接触是计数排序是leetcode上面有一道计算论文什么因子的题,基数排序和桶排序在上学期算法课上老师讲过,当时一知半解,一直以为他们两个是同一个算法。最近自习看完了这一张才发现这两个算法是完全不一样的,看了一些博客也有很多的把这两个算法混淆,我觉得这样是不好的,写出来的博客要负责任,不然容易误导新手,我在开始就走了很多弯路,所以想在此总结一下自己的理解。
一、计数排序
1、书上的算法
计数排序假设n个输入元素中的每一个都是在0到k区间内的一个整数,其中k为某个整数。详细的原理算法导论上面讲的很明白,在这里直接给出我的代码。
public static int[] countingSort_extra(int nums[], int k){
int n=nums.length;
int[] re=new int[n];
int[] C=new int[k+1];
for(int i=0; i<n; i++){
C[nums[i]]++;
}
for(int i=1; i<C.length; i++){
C[i]+=C[i-1];
}
for(int i=n-1; i>=0; i--){//必须从后向前遍历,才能保证稳定
re[C[nums[i]]-1]=nums[i];
C[nums[i]]--;
}
return re;
}
书上面说的很明白,计数排序主要有两个特点,一个是不基于比较,可以在线性时间内完成。还有就是稳定的排序,主要是作为下面讲到的基数排序的基础。
有一点就是书后面的思考题问道第10行(上面代码中的11行)的循环可不可以从前向后遍历,根据我的理解,如果从前向后遍历,算法会运行正确,但是不能确保排序的稳定性,所以如果作为基数排序的基础,必须要从后向前遍历以确保稳定性。
2、空间上的改进
上面的方法名之所以叫countingSort_extra是因为排序后的数组存在另一个数组中,所以一共需要两个额外的数组空间,如果改进一下,排序后直接写回原数组,可以节省空间,下面是我的代码
public static void countingSort(int[] nums, int k){
int n=nums.length;
int[] B=new int[k+1];
int count=0;
for(int i=0; i<n; i++){
B[nums[i]]++;
}
for(int i=0; i<=k; i++){
while(B[i]-->0){
nums[count++]=i;
}
}
}
二、基数排序
给定n个d位数,其中每个数位都有k个可能的取值。如果radixSort使用的稳定排序方法耗时O(n+k),那么它就可以在O(d(n+k))时间内将这样数排好序。
他与桶排序是完全不一样的,在此说一下几个用到的关键参数。
d:位数,它决定了内层排序的次数。比如329是一个三位数,d=3,基数排序的原理就是从低到高(注意一定要从低到高)按位比较,所以整个算法会从个位十位百位排序,而每次排序用到的就是上面的计数排序。
k:可能的取值,与上面计数排序的k意义相同,就是可能的取值区间,一般对数字排序的肯定都是0-9的10个取值。
n:就是元素的个数,即nums.length。因为要稳定排序,所以肯定需要额外的n长度数组来保存,在下面我的代码中,我建立了一个temp数组,两个数组依次从一个写到另一个中,这样就不用每次都回写了。
private static int[] c;
//计算num倒数第n位的数字
private static int valInBit(int num, int n){
int temp=1;
while(n>0){
temp*=10;
n--;
}
return num%temp/(temp/10);
}
//按照数组的倒数第n位数字进行排序
private static void countingSort(int[] nums, int[] re, int n){
Arrays.fill(c, 0);
for(int i=0; i<nums.length; i++){
c[valInBit(nums[i], n)]++;
}
for(int i=1; i<c.length; i++){
c[i]+=c[i-1];
}
for(int i=nums.length-1; i>=0; i--){
int val=valInBit(nums[i], n);
int index=c[val];
re[index-1]=nums[i];
c[val]--;
}
System.out.println("step"+n+": "+Arrays.toString(re));
}
/*
* 给定n个d位数,其中每个数位有k个可能的取值
*/
public static void radixSort(int[] nums, int d, int k){
c=new int[k];
int[] temp=new int[nums.length];
for(int i=1; i<=d; i++){
if((i&1)==1){
countingSort(nums, temp, i);
}else{
countingSort(temp, nums, i);
}
}
if((d&1)!=1){
nums=temp;
}
}
public static void main(String[] args) {
int[] nums={329,457,657,839,436,720,355};
radixSort(nums, 3, 10);
System.out.println(Arrays.toString(nums));
}
控制台的输出如下
step1: [720, 355, 436, 457, 657, 329, 839]
step2: [720, 329, 436, 839, 355, 457, 657]
step3: [329, 355, 436, 457, 657, 720, 839]
[720, 329, 436, 839, 355, 457, 657]
三、桶排序
桶排序将[0,1)区间划分为n个相同大小的子区间,或称为桶。然后将n个输入数分别放到各个桶中。为了得到输出结果,我们先对每个桶中的数进行排序,然后遍历每个桶,按照次序把各个桶中的元素列出来即可。
以上是算法导论中的说法,说实话这节我并没有看太明白,尤其是后面证明时间代价是O(n)。
我个人的理解,桶排序的关键在于桶的划分,有点类似于设计哈希函数,使得各个元素均分布在各个桶中,然后对每个桶中的元素进行排序,最后再统计到一起。
下面是书中的一个例子,对10个元素划分10个桶,然后按第一位小数对应各个桶中。
下面是我的代码,用ArrayList来代替了图中的链表。
private static int[] c;
//计算num倒数第n位的数字
private static int valInBit(int num, int n){
int temp=1;
while(n>0){
temp*=10;
n--;
}
return num%temp/(temp/10);
}
//按照数组的倒数第n位数字进行排序
private static void countingSort(int[] nums, int[] re, int n){
Arrays.fill(c, 0);
for(int i=0; i<nums.length; i++){
c[valInBit(nums[i], n)]++;
}
for(int i=1; i<c.length; i++){
c[i]+=c[i-1];
}
for(int i=nums.length-1; i>=0; i--){
int val=valInBit(nums[i], n);
int index=c[val];
re[index-1]=nums[i];
c[val]--;
}
System.out.println("step"+n+": "+Arrays.toString(re));
}
/*
* 给定n个d位数,其中每个数位有k个可能的取值
*/
public static void radixSort(int[] nums, int d, int k){
c=new int[k];
int[] temp=new int[nums.length];
for(int i=1; i<=d; i++){
if((i&1)==1){
countingSort(nums, temp, i);
}else{
countingSort(temp, nums, i);
}
}
if((d&1)!=1){
nums=temp;
}
}
public static void main(String[] args) {
int[] nums={329,457,657,839,436,720,355};
radixSort(nums, 3, 10);
System.out.println(Arrays.toString(nums));
}
控制台输出如下
[0.12, 0.17]
[0.21, 0.23, 0.26]
[0.39]
[0.68]
[0.72, 0.78]
[0.94]
[0.12, 0.17, 0.21, 0.23, 0.26, 0.39, 0.68, 0.72, 0.78, 0.94]
四、总结
以上是我根据书上的内容结合自己的理解写的,代码全部是自己敲的并且经过少量测试,个人水平有限不敢保证完全没有错误,主要用于自己总结加深理解,如有错误欢迎指正大家一起学习进步。