1. 字符串排序
对于许多排序应用来说,决定顺序的键都是字符串。给定一列字符串,需要按一定顺序排列整齐方便后序处理。
2. 键索引计数法
这个方法名字有点拗口,过程有点绕,但是每一步其实很简单。
举个简单的例子:
过程看着有点复杂,但是代码真的非常简单,一步一行就可以。
public static void string_count(String[] a, int R)
{
int N = a.length;
int[] Count = new int[R+1];
String[] aux = new String[N];
for(int i=0;i<N;i++)
Count[a[i].key()+1]++; //计数
for(int i=0;i<R;i++)
Count[i+1] += Count[i]; //Count数组向前累加
for(int i=0;i<N;i++)
aux[Count[a[i].key()]++] = a[i]; //将元素分类
for(int i=0;i<N;i++)
a[i] = aux[i]; //回写
}
这里a[i].key()表示字符串a[i]的键,我们按照键对数组a进行排序,也就是说键小的在前,键大的在后。
注意这里一定要理解清楚,键索引计数法是后面两种算法的基础。
而且键索引计数法是稳定的,也就是说,同一个键的话,字符串的相对顺序不会改变。
3. 低位优先的字符串排序
低位优先的字符串排序方法适合数组中所有字符串的长度相等的情况。
例如 String a[]= {"ao01", "adf5", "erh4","6te5"},字符串数组a中所有的字符串都是4位。
这种情况就可以用低位优先的字符串排序,具体来说就是从最末尾一位开始排序,排完了之后,再按照倒数第二位开始排序,直到第一个字符。一共要经历四轮排序,最后得到最终顺序。
我一开始看的时候也是好奇为什么要从最后一位开始比,然后再依次比到第一位。包括自己也拿程序验证了一下,如果是从第一位先排,再排到最后一位结果确实是不对的。
但是仔细想了一下,这个过程其实就类似于数字的比大小。以四位数为例,先从千位开始比,从小到大排好,再按百位排,就相当于打乱了这一顺序,得出来的排序没有任何意义,因为千位的排序本身是要比百位更重要的。相反,假如从个位开始排,那么个位数大的排在后面,然后再排十位,打乱的话,这个过程是有意义的。因为十位本身就决定了排序,而假如十位相同的话,那么第一轮决定了个位数大的排在后面。这个时候就真正实现了数字的排序。同理也可以应用于字符串的排序。
这个算法的核心在于排序次数线性正比于字符串长度。假如字符串长度均为n,那么就只需要操作n次。并且原地排序不需要占用任何其他内存。这样看这个算法其实是简洁且快速的。
public class LSD
{
public static void sort(String[] a, int W) //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]; //注意这一行是在循环里面,因为每一轮循环都有自己独立的count数组
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]; //Count向前累加
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]; // 回写
}
}
public static void main(String[] args)
{
String[] a = {"4ADC","4BPL","0HPD","0BME"};
int W = 4;
sort(a, 4);
for(int i=0;i<4;i++)
{
System.out.println(a[i]);
}
}
}
输出结果为
4. 高位优先字符串
高位优先字符串则更加通用一些,可以处理一个字符串数组中,字符长度不定的情况。
具体方法为,先用键索引计数法将所有字符串按照首字母排序,然后递归的将每个首字母所对应的子数组排序。如果遇到["A", "AB"],具有相同前缀,但有些字符串已经结束,另一些没结束,则把已经结束了的放在子字符串数组的前面。
public class MSD {
private static int R = 256;
private static String[] aux;
private static int charAt(String s, int d)
{
if(d>=s.length()) return -1;
else return s.charAt(d);
}
public static void sort(String[] a)
{
int N = a.length;
aux = new String[N];
sort(a, 0, N-1, 0);
}
//a为带排序数组,lo开始考虑的位置,hi考虑的最后一个字符串的位置,考虑的第d位字符
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[Count[charAt(a[i], d)+1]++] = a[i];
for(int i=lo;i<=hi;i++) //回写
a[i] = aux[i-lo];
for(int r=0;r<R;r++) //递归以每个字符为键进行排序
sort(a, lo + Count[r], lo + Count[r+1]-1, d+1);
}
public static void main(String[] args)
{
String[] a = {"4ADC","4BPL","0HPD","0BME"};
sort(a);
for(int i=0;i<4;i++)
{
System.out.println(a[i]);
}
}
}
用同样的测试数据,得到了和低位优先排序一样的结果。
5. 三向字符串快速排序
三向指的是每次排序都只把数组切分位三部分。这样可以提高每次排序的效率,避免每次都要生成一堆空子数组。
三向字符串快速排序特别适合有特别长的公共前缀的字符串
public class Quick3string {
private static int charAt(String s, int d) {
if (d >= s.length()) return -1;
else return s.charAt(d);
}
public static void sort(String[] a) {
sort(a, 0, a.length - 1, 0);
}
public static void sort(String[] a, int lo, int hi, int d) {
if (lo >= hi) return;
int lt = lo, gt = hi;
int v = charAt(a[lo], d);
int i = lo + 1;
while (i <= gt) {
int t = charAt(a[i], d);
if (t < v) exch(a, lt++, i++);
else if (t > v) exch(a, i, gt--);
else i++;
}
sort(a, lo, lt - 1, d); //先把小于首字母的部分按首字母再继续递归排序
if (v >= 0) sort(a, lt, gt, d + 1); //把等于首字母的,且还没有遍历完成的就排后一个字母
sort(a, gt + 1, hi, d); //大于首字母的也是继续按首字母再继续递归排序
}
}