目录
0、相关链接
1、定义
子字符串查找:给定一段长度为 N 的文本和一个长度为 M 的模式 (pattern)字符串,在文本中找到一个和该模式相符的子字符串。解决该问题的大部分 算法都可以很容易地扩展为找出文本中所有和该模式相符的子字符串、统计该模式在文本中的出现次 数、或者找出上下文(和该模式相符的子字符串周围的文字)的算法。
2、暴力子字符串查找
子字符串查找的一个最显而易见的方法就是遍历文本N,然后对每个字符都进行检查,确保这个字符和后面的M-1个字符和要找的子字符串是一样的。
在最坏情况下,暴力子字符串查找算法在长度为 N 的文本中查找长度为 M 的模式需 要 ~NM 次字符比较。这是因为第一层for循环需要(N-M+1)第二层for循环需要M次,总共需要「 M*(N-M+1)」,因为M一般都远小于N,所以省去M^2+M。
public class SubstringSearch {
/**
* 实现一,正循环
*/
public static int search(String pat, String txt) {
int N = txt.length();
int M = pat.length();
for (int i = 0; i <= N - M; i++) {
int j;
for (j = 0; j < M; j++) {
if (pat.charAt(j) != txt.charAt(i + j))
break;
}
if (j == M)
return i; // 找到匹配
}
return N; // 未找到匹配
}
/**
* 实现二,显示回退
* 这段代码中的 i 值相当于上一段代码中的 i+j:它指向的是文本中已经匹配过的字 符序列的末端(i 以前指向的是这个序列的开头)。
* 如果 i 和 j 指向的字符不匹配了,那么需要回 退这两个指针的值:将 j 重新指向模式的开头,将 i 指向本次匹配的开始位置的下一个字符。
*/
public static int search1(String pat, String txt) {
int N = txt.length();
int M = pat.length();
int i,j;
for (i = 0, j = 0; i < N && j < M; i++) {
if (pat.charAt(j) == txt.charAt(i)) {
j++;
} else {
i = i - j;
j = 0;
}
}
if (j==M) return i-M;// 找到匹配
else return N; // 未找到匹配
}
public static void main(String[] args) {
String pat = "abab";
String txt = "sjosajofajfiajsessasabacababfofkokokofj";
int result = search(pat, txt);
int result1 = search1(pat, txt);
System.out.println("result=" + result+",result1="+result1);
System.out.println("txt.length=" + txt.length());
}
}
3、Knuth-Morris-Pratt 子字符串查找
上面的暴力算法在最坏情况下需要N*M的时间复杂度,这不是我们想要的。这里将讲解一个最坏情况下也只需要N次循环的查找算法。
KMP算法时借鉴了上面暴力算法的第二种实现方式,它会遍历文本N,同时与pat匹配,当不匹配的时候回退子针。KMP与其不同的地方是:KMP不会回退子针,而是当你匹配0-M时,如果在中间某个字符不匹配时,通过预处理信息,直接知道我下一步应该怎么重新开始0-M的遍历。该算法理解起来还是比较麻烦的,所以可以先看一下代码。
通俗点说就是当 我们的pat=“aab”, 文本txt=“abcaaaabc”。
当我们遍历文本N的第一字符a时,j=1;
遍历第二个字符b时,j变成了0;
遍历第4和第5个字符时,j=2,但遍历到第三个a时,j不会变成0,j还是=2;
遍历第四个a的时候,j=2;
最后遍历到b,j=3,结果出来了。
对于长度为 M 的模式字符串和长度为 N 的文本,Knuth-Morris-Pratt 字符串查找算法访 问的字符不会超过 M+N 个。
KMP 算法为最坏情况提供的线性级别 运行时间保证是一个重要的理论成果。在 实际应用中,它比暴力算法的速度优势并 不十分明显,因为极少有应用程序需要在 重复性很高的文本中查找重复性很高的模 式。
public class KMP {
private String pat; // 需要查找的字符串
private int[][] dfa; // 预处理的二维数组
private int R = 256; // 字母表的大小
public KMP(String pat) {
this.pat = pat;
int M = pat.length();
dfa = new int[R][M];
dfa[pat.charAt(0)][0] = 1; //0、预处理pat的第一个字符
for (int X = 0, j = 1; j < M; j++) {
for (int c = 0; c < R; c++) {
dfa[c][j] = dfa[c][X]; //1、复制匹配失败情况下的值
}
dfa[pat.charAt(j)][j] = j + 1; //2、设置匹配情况下的值
X = dfa[pat.charAt(j)][X]; //3、更新重启状态,这里主要是给 pat="aaabc"
// 前面的三个a或者更多的a更新重启状态,b、c字母会重启为0
}
}
public int search(String txt) {
int i, j;
int N = txt.length(), M = pat.length();
for (i = 0, j = 0; i < N && j < M; i++) {
j = dfa[txt.charAt(i)][j];
}
if (j == M)
return i - M;
else
return N;
}
public static void main(String[] args) {
String pat = "abab";
String txt = "sjosajofajfiajsessaababababsfofkokokofj";
KMP kmp = new KMP(pat);
int offset = kmp.search(txt);
System.out.println("offset=" + offset);
System.out.println("txt.length=" + txt.length());
}
}
1、该算法会创建一个二维数组dfa来预处理要查找的模式字符串pat。
2、dfa中保存着256个大小为pat长度M的数组。 dfa = new int[R][M]; 也就是每个字符都有一个长度和pat长度一样的数组来保存与该字符匹配成功后j值的变化,和匹配失败后j值的变化。
3、pat=“aaa” dfa[a][0]=1; dfa[a][1]=2; dfa[a][2]=3; 其余的数据都是0;
pat=“aab” dfa[a][0]=1; dfa[a][1]=2; dfa[a][2]=2; 其余的数据都是0;
dfa[b][0]=0; dfa[b][1]=0; dfa[b][2]=3;
pat=“abc” dfa[a][0]=1; dfa[a][1]=1; dfa[a][2]=1; 其余的数据都是0;
dfa[b][0]=0; dfa[b][1]=2; dfa[b][2]=0;
dfa[b][0]=0; dfa[b][1]=0; dfa[b][2]=3;
上面的数据大致包括了字符串的几种形式‘abc'、’aab'、‘aaa'。
先从’aaa'开始,从上面的数据我们可以看到当我们遇到一个a的时候j=1;当我们遇到第二个a的时候j=2;但第三个字符不是a,那么j就会变回0,但是并没有回退。而是继续向后遍历。
‘aab'形式,当我们遇到一个a的时候j=1;第二个a的时候j=2,但第三个还是a的话,说明第三个字符不匹配,但是我们通过j我们知道前面还有两个a,所以我们不需要回退子针,也不需要将j变回0,而是继续从j=2开始匹配下一个字符,如果下一个字符还是a,那么j还是=2;如果不是a也不是b那么变回0.
‘abc'形式,所有子字符都不相同的情况就好处理了,除了每次碰见a,j变为1以外,其他位置的字符都只有前一个字符匹配的时候才会j才会增加,否则都会变成0。
4、Boyer-Moore 字符串查找算法
Boyer-Moore 算法选择从右到左扫描模式pat字符串并与txt文本比较。它借用了KMP算法的思想也通过预处理right[]数组来对所有字符进行位置的记录,然后当发生不匹配的时候,提供跳转的数量。
right[]数组会保存模式字符串 pat= “abc”中三个字符的索引,right[a]=0,right[b]=1,right[c]=2,然后其余的253个字符全是 -1.
模式字符串pat= “a b c” ,文本txt=“a b d e c b c a b c” 。
1、我们先从 txt的第三个字符“d”和 “abc”比较,发现“d”和“c”不匹配,又因为“d”不存在pat字符串中,说明txt的前三个字符肯定不会出现和pat完全匹配的子字符串,所以我们直接跳转2-(-1)=3个位置。(其中2是当前扫描的pat中字符的索引,c的索引j=2)
2、然后用用第六个字符“b”和“abc”比较,又发现“b”和“c”不匹配,但是“b”存在于“abc”中,我们预处理right[]数组会保存“b”存在于“abc”的位置为1,这时候我们需要将 “abc”向后跳转 2-1=1个位置。
3、然后我们用第七个字符“c”和“abc”比较,发现匹配,并且前面的“b”也和“abc”匹配,但是再前面“c”和“a”不匹配,这时候扫描的字符的索引 j[a]=0, 0-2=-2。我们“abc”肯定不能跳转-2个位置,当我们遇到跳转小于的1的位置的时候,我们强制让他跳转1个位置,防止它原地打转。
上面的三种情况包含了我们在跳转过程中遇到的所有情况,只要我们把这三种情况处理好,那这个算法就成功了,并且还能将时间复杂度降低到(~N/M)。
在一般情况下,对于长度 为 N 的文本和长度为 M 的模式字 符串,使用了 Boyer-Moore 的子字 符串查找算法通过启发式处理不匹 配的字符需要 ~N/M 次字符比较。
public class BoyerMoore {
private String pat;
private int[] right;
/**
* 计算跳跃表 right
*/
public BoyerMoore(String pat) {
this.pat = pat;
int R = 256;
int M = pat.length();
right = new int[R];
for (int c = 0; c < R; c++) {
right[c] = -1; // 不包含在模式字符串中字符的值为-1
}
for (int j = 0; j < M; j++) {
right[pat.charAt(j)] = j; // 包含在模式字符串中的字符的值为它出现在模式字符串中最右的位置,
// 就比如 pat="aaa" right[a]=2;不是0和1
}
}
public int search(String txt) {
int N = txt.length();
int M = pat.length();
int skip;
for (int i = 0; i <= N - M; i += skip) {
skip=0;
for (int j = M - 1; j >= 0; j--) { //1、每次从pat的最右边的字符开始与txt比较
if (pat.charAt(j) != txt.charAt(j + i)) { //2、如果第 j+i个字符和pat不匹配,进行跳转
skip = j - right[txt.charAt(j + i)]; //3、跳转的距离等于
if (skip < 1) skip = 1;
break;
}
}
if (skip == 0) return i;
}
return N;
}
}
5、Rabin-Karp 指纹字符串查找算法
M.O.Rabin 和 R.A.Karp 发明了一种完全不同的基于散列的字符串查找算法。
我们需要计算模式 字符串的散列函数,然后用相同的散列函数计算文本中所有可能的 M 个字符的子字符串散列值并 寻找匹配。如果找到了一个散列值和模式字符串相同的子字符串,那么再继续验证两者是否匹配。 这个过程等价于将模式保存在一张散列表中,然后在文本的所有子字符串中进行查找。但不需要为 散列表预留任何空间,因为它只会含有一个元素。
根据这段描述直接实现的算法将会比暴力子字符 串查找算法慢很多(因为计算散列值将会涉及字符串中的每个字符,成本比直接比较这些字符要高 得多)。Rabin 和 Karp 发明了一种能够在常数时间内算出 M 个字符的子字符串散列值的方法(需 要预处理),这样就得到了在实际应用中的运行时间为线性级别的字符串查找算法。
public class RabinKarp {
private String pat; // the pattern // needed only for Las Vegas
private long patHash; // pattern hash value
private int m; // pattern length
private long q; // a large prime, small enough to avoid long overflow
private int R; // radix
private long RM; // R^(M-1) % Q
/**
* Preprocesses the pattern string.
*
* @param pattern the pattern string
* @param R the alphabet size
*/
public RabinKarp(char[] pattern, int R) {
this.pat = String.valueOf(pattern);
this.R = R;
throw new UnsupportedOperationException("Operation not supported yet");
}
/**
* Preprocesses the pattern string.
*
* @param pat the pattern string
*/
public RabinKarp(String pat) {
this.pat = pat; // save pattern (needed only for Las Vegas)
R = 256;
m = pat.length();
q = longRandomPrime();
// precompute R^(m-1) % q for use in removing leading digit
RM = 1;
for (int i = 1; i <= m-1; i++)
RM = (R * RM) % q;
patHash = hash(pat, m);
}
// Compute hash for key[0..m-1].
private long hash(String key, int m) {
long h = 0;
for (int j = 0; j < m; j++)
h = (R * h + key.charAt(j)) % q;
return h;
}
// Las Vegas version: does pat[] match txt[i..i-m+1] ?
private boolean check(String txt, int i) {
for (int j = 0; j < m; j++)
if (pat.charAt(j) != txt.charAt(i + j))
return false;
return true;
}
// Monte Carlo version: always return true
// private boolean check(int i) {
// return true;
//}
/**
* Returns the index of the first occurrrence of the pattern string
* in the text string.
*
* @param txt the text string
* @return the index of the first occurrence of the pattern string
* in the text string; n if no such match
*/
public int search(String txt) {
int n = txt.length();
if (n < m) return n;
long txtHash = hash(txt, m);
// check for match at offset 0
if ((patHash == txtHash) && check(txt, 0))
return 0;
// check for hash match; if hash match, check for exact match
for (int i = m; i < n; i++) {
// Remove leading digit, add trailing digit, check for match.
txtHash = (txtHash + q - RM*txt.charAt(i-m) % q) % q;
txtHash = (txtHash*R + txt.charAt(i)) % q;
// match
int offset = i - m + 1;
if ((patHash == txtHash) && check(txt, offset))
return offset;
}
// no match
return n;
}
// a random 31-bit prime
private static long longRandomPrime() {
BigInteger prime = BigInteger.probablePrime(31, new Random());
return prime.longValue();
}
/**
* Takes a pattern string and an input string as command-line arguments;
* searches for the pattern string in the text string; and prints
* the first occurrence of the pattern string in the text string.
*
* @param args the command-line arguments
*/
public static void main(String[] args) {
String pat = args[0];
String txt = args[1];
RabinKarp searcher = new RabinKarp(pat);
int offset = searcher.search(txt);
// print results
StdOut.println("text: " + txt);
// from brute force search method 1
StdOut.print("pattern: ");
for (int i = 0; i < offset; i++)
StdOut.print(" ");
StdOut.println(pat);
}
}
6、各种子字符串查找算法的成本比较