今天,我来讲一讲基数排序。基数排序是一种用在老式穿卡机上的算法。一张卡片有80列,每一列可以在12个位置中的任一处穿孔。排序器可以被机械地“程序化”,以便对一叠卡片中的每一列进行检查,再根据穿孔的位置将它们放入12个盒子里。这样,操作员就可以逐个地将它们收集起来,其中第一个位置穿孔的放在最上面,第二个其次,等等。对于十进制数字来说,每列中只用到10个位置(另两个位置用于编码非数值字符)。一个d位数占用d个列。因为卡片排序器一次只能查看一个列,因此,要对n张卡片上的d位数进行排序,就需要用到排序算法。
从直觉上来看,人们可能会觉得应该按最高有效位进行排序,然后再对每个盒子中的数递归地排序,最后再把结果合并起来。不幸的是,为排序每一个盒子中的数,10个盒子中的9个必须先放在一遍,这个过程产生了许多需要加以记录的中间卡片堆。
与人们的直觉相反,基数排序是首先按最低有效位数字进行排序,以解决卡片排序问题。同样,把各堆卡片收集成一叠,其中0号盒子中的在1号盒子中的前面,后者又在2号盒子中的前面,等等。然后,对整个一叠卡片按次低有效位排序,并把结果同样地合并起来。重复这个过程,直到对所有的d位数字都进行了排序。所以,仅需要d遍就可以将一叠卡片排好序。
下图说明了基数排序作用于“一叠”共7个3位数的过程。
这里也有很重要的一点,就是基数排序的稳定性,所谓稳定性,是指当有多个相同元素的值时,排序后,他们的相对次序不变。参考计数排序
想想很好理解,因为操作员把卡片从盒子里拿出来时不能改变它们的次序,否则不是乱套了吗,即使数字相同,所属者也可能是不同的。
基数排序要保持稳定,那在对每一位进行排序的时候就必须使用一个稳定的算法,这里我们就可以使用计数排序作为这个子算法。参考计数排序
基数排序的代码是很直观的。
/**基数排序
* @param A 需要排序的数组,返回时已经排序
* @param d 最高位,最低位是1
*/
public int[] RadixSort(int[] A, int d){
int[] B = A.clone();
for(int i = 1; i <= d; i++){
//调用某一稳定排序对A数组进行排序(基于i位)
CountingSort2(A, B, i);
A = B.clone();
}
return A;
}
这里的CountingSort和我之前的博客写的略有区别,对判断的依据做了修改,因为是按位判断
/**
* 计数排序变种,根据某一位进行排序,k不需要,因为每一位数字在0到9之间。
* @param A
* @param B
* @param index
*/
public void CountingSort2(int[] A, int[] B, int index){
int[] C = new int[10];
for(int i = 0; i < A.length; i++){
C[getDigit(A[i], index)]++;
}
for(int i = 1; i < 10; i++){
C[i] += C[i - 1];
}
for(int i = A.length - 1; i >= 0; i--){
B[C[getDigit(A[i], index)] - 1] = A[i];
C[getDigit(A[i], index)]--;
}
}
这里用到了一个函数,是获得某一位,代码很简单:
/**
* 获得数字的某位
* @param digit 数字
* @param i 1-d位
* @return 单位数字
*/
public int getDigit(int digit, int i){
int result = 0;
for(int j = 0; j < i; j++){
result = digit % 10;
digit /= 10;
}
return result;
}
如果对代码有疑惑可以看我的github源码,上面有所有方法的单元测试:https://github.com/qjkobe/IntroductionToAlgorithms
如果发现问题,请立刻告诉我。我可不想误人子弟