字符串
字符串处理算法在多个领域展现出其多样性与重要性。
– 信息处理:诸如根据给定关键字搜索网页等。
– 基因组学:根据密码子将DNA碱基转换为字符串并进行生物学研究
– 通信系统:短信,电子邮件的发送或者电子书下载,本质都是字符串的传送
– 编程系统:程序由字符串组成,编译器,解释器等都是通过强大复杂的字符串算法将它们转换为机器语言的
字母表
一些应用程序可能会对字符串的字母表(包含所有字符串的字符集)做出限制,这时我们就可能需要更换字母表或自定义字母表
自定义的字母表在功能上可能不能达到尽善尽美,但以其短小且适应问题而具有研究价值,如果一个问题只需要四个字符组成的字母表,这时候再采用系统内置的标准字母表就显得得不偿失
字母表API
返回值 | 函数名 | 作用 |
构造方法 | Alphabet(String s) | 根据s中的字符创建一张新的字母表 |
char | toChar(int index) | 获取字母表中索引位置的字符 |
int | toIndex(char c) | 获取c的索引,在0-(R-1)之间 |
boolean | contains(char c) | c是否在字母表中 |
int | R() | 基数(字母表中的字符数量) |
int | lgR() | 表示一个索引所需的比特数 |
int[] | toIndices(String s) | 将s转换为R进制整数 |
String | toChars(int[] indices) | 将R进制整数转换为基于该字母表的字符串 |
代码实现:
package cn.ywrby.MyString;
import edu.princeton.cs.algs4.StdOut;
public class Alphabet {
/*
* 定义标准字母表
* */
//The binary alphabet { 0, 1 }. 01字符集
public static final Alphabet BINARY = new Alphabet("01");
//The octal alphabet { 0, 1, 2, 3, 4, 5, 6, 7 }. 八进制字符集
public static final Alphabet OCTAL = new Alphabet("01234567");
//The decimal alphabet { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }. 小数集
public static final Alphabet DECIMAL = new Alphabet("0123456789");
//The hexadecimal alphabet { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F }. 十六进制字符集
public static final Alphabet HEXADECIMAL = new Alphabet("0123456789ABCDEF");
//The DNA alphabet { A, C, T, G }. DNA字符集
public static final Alphabet DNA = new Alphabet("ACGT");
//The lowercase alphabet { a, b, c, ..., z }. 小写字母字符集
public static final Alphabet LOWERCASE = new Alphabet("abcdefghijklmnopqrstuvwxyz");
//The uppercase alphabet { A, B, C, ..., Z }. 大写字母字符集
public static final Alphabet UPPERCASE = new Alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
//The protein alphabet { A, C, D, E, F, G, H, I, K, L, M, N, P, Q, R, S, T, V, W, Y }. 蛋白质字符集
public static final Alphabet PROTEIN = new Alphabet("ACDEFGHIKLMNPQRSTVWY");
//The base-64 alphabet (64 characters). base-64字符集
public static final Alphabet BASE64 = new Alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
//The ASCII alphabet (0-127). ASCII码字符集
public static final Alphabet ASCII = new Alphabet(128);
//The extended ASCII alphabet (0-255). 扩展ASCII码字符集
public static final Alphabet EXTENDED_ASCII = new Alphabet(256);
//The Unicode 16 alphabet (0-65,535). Unicode16字符集
public static final Alphabet UNICODE16 = new Alphabet(65536);
private char[] alphabet; //用字符数组存储字母表
private int[] inverse; //存储字符的索引值的整数数组(利用字符获得字符的索引值)
private int R; //字母表大小
//构造函数(利用字符串)
public Alphabet(String alpha){
boolean[] unicode=new boolean[Character.MAX_VALUE];
for(int i=0;i<alpha.length();i++){
char c=alpha.charAt(i);
//避免重复字符
if(unicode[c]){
throw new IllegalArgumentException("Illegal alphabet: repeated character = '" + c + "'");
}
unicode[c]=true;
}
alphabet=alpha.toCharArray(); //将字符串转换为字符型数组并用作字母表
R=alpha.length();
inverse=new int[Character.MAX_VALUE];
//将索引值各项初始化为-1,然后再单独修改使用到的字符
//这样返回值为-1就表示该字符不在字母表中
for(int i=0;i<inverse.length;i++){
inverse[i]=-1;
}
for(int i=0;i<R;i++){
inverse[alphabet[i]]=i;
}
}
//构造函数(利用整数)
public Alphabet(int radix){
this.R=radix;
alphabet=new char[R];
inverse=new int[R];
for(int i=0;i<R;i++){
alphabet[i]=(char)i;
}
for(int i=0;i<R;i++){
inverse[i]=i;
}
}
//默认构造
public Alphabet(){this(256);}
//字符c是否存在字母表中
public boolean contains(char c){ return inverse[c]!=-1; }
//字母表大小
public int R(){return R;}
//一个索引所需的比特数
public int lgR(){
int lgR=0;
for(int t=R-1;t>=1;t/=2){
lgR++;
}
return lgR;
}
//获取字符的索引
public int toIndex(char c){
if(c>=inverse.length||inverse[c]==-1){
throw new IllegalArgumentException("Character " + c + " not in alphabet");
}
return inverse[c];
}
//将s转换为R进制整数
public int[] toIndices(String s){
char[] source=s.toCharArray();
int[] target=new int[s.length()];
for(int i=0;i<s.length();i++){
target[i]=toIndex(source[i]);
}
return target;
}
//将R进制整数转换为字符
public char toChar(int index){
if(index>=R||index<0){
throw new IllegalArgumentException("index must be between 0 and " + R + ": " + index);
}
return alphabet[index];
}
//将R进制整数数组转换为字符串
public String toChars(int[] indices){
StringBuilder s=new StringBuilder(indices.length);
for(int i=0;i<indices.length;i++){
s.append(toChar(indices[i]));
}
return s.toString();
}
public static void main(String[] args) {
int[] encoded1 = Alphabet.BASE64.toIndices("NowIsTheTimeForAllGoodMen");
String decoded1 = Alphabet.BASE64.toChars(encoded1);
StdOut.println(decoded1);
int[] encoded2 = Alphabet.DNA.toIndices("AACGAACGGTTTACCCCG");
String decoded2 = Alphabet.DNA.toChars(encoded2);
StdOut.println(decoded2);
int[] encoded3 = Alphabet.DECIMAL.toIndices("01234567890123456789");
String decoded3 = Alphabet.DECIMAL.toChars(encoded3);
StdOut.println(decoded3);
}
}
字符串排序
键索引计数法
一种适用于小整数键的简单排序算法,这个算法依托于每个字符串对应于一个键,这个键是一个编号较小的整数。
整个算法在排序前是有序的,算法实现的是将各个字符串按键的顺序重新排列(不改变相同键形成的小组内部字符串仍然是相对有序的)
具体应用场景类似于学生统计问题,学生被分成若干小组(组号就是每个人的键值),每组人数不定,现在老师手里有按照姓名排序(字符串有序)的表格,现在要将表格按照小组顺序重新排列,并且排列后小组内部姓名仍然是有序的(不改变相同键形成小组内部字符串相对有序)
算法执行过程:
- 通过一次遍历,统计各个键的频率(利用int[] count保存,如果访问键r则将count[r+1]加1(这样可以方便后续计算,并轻松的将频率转换为各组的起始索引))。
- 将频率转换为索引,上一步统计了各个小组的人数,所以只需逐项增加前一项大小就可以得到该组的起始索引位置(例如:第1组是第1项,由于0项=0,所以0+0=0,起始索引为0,第2组是第2项,0+count[2]=count[2]就是第二组的索引起始位置,以此类推得到各项的起始索引)
- 数据分类,创建辅助数组,然后遍历原数组,按照数组键值以及count数组提供的索引位置写入新数组(由于之前的字符串是有序的,所以遍历并将数据写入新数组的过程中没有打乱小组内部的有序性),每次写入后都要对count数组对应项+1,保证指向小组内下一个索引位置
- 回写数据,将排序后的数据再次遍历写入原数组中
简单代码实现
package cn.ywrby.MyString.KeyIndexCounting;
public class Student {
private String name; //姓名
private int group; //所在小组
public Student(String name,int group){
this.name=name;
this.group=group;
}
public int group(){return group;}
}
package cn.ywrby.MyString.KeyIndexCounting;
import edu.princeton.cs.algs4.In;
//键索引计数法
public class KeyIndexCount {
public static void main(String[] args) {
Student[] stu=new Student[60];
int N=stu.length;
int[] count=new int[N];
int R=256;
Student[] aux=new Student[N];
/*
* 通过一次遍历,统计各个键的频率(利用int[] count保存,
* 如果访问键r则将count[r+1]加1(这样可以方便后续计算,
* 并轻松的将频率转换为各组的起始索引))。
* */
for(int i=0;i<N;i++){
count[stu[i].group()+1]+=1;
}
/*
* 将频率转换为索引,上一步统计了各个小组的人数,
* 所以只需逐项增加前一项大小就可以得到该组的起始索引位置
* (例如:第1组是第1项,由于0项=0,所以0+0=0,起始索引为0,
* 第2组是第2项,0+count[2]=count[2]就是第二组的索引起始位置,以此类推得到各项的起始索引)
* */
for(int r=0;r<R;r++){
count[r+1]+=count[r];
}
/*
* 数据分类,创建辅助数组,然后遍历原数组,
* 按照数组键值以及count数组提供的索引位置写入新数组
* (由于之前的字符串是有序的,所以遍历并将数据写入新数组的过程中没有打乱小组内部的有序性),
* 每次写入后都要对count数组对应项+1,保证指向小组内下一个索引位置
* */
for(int i=0;i<N;i++){
aux[count[stu[i].group()+1]++]=stu[i];
}
//回写数据,将排序后的数据再次遍历写入原数组中
for(int i=0;i<N;i++){
stu[i]=aux[i];
}
}
}
键索引计数法排序N个键为0-(R-1)之间的元素需要访问整个数组11N+4R+1次(突破了排序算法NlgN时间的下限,因为并不是通过比较进行排序的,而是利用键值进行排序)
低位优先的字符串排序(LSD)
低位优先的字符串排序适用于那些长度相同的字符串组进行排序
排序的过程
- 对于一个每项都是N长度的字符串数组,字符串从低位开始,把每一位当作是这个字符串的键,执行键索引计数法排序。
- 在执行第一次后,所有字符串的最后一位有序,也就是字符串中任意两个字符串的最后一位都是有序的。
- 由于键索引计数法的稳定性,所以在计算倒数第二位的键索引计数法排序时,首先能保证倒数第二位有序,其次能保证倒数第二位相同的字符串,倒数第一位也有序(有序性没有被破坏)。
- 以此类推,直到执行到第一位,就能够保证第一位有序,并且第一位相同的第二位有序,第二位相同的第三位有序……可以得出字符串数组整体有序。
算法实现:
package cn.ywrby.MyString;
//低位优先的字符串排序
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]+=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)+1]++]=a[i];
}
for(int i=0;i<N;i++){
a[i]=aux[i];
}
}
}
}
算法分析:
基于R个字符的字母表的N个长为W的字符串为键的元素,低位优先的字符串排序需要访问~7WN+3WR次数组,占用空间于N+R成正比
该算法等价于调用W轮键索引计数法,但是aux数组只初始化一回,所以得到如上的结果
高位优先的字符串排序算法
上文谈及的低位优先排序算法只适用于所有字符串长度一致的情况下,这明显是有局限的,对于更一般的字符串数组,高位优先的字符串排序算法更有优势
高位优先的算法对传入字符串长度没有了限制,因此增加了charAt方法用于判别字符串是否已经到达尾部,同时为了算法的高效性加入了切换阈值这一常量,当达到切换阈值时,算法就会自动更换策略为更适用于小型排序的插入排序
package cn.ywrby.MyString;
import edu.princeton.cs.algs4.Insertion;
//高位优先的字符串排序
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[] s){
int N=s.length;
aux=new String[N];
sort(s,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);
}
}
}