给定文本串以及模式串,求出文本串中是否含有模式串。
Table of Contents
Boyer-Moore字符串匹配算法
对齐文本串S[0,N-1]以及模式串P[0,M-1],匹配时,从模式串的右边j=M-1向左边开始匹配文本串i+j,如果模式串指针从右边一直移动到左边,说明该位置已经匹配;如果在模式串的某一个位置j,出现P[j]!=S[i+j]
此时分为三种情况:
S[i+j]没有出现在P中,此时S[i+j]不与P[0,j]匹配,因此将i=i+j+1,将文本串移动到该位置即可
如果S[i+j]出现在P中最右边位置为right[S[i+j]]<j,此时可以将S[i+j]与P[right[S[i+j]]<j]对齐继续比较,此时相当于i=i+j-right[S[i+j]
如果S[i+j]出现P中最右边位置right[S[i+j]]>j。此时如果将S[i+j]与P[right[S[i+j]]<j]对齐,相当于i回退,即该种信息是没有意义的,将i=i+1,后移一位继续比较即可。
利用文本串与从右边比较的模式串不匹配字符在模式串中的最右边位置,将文本串指针i启发性的一次移动多个位置,提高效率。
首先对于模式串进行处理,记录每个字符在模式串中的最右边位置,此时使用字母表索引数组来映射每一个字符。
public static int boyerMoore(String txt, String pat){
int[] right = getBMRight(pat);
int N = txt.length();
int M = pat.length();
int skip = 0;
//i文本串从0开始遍历 每次跳跃skip位置
for(int i=0; i<N; i+=skip){
skip = 0;
//模式串从右向左
for(int j=M-1; j>=0; j--){
//从右向左 如果出现不匹配,则计算i应该的位置
//如果文本中不匹配字符在模式串中right[txt.charAt(i+j)]==-1不存在,则skip=j+1
//如果文本中不匹配字符出现在模式串中right[txt.charAt(i+j)] 则skip=j-right[txt.charAt(i+j)]
//如果启发式不起作用即right[txt.charAt(i+j)]>j 此时如果调整相当于模式串左移j++ 回退到以前状态 此时 skip=1即可,判断文本串下一个字符
if(txt.charAt(i+j) != pat.charAt(j)){
skip = j - right[txt.charAt(i+j)];
if(skip<1) skip = 1;
break;
}
}
//如果skip最终等于零,说明此时j==0 即此时已经匹配返回i即可
if(skip == 0) return i;
}
return -1;
}
public static int[] getBMRight(String pat){
int M = pat.length();
int R = 256;//匹配字符串的字母表基数
//right记录模式串中每个字符在模式串中出现的最右边位置 -1表示该字符没有出现
int[] right = new int[R];
for(int i=0; i<R; i++){
right[i] = -1;
}
for(int i=0; i<M; i++){
right[pat.charAt(i)] = i;
}
return right;
}
Rabin_Karp指纹字符串查找算法
利用hash思想,分别求出模式串hash值,文本串不同子串的hash值,比较是否文本串中有一段字符hash值与模式串相等,如果hash值相等,再进行验证,判断两者是否完全匹配;
由于需要计算文本串在不同起始位置连续子串的hash值,可以使用Horner除留余数法来求解hash值,利用文本串i位置以及i+1位置为起始点的子串递推式来常数时间内求出文本串子串的hash值。
Horner除留余数法
//Horner 除数留余法计算散列值
private static long hash(String key, int M, int R, long Q){
long h = 0;
for(int i=0; i<M; i++){
h = (h*R+key.charAt(i)) % Q;
}
return h;
}
给定字符串key,给定待求子串hash值长度M,其中R为字母表基数,Q为散列表的大小,这里并不需要创建Q大小散列表,Q值越大,hash函数值冲突概率越小。
以上方法实际是赋予子串不同字符位置不同的权重,假设文本串位置i处字符的ASCII为ti,每一位权重基数为R,散列表大小为Q
则文本串从i位置开始,连续M个字符组成的子串的hash值为:
文本串从i+1位置开始,组成子串Xi+1为:
得到xi与xi+1递推式:
由除留余数法得:
得到i+1位置处hash值:
最终代码实现如下:
public static int rabinKarp(String txt, String pat){
int M = pat.length();
int N = txt.length();
//模式串的散列值
long patHash = 0;
//hash表的大小 为一个大的long值
long Q = 12347;
//字母表基数
int R = 256;
//R^(M-1)%Q
long RM = 1;
//计算RM值
for(int i=1; i<=M-1; i++){
RM = (R*RM)%Q;
}
//计算模式串散列值
patHash = hash(pat, M, R, Q);
//计算文本串前M个字符散列值
long txtHash = hash(txt, M, R, Q);
//如果文本串以及模式串散列值相等,有极大概率说明两个子串相等,如果Q非常大,两个产生碰撞的概率极小
if(patHash == txtHash && check(0, txt, pat)) return 0;
//从M位置后每移动一位,计算一次新的文本串散列值,比较之
for(int i=M; i<N; i++){
txtHash = (txtHash + Q - RM*txt.charAt(i-M) % Q) % Q;
txtHash = (txtHash * R + txt.charAt(i)) % Q;
if(txtHash == patHash && check(i-M+1, txt, pat)){
return i-M+1;
}
}
return -1;
}
//从文本串i位置开始,检查后面连续字符串是否与模式串pat相等
private static boolean check(int i, String txt, String pat){
//不检查也行 当Q特别大,发生冲突概率很小
//拉斯维加斯
// for (int j = 0; j < pat.length(); j++)
// if (pat.charAt(j) != txt.charAt(i + j))
// return false;
//蒙特卡洛
return true;
}
//Horner 除数留余法计算散列值
private static long hash(String key, int M, int R, long Q){
long h = 0;
for(int i=0; i<M; i++){
h = (h*R+key.charAt(i)) % Q;
}
return h;
}
关于文本串子串与模式串hash值相等,是否需要进行匹配验证问题,由于Q很大,两者冲突概率很小,如果不验证称为蒙特卡洛算法,如果再次进行匹配验证,称为拉斯维加斯算法;
字符串查找算法总结如下:
参考《算法第四版》