我们现在已经知道了常用了基于比较的排序算法。现在聊聊基于非比较的排序算法——桶排序,计数排序和基数排序。
1.桶排序:其实我认为桶排序其实是一类算法的统称,它的思想是将待排序的数按照他们的数据范围,在这个范围上平均的分配M个桶,然后扫描一遍输入,将所有元素放入对应桶中,如果桶中有多于一个元素,则用插入排序将桶内的元素进行排序。最后按顺序输出所有桶与其内部元素。
它的时间复杂度为O(N+c),c为桶内的插入排序的时间复杂度,相应的空间复杂度为O(N+M),M为桶数。相当于利用桶同时进行M个排序,这是一种典型的用空间换取时间的策略。最要的一点是,所有的桶排序类的排序都是稳定的。
2.计数排序:当桶排序中每个桶的大小为1的时候,就叫做计数排序,它是桶排序的一种变形。那么我们具体该怎么实现呢,下面我们一起来推敲下它该怎么实现。
实现过程:当对数组或字符串进行排序时,如果能知道每个元素应该放置在哪里就很好办了,那么怎么知道元素该被放在哪里呢?答案是计算出元素要被放置的起始位置。那么起始位置该如何得到呢?起始位置我们可以通过累加在该元素所在桶之前所有桶的元素个数总和来得到。我们又可以统计每个桶的元素个数。那么我们我计数排序就完成了。
import java.util.*;
public class CountingSort {
private final static int R = 1000; // 构建1000个桶 (排序0-999之间的数)
public static int[] countingSort(int[] A, int n){
int[] aux = new int[n];
int[] count = new int[R + 1];
//1.统计出现频率
for(int i=0; i<A.length; i++){
count[A[i] + 1]++;
}
//2.根据频率确定起始下标
for(int i=0; i<R; i++){
count[i+1] += count[i];
}
//3.在辅助数组中记录有序元素
for(int i=0; i<A.length; i++){
aux[count[A[i]]++] = A[i];
}
//4.回写
for(int i=0; i<A.length; i++){
A[i] = aux[i];
}
return A;
}
}
count数组的目的是得到每个元素的起始位置,为了得到这个目的,就要统计每个桶中元素的出现频率。注意这里的count数组申请的大小是R+1个,原因就是通过计算得到每个元素前面有多少个元素(元素的起始坐标)。时间复杂度是(N+ R),空间复杂度也是O(N+R),这里的时间复杂度和桶排序说的不一样是因为计数排序这种实现并没有让M个桶内元素并行计算而是串行计算,M如果远小于N则可以忽略不计。
3.基数排序:分为低位优先和高位优先,是对计数排序的一种改进,首先看看低位优先(LSD)
思路:从低到高扫描每个元素的位,按位分别进行独立的排序,利用计数排序的稳定性,得到最终的结果。
LSD主要有2种应用:对数字的排序和对字符串的排序
对数字排序:
import java.util.*;
public class RadixSort {
public static int[] radixSort(int[] A, int n) {
int d = A[0]; //输入数据中最大的数
for(int i : A){
if(i > d){
d = i;
}
}
int position = 1; //从个位开始排序
int index = 0; //从桶中放回原数组过程中原数组的下标
int bucket[][] = new int[10][n]; //申请10个桶,每个桶大小为数组的长度(适应每个数组的对应为都相等的情况)
int count[] = new int[10]; //记录每个桶的大小
while(d > position){
//遍历数组A,并放入桶中
for(int i=0; i<n; i++){
int digit = (A[i] / position) % 10; //桶号
bucket[digit][count[digit]++] = A[i];
}
//将桶中的数组按从小到大的顺序放回
for(int i=0; i<10; i++){
if(count[i] != 0){
for(int j=0; j<count[i]; j++)
A[index++] = bucket[i][j];
}
count[i] = 0;
}
index = 0;
position *= 10;
}
return A;
}
}
对字符串的排序:
//将字符串的前W位按照低位优先的方式进行排序,可以处理字符长度相同的数据
public class LSD {
public static void sort(String[] a, int w){
int N = a.length;
int R = 256;
String[] aux = new String[N];
for(int d=w-1; d>=0; d--){
int[] count = new int[R+1];
for(int i=0; i<N; i++){ //统计数组中的每一个字符串中的某位中字符出现的频率
count[a[i].charAt(d)+1]++;
}
for(int i=0; i<R; i++){ //计算每组字符的位置
count[i+1] += count[i];
}
for(int i=0; i<N; i++){ //根据计算位置,把整个字符串防在相应的位置上
aux[count[a[i].charAt(d)]++] = a[i];
}
for(int i=0; i<N; i++){ //回写
a[i] = aux[i];
}
}
}
}
HSD在此不过多讨论了:主要思想就是利用嵌套从高位到低位进行排序,这里要注意的是当元素已经到达结尾的时候,应该放在当前排序的最前面。下面是它的实现:
public static void sort(String[] a){
int N = a.length;
aux = new String[N];
sort(a, 0, N-1, 0);
}
//以第d位为键把字符串lo到hi排序
private static void sort(String[] a, int lo, int hi, int d){
if(hi <= lo) return;
int[] count = new int[R+2]; // 多一个-1(没有找到指定d位置的字符)位置
for(int i=lo; i<=hi; i++){
count[charAt(a[i], d)+2]++;
}
for(int r=0; r<R+1;r++){
count[r+1] += count[r];
}
for(int i=lo; i<=hi; i++){
aux[count[charAt(a[i], d) + 1]++] = a[i];//这里+1的效果把小于d的字符串放在了aux[0]上
}
for(int i=lo; i<=hi; i++){ //aux数组只使用了(0到hi-lo)
a[i] = aux[i - lo];
}
for(int r=0; r<R; r++){
sort(a, lo+count[r], lo+count[r+1]-1, d+1);
}
}
private static int charAt(String s, int d) {
if(d < s.length()) return s.charAt(d);
else return -1;
}
好了,关于排序的话题我们就聊到这里了,下次我们说说算法中的第二个主题:关于查找的数据结构与相应算法。