对于很多排序应用,决定顺序的都是字符串。我们当然可以用通用的排序方法如插入排序、选择排序、归并排序、快速排序等诸多方法将其排序。但是由于字符串的特殊性质,我们可以选择更为快速的方式进行排序。低位优先排序、高位优先排序、以及三向字符串快速排序。
目录
一、键索引计数法
在学习低位优先排序、高位优先排序、以及三向字符串快速排序之前,先学习一种适用于小整数键的简单排序方法----键索引计数法。
对班级里的同学进行分组,分为1、2、3、4, 以组别为键进行排序分类。对于类似这种小整数键排序,键索引计数法简洁又快速。
索引计数法需要两个额外的数组一个是count记录键出现的次数,另一个是辅助数组用于将数据分类排序。分为四个步骤:
1、记录键出现的频率
2、将频数转化为索引
3、数据分类
4、回写
1、记录键出现的频率
int[] count = new int[R+1]; //R是字母表字符数量,一般默认字母表是ASCII字母表,在这个例子中只有1、2、3、4,我们可以只使用ASCii码表的前5位{0,1,2,3,4,5},所以在该例子中R==5
String[] aux = new String[N]; //N是数据条目数,例如上述分组N代表学生数
for(int i = 0;i<N;i++)
{
count[a[i].key()+1]++; 为什么要在a[i].key()+1的位置++,而不是a[i].key()++
}
因为组别是数组1、2、3、4,所以我们直接用键(组别号)作为索引。统计频率是为了给转化为索引打基础。
2、将频率转化为索引
第一步将频率统计好后结果如下
为什么要在索引+1的位置来记录频率呢,原因就是便于转化为索引。如,第1组有三个人,那么count[1]位置的0代表的是第1组第一个人放入辅助数组aux的位置,也就是Harris放在 aux[ 0 ]的位置。第2组的起始索引就放在 aux[ 3 ]的位置,即Anderson放在 aux[ 3 ]。
所以只需要将该键及其之前的数相加即可得到该键放在辅助数组的起始位置。
代码实现如下
for(int i = 0;i<R;i++)
count[i+1] +=count[i];
这样一来频率便都转化为了索引。
3、数据分类
所谓数据分类其实就是根据数据的键进行分类,在上述例子中即把第1、2、3、4组的人分类到一块儿,同时也是排序的过程。
例如在遍历原始数据时,第一个是Anderson, 键值为2,由于上一步我们已经将count数组转化为索引,count[2] = 3, 所以Anderson就可以放在辅助数组aux[ 3 ]的位置上,同时count[2]++即count[2] = 4, 因为当遍历到下一位第2组的同学Martinez时,便可以将其放置在aux[ 4 ] 的位置上紧挨着Anderson,然后别忘了count[2]++。这样一来便可以将各组的同学按照组别分在一块儿。
for(int i = 0;i<N;i++)
{
aux[count[a[i].key()]++] = a[i]; 数组a是原始数组
}
该过程可拆解为三部分
1、a[i].key()+1 获得该键起始位置的索引
2、count[...]++ 更新该键的起始位置索引
3、aux[...] = a[i] 填充辅助数组来进行数据分类
4、回写
辅助数组aux填充好后回写原始数组,从而达到彻底分类排序的目的
for(int i =0;i<N;i++)
{
a[i] = aux[i];
}
二、低位优先的排序方法
所谓低位优先排序法就是把字符串看成是一个R进制的数字,从字符串最右边的字符开始,将其作为键进行排序。低位优先算法仅适用于字符串长度一致的情况。
如在一繁华路段统计同一辆车经过该路段的次数
正如前面所说: 只有所有要排序的字符串的长度一致的时候,才能使用低位优先排序算法,所有车牌号的长度自然是一致的。
把字符串看成一个R进制的整数,如“4PGC938”, 由于车牌号只有数字和大写字母组成,所以我们可以将车牌看作是长度为7的36进制的整数。像该例子中所有字符串长度均为7,从左向右将以每个位置的字符作为键,用键索引计数法将字符串排序7遍,相当于从整数的最低位开始排序。所以称之为低位优先排序。
public class LSD
{
public static sort(String[] a, int w) //a是要排序的数组,w是字符串的统一长度
{
int N = a.length;
int R = 256; //默认使用的ASCII码
for(int d = w-1;d>0;d--) //从最右边的字符开始,依次为键,进行键索引计数法排序
{
int[] count = new int[R+1];
String[] aux = new String[N];
//统计频率
for(int i = 0;i<N;i++)
{
count[a[i].charAt(d)+1]++;
}
//转化为索引
for(int r = 0;r<R;r++)
{
count[r+1] += count[r];
}
//数据分类
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];
}
}
}
}
三、高位优先排序
低位优先排序使用的是迭代法进行排序,因为其要处理的字符串都是等长的,迭代次数不会超过字符串长度。但是对于字符串长度不一时,我们无法确切知道需要迭代的次数,所以使用递归法看起来更合适一些。
所谓高位优先排序就是将字符串从左到右的字符依次为键进行排序。
先将一个完整的大数组分割成若干小的数组,然后递归的排序直到排序完成。
比如我们在排序下列字符串时
各字符串长短不一,当我们用高位优先算法排序时首先用最左边的第一个字符为键,使用键索引计数法将完整数组分为了若干小数组----[ are ]、[ by ]、[ she, sells, ......]、[the, the]
在这里排序时使用键索引计数法,同样需要两个辅助数组,一个是count[ ] 统计频率并转化为索引,另一个是aux[ ]用于数据分类和回写。这里使用的字母表是Alphabet.LOWCASE(小写字母表有26个不同的字符即R = 26), 所以按照之前的逻辑count数组的大小似乎只需要为R+1(也就是27即可), 以'a'为键的字符串有1个,就在'a'+1的位置记录1;以'b'为键的字符串有1个,就在'b'+1的位置记录1; 以's'为键的字符串有10个,就在's'+1的位置记录10......
count数组, 0对应a, 1对应b...... | |||||||||||||||||||||||||||
0(a) | 1(b) | 2(c) | 3(d) | 4(e) | 5(f) | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18(s) | 19(t) | 20(u) | 21 | 22 | 23 | 24 | 25 | 26 | |
0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 10 | 2 | |||||||
然后将频率转化为索引就是每个字符在数组的起始位置(表格省略),如以字符'a'开头的字符串起始位置就是0,把"are"放入辅助数组aux[0];"by"就放在aux[1];......然后回写即可。但是,如果用这种方法便忽视了达到字符串末尾的情况。
如果我们所取的键的位置超过了某些字符串长度,如下
取第四个字符为键,但是"sea"总长度为3, sea.charAt(3)会报错,所以要重写一个charAt()函数,以便能处理这种情况(如下代码所示)。很明显,"sea"排序要放在其他的字符串前面,所以在count数组中统计频率时,必须留个位置记录这些“稍短”的字符串。以便于数据分类时能将它们排在其他“长字符串”的前面。
private static int charAt(String s,int d)
{
if(d<s.length()) return s.charAt(d);
return -1;
}
所以我们'a'、'b'、...、'z'等频率统计的位置整体向后挪一位(同时数组长度变为R+2),比如原来'a'的频率统计放在'a'+1的位置上,现在放到'a'+2。这样就腾出位置统计"短"字符串。
0(a) | 1(b) | 2(c) | 3(d) | 4(e) |
0 | 1 | 1 | 0 | 0 |
0(a) | 1(b) | 2(c) | 3(d) | 4(e) |
0 | 0("短"字符串频数) | 1(以'a'为键的字符串数量) | 1(以‘b’...) | 0 |
还有就是如何递归地排序子数组,在键索引计数法结束后,count数组两个相邻位置的数字左边就代表子数组的起始位置,右边的数字-1代表终止位置(懒得画图了,贴个书本的图自行体会)。
public calss MSD
{
private static int R = 256;
private static String[] aux;
private static int charAt(String s, int d)
{
if(d<s.length()) return s.charAt(d);
return -1;
}
public static void sort(String[] a)
{
int N = a.length;
aux = new String[N];
sort(a,0,N-1,0);
}
public static void sort(String[] a, int lo,int hi,int d)
{
if(lo>=hi)
return;
int[] count = new int[R+2];
//统计频率
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[charAt(a[i],d)+1] = a[i];
}
//回写
for(int i = lo;i<=hi;i++)
{
a[i] = aux[i];
}
//递归地排序子数组
for(int r = 0;r<R;r++)
{
sort(a,lo+count[r],lo+count[r+1]-1,d+1);
}
}
}
四、三向字符串快速排序
在高位优先排序的过程中,有多少个作为键的字符,就把完整数组分为多少子数组,如把'a'为键的分为一组,'b'为键的分为一组等等。
三向字符串快速排序,只把要排序的数组分为三组:大于、等于、小于指定键,然后递归地排序子数组。
在三向字符串快速排序算法中,不再需要额外的辅助数组,但是需要一个exch()方法来交换数组内字符串的位置。
public calss Quick3String
{
private void exch(String[] a,int i,int j)
{
String aux = a[i];
a[i] = a[j];
a[j] = a[i];
}
private int chatAt(String s,int d)
{
if(s.length()>d)
return s.charAt(d);
return -1;
}
public static void sort(String[] a)
{
int N = a.length;
sort(a,0,N-1,0);
}
public static void sort(String[] a,int lo,int hi,int d)
{
if(lo>=hi) return;
int lt = lo, i = lo+1, gt = hi;
char b = a[lt].charAt(d); //以第一个字符串的字符为标准键
while(i<=gt)
{
char c = a[i].charAt(d); //找到字符串的键,与标准键进行比较
if(c<b) exch(a,lt++,i++); //当该键小于标准键时
else if(c>b) exch(a,gt--,i); //当该键大于标准键时
else i++; //当该键等于标准键时
}
sort(a,lo,lt-1,d);
if(c>0) sort(a,lt,gt,d+1);
sort(a,gt+1,hi,d);
}
}