《算法》笔记 13 - 字符串排序
- 键索引计数法
- 频率统计
- 将频率转换为索引
- 数据分类
- 回写
- 低位优先的字符串排序
- 高位优先的字符串排序
许多重要而熟悉的问题都是基于字符串处理的,比如信息处理(根据给定的关键字搜索网页、文档)、通信系统(发送文本消息、电子邮件、下载电子书)、编程系统(程序是由字符串组成的,再由编译器或解释器将字符串转换为机器指令)、基因组学(生物学家根据密码子将DNA转换为由A、C、T、G四个字符组成的字符串,字符串处理已经成为了计算生物学研究的基石)。
对于许多排序应用,决定顺序的键都是字符串,利用字符串的特殊性质开发的字符串键排序方法将比之前学过的通用排序方法效率更高。
键索引计数法
键索引计数法是一种适用于小整数键的简单排序方法,同时也是后续两种字符串排序方法的基础。
平时会接触到很多小整数的场合,比如老师在统计学生分数时,希望将全班同学按组分类,这而组的编号一般都是较小的整数。这时键索引计数法。
以学生分组排序为例键索引计数法分为4步,数组如下
2 Anderson
3 Brown
3 Davis
4 Garcia
1 Harris
3 Jackson
4 Johnson
3 Jones
1 Martin
2 Martinez
2 Miller
1 Moore
2 Robinson
4 Smith
4 Taylor
4 Thomas
2 Thompson
3 White
4 Williams
4 Wilson
频率统计
第一步是使用int数组count计算每个键出现的频率。对于数组中的每个元素,都使用它的键访问count中的相应元素,并将其加1。为了后面处理的方便,对于键r,增加的是数组中r+1的位置。
由于Anderson在第二组,便将count[3]加1,Brown在第3组,将count[4]加1...,count[0]的位置不会使用,值总是0,在这个例子中count[1]的值也始终为0,因为没有第0组。
将频率转换为索引
然后使用上一步count的结果来计算每个键在排序结果中的起始索引位置。
第1组有3个人,第2组有5个人,所以第2组的数据在排序结果中的起始索引位置为3,第3组的数据在排序结果中的起始索引位置就是8...
经过如下的运算,就可将频率转换为索引,之后根据count[r]就可拿到r组的起始索引。这也是上一步增加r+1位置的原因。
for(int r=0;r<R;r++){
count[r+1]+=count[r];
}
数据分类
将count数组转换为一张索引表之后,再将所有元素转移到一个辅助数组aux,每个元素在aux中的位置是由它的键对应的count值决定的,每转移一个元素,就将这个元素在count中对应位置的值加1,在将元素全部转移到aux数组后,排序结果就已经产生了。
回写
最后将aux中排好序的结果复制回原始数组。
使用这种排序方式,键的相对顺序没有变化,是一种稳定的排序方法。
低位优先的字符串排序
低位优先的字符串排序是基于键索引计数法的一种排序方法,适用于对长度固定的字符串进行排序,比如电话号码、银行帐号、IP地址、车牌号码等。
如果字符串的长度都为W,这种方法会从右向左以每个位置的字符作为键,用键索引计数法将字符串排序W遍。由于键索引计数法是稳定的,经过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 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];
}
}
}
高位优先的字符串排序
低位优先的字符串排序方法只适用于字符串长度统一的场合,但通用的字符串排序算法要能够处理字符串长度不相同的情况。高位优先的字符串排序就是这样一种方法,因为字符串长度不一定相同,就采用从左向右遍历字符的方式,对于所有字符都已经检查过的字符串(较短的字符串),规定它们排在更长的字符串的前面。
这里专门封装了一个charAt方法,当指定的位置超过了字符串的末尾时,这个方法会返回-1。然后将所有返回值加1,得到一系列非负整数,用它们作为count数组的索引。因为键索引计数法本来就需要一个额外的位置,所以这里初始化count数组时要将数组大小指定为R+2.
public class MSD {
private static int R = 256;
private static final int M = 0;
private static String[] aux;
private static int charAt(String s, int d) {
if (d < s.length())
return s.charAt(d);
else
return -1;
}
public static void sort(String[] a) {
int N = a.length;
aux = new String[N];
StdOut.println(String.format("sort(a, %s, %s, %s)", 0, N - 1, 0));
sort(a, 0, N - 1, 0);
}
public static int index;
public static void sort(String[] a, int lo, int hi, int d) {
index++;
if (hi <= lo + M) {
insertion(a, lo, hi, d);
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);
}
}
// insertion sort a[lo..hi], starting at dth character
private static void insertion(String[] a, int lo, int hi, int d) {
for (int i = lo; i <= hi; i++)
for (int j = i; j > lo && less(a[j], a[j - 1], d); j--)
exch(a, j, j - 1);
}
// exchange a[i] and a[j]
private static void exch(String[] a, int i, int j) {
String temp = a[i];
a[i] = a[j];
a[j] = temp;
}
// is v less than w, starting at character d
private static boolean less(String v, String w, int d) {
// assert v.substring(0, d).equals(w.substring(0, d));
for (int i = d; i < Math.min(v.length(), w.length()); i++) {
if (v.charAt(i) < w.charAt(i))
return true;
if (v.charAt(i) > w.charAt(i))
return false;
}
return v.length() < w.length();
}
}
和快速排序一样,高位优先的字符串排序会将数组切分为能够独立排序的子数组来完成排序任务,但它的切分会为每个首字母得到一个子数组,而不是像快速排序那样产生固定的两个或三个切分。
另外为了避免小数组产生过多的递归,会在子数组较小时切换到插入排序。