键索引计数法
我们先介绍一种适合小整数键的简单排序方法,这是我们将要学习的字符串排序的基础,举个例子,我们希望将全班学生按组分类。如图
姓名 |
An |
Br |
Da |
Ga |
Ha |
Ja |
Jh |
Jn |
Ma |
组号 |
2 |
3 |
3 |
4 |
1 |
3 |
4 |
3 |
1 |
姓名 |
Mb |
Mi |
Mo |
Ro |
Sm |
Ta |
Ta |
Tp |
Wh |
组号 |
2 |
2 |
1 |
2 |
4 |
3 |
4 |
4 |
2 |
姓名 |
Wl |
Ws |
|
||||||
组号 |
3 |
4 |
|
我们这里用数组a[]来存储每个元素,其中每个元素都包含=一个名字和一个组号,a[i].key()返回元素的组号。
排序后的结果
姓名 |
Ha |
Ma |
Mo |
An |
Mb |
Mi |
Ro |
Wh |
Br |
组号 |
1 |
1 |
1 |
2 |
2 |
2 |
2 |
2 |
3 |
姓名 |
Da |
Ja |
Jn |
Ta |
Wl |
Ga |
Jh |
Sm |
Ta |
组号 |
3 |
3 |
3 |
3 |
3 |
4 |
4 |
4 |
4 |
姓名 |
Tp |
Ws |
|
||||||
组号 |
4 |
4 |
|
排序共分为四个步骤:
(一)频率统计
组号在0-R之间,键为组号,用一个int数组(初始全为0)统计每个键出现的频率,如果键为r,则count[r]++,但是在实际使用中我们用count[r+1]++,至于为什么将r加1,这是为了计算方便(下一步详细说明)
我i们先来进行统计:
An在第二组中,count[2+1]加1,count[3]==1继续扫描数组,Br在第三组,count[4]+1,count[4]==1,继续扫描,Da在第三组中,count[4]+1,count[4]==2……
扫描一遍数组得到count[0]=0,count[1]=0,count[2]=3,count[3]=5,count[4]=6,count[5]=6,即键1,2,3,4出现的次数分别为3,5,6,6次。
将频率转化为索引:
用count[]来计算每个键在排序结果中的索引位置,例如,第一组有三个人,第二组有5个人,那么第三组的同学在排序结果数组中的位置一定是8。
如图:下标从0开始
1 |
1 |
1 |
2 |
2 |
2 |
2 |
2 |
3(下标为8) |
…… |
即
键1下标开始count[1]=count[1]+count[0]=0
键2下标开始count[2]=count[2]+count[1]+count[0]=3.
键3下标开始count[3]=count[3]+count[2]+count[1]+count[0]=8
即对于每个键r,小于r+1的键的频率之和为小于r的键的频率之和在加上count[r]。
这样我们就能直观的看出来,计算某个键的起始位置,只需要计算count[这个键]+……count[0]即可,count[]数组的根本目的在于计算并存储索引位置,不在于存储键的频率。
数据分类
‘在将count[]数组转化为一张索引表后,我们将所有元素移到一个辅助数组aux[]中进行排序,每个元素在aux[]中的位置是由它的键(组号)决定的,在移动后将count[]中对应的元素加1,这个过程只需要遍历一遍数组即可完成。这种排序是稳定的。
for(int i=0;i<a.length;i++)
{ aux[count[a[i].key()]++]=a[i];
}
其中a[i].key()获取元素的组号,count[a[i].key()]++来保证下一个元素的索引位置。
步骤如图:
分类前:
aux[]
Count[1] |
|
|
Count[2] |
|
|
|
|
Count[3] |
|
|
|
|
|
Count[4] |
|
|
|
|
|
分类中:
1 |
Count[1 |
|
2 |
2 |
Count[2] |
|
|
3 |
3 |
3 |
3 |
Count[3] |
|
4 |
4 |
Count[4] |
|
|
|
分类后:
1 |
1 |
1 |
2 |
2 |
2 |
2 |
2 |
3 |
3 |
3 |
3 |
3 |
3 |
4 |
4 |
4 |
4 |
4 |
其中count[]指向3,count[2]指向8……
回写:
将辅助数组中的元素移动到原数组中
相关代码:
int N=a.length;
String aux[]=new String[N];
int[] count=new int[R+1];
//计算出现的次数
for(int i=;i<N;i++)
{
count[a[i].key()+1]++;
}
//将频率转化为索引
for(int r=0;r<R;r++)
{
count[r+1]+=count[r];
}
//将元素分类
for(int i=;i<N;i++)
{
aux[count[a[i].key()]++]=a[i];
}
//回写
for(int i=;i<N;i++)
{
a[i]=aux[i];
}
低优先的字符串排序
一般称为低位优先,对于一个字符串,从右向左扫描,这个方法依赖于我们上面介绍的键索引记数法,非常适合排序定长的字符串,比如身份证,车牌号,IP地址等。
代码实现:
1 public class LSD { 2 3 public static void sort(String[] a,int w) 4 { 5 int N=a.length; 6 int R=256;//使用扩展的ASCII字符集 7 String[] aux=new String[N]; 8 9 for(int d=w-1;d>=0;d--) 10 11 { 12 int[] count=new int[R+1]; 13 for(int i=0;i<N;i++) 14 { 15 16 count[a[i].charAt(d)+1]++; 17 } 18 19 for(int r=0;r<R;r++) 20 { 21 count[r+1]+=count[r]; 22 } 23 24 for(int i=0;i<N;i++) 25 { 26 aux[count[a[i].charAt(d)]++]=a[i]; 27 } 28 29 for(int i=0;i<N;i++) 30 { 31 a[i]=aux[i]; 32 } 33 } 34 } 35 36 public static void main(String[] args) { 37 String[] a= {"564","964","637","159"}; 38 System.out.println(a[1].charAt(2)); 39 sort(a,3); 40 for(String s:a) 41 { 42 System.out.println(s); 43 } 44 } 45 46 }
要将每个元素均为含有w个字符的字符串数组a[]排序,需要进行w次键索引计数排序;从右向左,以每个字符为键排序一次。
高位优先的字符串排序:
如果要处理的字符串的大小不同,我们应该考虑从左向右遍历所有字符,例如以a开头的字符串应该排在以b开头的字符串前面。首先用键索引计数法将所有字符串按首字母排序,然后(递归的)将每个首字符对应的子数组排序(忽略首字母,因为每个首字母都是相同的)。和快速排序一样,高位的字符会将数组切分为能够独立排序的子数组来完成排序任务,但是它产生的切分会分为每个首字母得到一个子数组,而不是像快速排序那样产生固定的两个或三个切分。
对字符串末尾的约定:
一个合理的做法是将所有字符已经被检查过的字符串所在额子数组排在所有子数组的前面。这样就不需要递归的将该子数组排序。我们使用charAt()方法接收两个参数,并将字符串中的字符索引转化为数组索引,当指定的位置超过了字符串的末尾时该方法返回-1。然后我们将所有返回值加1得到一个非负的int值并用它作为count[]的索引,即0表示字符串末尾,1表示字符串第一个字符。。。。。。所以每个字符可能产生R+1种可能的位置情况。又因为键索引计数法本来就需要一个额外的位置,所以count[]=new int[R+1+1];这里的R指的是字符串中使用的字符集所包含的字符个数,如:字符集为ASCII,则R=128;扩展ASCII,R=256,Unicode,R=65536。当然你也可以选择自定义,如果字符串里只包含英文字母的大小写,则R=26+26=52;
代码实现:
1 public class MSD { 2 3 private static int R=256;//扩展ASCII 4 private static final int M=15;//小数组的切换阈值。 5 private static String[] aux; 6 7 public static void sort(String[] a) 8 { 9 int N=a.length; 10 aux=new String[N]; 11 sort(a,0,N-1,0); 12 } 13 private static void sort(String[] a,int begin,int end,int index) { 14 if(end<begin+M) { 15 insertion(a,begin,end,index); 16 return; 17 } 18 19 int count[]=new int[R+2]; 20 for(int i=begin;i<=end;i++) 21 { int c=charAt(a[i],index); 22 count[c+2]++; 23 /* 24 * 对于键r我们是存储在count[r+1]里的,例如字符串are(假设a是字符集里的第一个字符),我们将a的频率存在count[2] 25 中,在调用charAt()方法时,chartAt("are",0)返回的结果其实是0,因为我们实际上调用的是String类里的 26 方法,数组下标从0开始,所以这里需要加2来保证正确性。 27 */ 28 } 29 30 for(int r=0;r<R+1;r++) 31 { 32 count[r+1]+=count[r]; 33 } 34 35 for(int i=begin;i<end;i++) 36 { 37 aux[count[charAt(a[i],index)+1]++]=a[i]; 38 } 39 40 for(int i=begin;i<=end;i++) 41 { 42 a[i]=aux[i-begin]; 43 } 44 45 for(int r=0;r<R;r++) 46 { 47 sort(a,begin+count[r],begin+count[r+1]-1,index+1); 48 } 49 } 50 //插入排序 51 private static void insertion(String[] a,int begin,int end,int index) 52 { 53 for(int i=begin;i<=end;i++) 54 for(int j=i;j>begin&&less(a[j],a[j-1],index);j++) 55 { 56 exch(a,j,j-1); 57 } 58 } 59 60 private static int charAt(String s,int index) 61 { 62 if(index<s.length()) 63 return s.charAt(index); 64 65 else 66 return -1; 67 } 68 69 70 71 72 73 private static void exch(String[] a, int i, int j) { 74 String temp = a[i]; 75 a[i] = a[j]; 76 a[j] = temp; 77 } 78 79 private static boolean less(String v,String w,int index) 80 { 81 int min=Math.min(v.length(), w.length()); 82 for(int i=index;i<min;i++) 83 { 84 if(v.charAt(i)<w.charAt(i)) return true; 85 if(v.charAt(i)>w.charAt(i)) return false; 86 } 87 return v.length()<w.length();//如果指定位置的字符都相等,则比较长度 88 } 89 public static void main(String[] args) { 90 String[] a= {"159","126","654","354","681"}; 91 sort(a); 92 for(String s:a) 93 { 94 System.out.println(s); 95 } 96 } 97 98 }
算法分析:
小数组(字符串)问题:
当我们需要排序的字符串有数百万个(ASCII集),但是大部分字符串的长度都在10左右,如果我们不对这些小型数组进行处理,那么每次排序都需要初始化258个count[]里的元素并将它们都转化为索引,这部分需要的代价很高,因此对小型的字符串进行插入排序是很有必要的。
等值键:
对于含有大量等值键的子数组排序会非常慢,最坏二点情况就是所有的键都相同。在实际问题中,我们可能会遇到大量含有相同前缀的字符串。(解决的方法是可以使用三向字符串快速排序)
额外空间:
为了切分,我们使用了两个辅助数组,aux[]和count[]。