第五章 字符串(一) - 《算法》读书笔记
目录
第五章 字符串(一)
5.1 字符串排序
- 低位优先(LSD)的字符串排序:从右到左检查键中的字符
- 适用于键的长度都相同的字符串
- 高位优先(MSD)的字符串排序:从左到右检查键中的字符
- 不一定需要检查所有的输入就能完成排序
5.1.1 键索引计数法
- 将键值key作为索引,来进行排序
5.1.1.1 频率统计
- 使用int数组count[]计算每个键出现的频率,如果键为r,则将count[r+1]加1
- count[0]总是0,如果key从1开始,count[1]也为0
for(int i = 0; i < N; i++)
count[a[i].key() + 1]++;
5.1.1.2 将频率转换为索引
- 使用count[]来计算每个键在排序结果中的起始索引位置
- 任意给定的键的起始索引均为所有较小的键所对应的出现频率之和
for(int r = 0; r < R; r++)
count[r+1] += count[r];
5.1.1.3 数据分类
- 在将count[]数组转换为一张索引表之后,将所有元素移动到一个辅助数组aux[]中进行排序
- 每个元素在aux[]中的位置是由它的键对应的count[]值决定
- 在移动之后将count[]中对应元素的值加1,以保证count[r]总是下一个键为r的元素在aux[]中的索引位置
for(int i = 0; i < N; i++)
aux[count[a[i].key()]++] = a[i];
- 这种实现方式具有稳定性,即键相同的元素排序后会被聚集到一起,但相对顺序没有变化
5.1.1.4 回写
- 最后一步就是将结果复制回数组中
for(int i = 0; i < N; i++)
a[i] = aux[i];
键索引计数法排序N个键为0到R-1之间的整数的元素需要访问数组11N+4R+1次。
- 键索引计数法突破了NlogN的排序算法运行时间下限,因为它不需要比较
- 只要当R在N的一个常数因子范围之内,它都是一个线性时间级别的排序方法
5.1.2 低位优先的字符串排序
- 如果字符串的长度均为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];
}
}
}
低位优先的字符串排序算法能够稳定地将定长字符串排序。
- 如果有两个键,它们中还没有被检查过的字符是完全相同的,那么键的不同之处就仅限于已经被检查过的部分,因为两个键已经被排序过,所以出于稳定性,它们将一直保持有序
- 如果还没有被检查过的部分是不同的,那么已经被检查过的字符对于两者的最终顺序没有意义,之后的某轮处理会根据更高位字符的不同,修正这对键的顺序
对于基于R个字符的字母表的N个以长为W的字符串为键的元素,低位优先的字符串排序需要访问~7WN+3WR次数组,使用的额外空间与N+R成正比。
5.1.3 高位优先的字符串排序
- 首先用键索引计数法将所有字符串按照首字母排序,然后再将每个首字母所对应的子数组排序
5.1.3.1 对字符串末尾的约定
- 将所有字符都已被检查过的字符串所在的子数组,排在所有子数组的前面
- 具体方法:
- 使用charAt()方法来讲字符串中的字符索引转化为数组索引,当指定的位置超过了字符串的末位时,返回-1
- 然后将所有返回值加1,得到一个非负的int值并用它作用count[]的索引
- 此外,增加一个条件语句以在子数组较小时切换至插入排序
public class MSD{
private static int R = 256;
private static final int M = 15; //小数组的切换阈值
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];
sort(a, 0, N-1, 0);
}
private static void sort(String[] a, int lo, int hi, int d){
if(hi <= lo + M){
Insertion.sort(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);
}
}
5.1.3.7 性能
要将基于大小为R的字母表的N个字符串排序,高位优先的字符串排序算法平均需要检查NlogRN个字符;访问数组的次数在8N+3R到~7wN+3wR之间,其中w是字符串的平均长度;最坏情况下所需的空间与R乘以最长的字符串的长度之积成正比。
5.1.4 三向字符串快速排序
- 根据高位优先的字符串排序,改进快速排序
- 根据键的首字母进行三向切分,仅在中间子数组中的下一个字符继续递归排序
public class Quick3string{
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){
sort(a, 0, a.length - 1, 0);
}
private static void sort(String[] a, int lo, int hi, int d){
if(hi <= lo) 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++;
}
//a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi]
sort(a, lo, lt-1, d);
if(v > 0) sort(a, lt, gt, d+1);
sort(a, gt+1, hi, d);
}
}
5.1.4.4 性能
要将含有N个随机字符串的数组排序,三向字符串快速排序平均需要比较字符~2NlnN次。
5.1.5 字符串排序算法的选择
- 将基于大小为R的字母表的N个字符串排序,平均长度为w,最大长度为W
| 算法 | 是否稳定 | 原地排序 | 运行时间 | 额外空间 | 优势领域 |
|---|---|---|---|---|---|
| 字符串的插入排序 | 是 | 是 | N到N2之间 | 1 | 小数组或是已经有序的数组 |
| 快速排序 | 否 | 是 | Nlog2N | logN | 通用排序算法,特别适合用于空间不足的情况 |
| 归并排序 | 是 | 否 | Nlog2N | N | 稳定的通用排序算法 |
| 三向快速排序 | 否 | 是 | N到NlogN之间 | logN | 大量重复键 |
| 低位优先的字符串排序 | 是 | 否 | NW | N | 较短的定长字符串 |
| 高位优先的字符串排序 | 是 | 否 | N到Nw之间 | N+WR | 随机字符串 |
| 三向字符串快速排序 | 否 | 是 | N到Nw之间 | W+logN | 通用排序算法,特别适合用于含有较长公共前缀的字符串 |

浙公网安备 33010602011771号