最近学习了一下字符串匹配的一些算法,在这里做一个总结。最简单的暴力匹配就不说了。
我们检查在目标串中是否存在模式串。
1.KMP算法
class KMP{
private int next[];
private String pattern;
private int size;
public KMP(String p)
{
pattern = p;
size = p.length();
next = new int[size+1];
getNext();
}
private void getNext()//求next数组
{
int i = 0;
int j = -1;
next[0] = -1;
while(i < size)
{
while(j >= 0 && pattern.charAt(i)!= pattern.charAt(j) )j=next[j];
++i;
++j;
next[i] = j;
}
}
public int KMPMatch(String t)
{
int lenp = size;
int lent = t.length();
int i = 0, j = 0;
while(i < lent && j < lenp)
{
if(j == -1 || t.charAt(i) == pattern.charAt(j))
{
i++;j++;
}
else j = next[j];
}
if(j == lenp)
return i - j; //匹配成功
else return -1; //匹配失败
}
}
KMP在最坏情况下也有着O(n+m)的复杂度,也就是在线性时间内能够完成匹配,但是有人说该算法和strstr函数的速度差不多。
本人水平有限,就不具体说算法的原理了,有一篇写的还不错的学习KMP的文章,推荐一下,KMP算法。
2.BM算法
BM算法相对于KMP比较好理解,同时,它的最优时间复杂度可以达到O(n/m),其中n为目标串的长度,m为模式串的长度。最坏时间复杂度为O(nm),但是总体来说BM算法的表现要比KMP算法好,我们通常用的ctrl + F 就是用BM算法实现的。
该算法将模式串从后往前与目标串匹配,匹配过程遵循以下两个规则(可以只用其中一个规则,或者两个规则中选择移动距离最长的规则):
(1)坏字符规则:
在当前字符失配的情况下(假设目标串中失配字符为 X,X在目标串的位置为 j ),则检查模式串中是否存在 X 字符,
i: 如果不存在,那么显然X之前的字符串都不可能和模式串匹配。所以我们直接移动m – j + 1个字符来跳过X字符。举个例子:
0 12 3 4 5 6 7 8 9 10 11
…a c b d b c a b c a b c
pattern[ ] a b c a b c
匹配到 d->a 时,出现失配,这个时候为了避开d字符,我们向后移动 6 – 3 + 1个字符,进行下一轮匹配。
ii: 如果模式串中存在X字符,那么我们就找到模式串中最右端的X,假设其位置为pos,将其与目标串中的X与模式串中第pos个字符对齐。
然后从后往前继续进行匹配。例如:
0 12 3 4 5 6 7 8 9 10 11
…a c b b b c a b c a b c
pattern[ ] a b c a b c
移动后: a
b c a b c
规则的实现:进行预处理,记录每个字符出现的最右端的位置。(后面有代码)
(2)好后缀规则:
有两种情况:
i: 举个例子来说明
0 1 2 3 4 5 6 7 8 9 10 11
d d d d b c a b c
pattern[] d b c a b c
我们从后往前匹配,b c匹配成功,这个时候我们称b c为好后缀。
这个时候我们检查模式串中是否存在好后缀,如果存在,我们需要的是最右边的好后缀的起始位置。在例子中,该位置为 1。
这个时候我们将找到的位置 1 与 当前失配位置的后一个位置(即 4) 对齐。实质是将模式串中bc与目标串中的bc对齐。
可以得到:
0 12 3 4 5 6 7 8 9 10 11
d d d d b c a b c
pattern[] d b c a b c
d
b c a b c
ii: 有了第一种情况,第二种情况就好解释了。假设我们已经得到了好后缀,并且在整个模式串中都没有好后缀,但是,模式串的开始有一部分是好后缀的子串(我们称之为 start),此时我们将 start 和 好后缀中相应的部分对齐。例子:
0 1 2 3 4 5 6 7 8 9 10 11
d d d d c a b …
pattern[] a b c c c a b
a b c c c a b
该例中 a b 即为start。
有了这两个规则我们就可以进行匹配了。代码如下:
class BM{
private int[] right;//用来记录每个字符在最右端的位置.
private int[] goodSuffix;//goodSuffix[i]记录若i-1处字符失配时,应该移动的位数。
private int[] t;
private String pattern;
private int size;
public BM(String p) {
pattern = new String(p);
size = pattern.length();
right = new int[256];
t = new int[size+1];
goodSuffix = new int[size+1];
countRight();
countGoodSuffix1();
countGoodSuffix2();
}
private void countRight() // 坏字符规则的预处理
{
for(int i = 0; i < 256; i++)
right[i] = -1;
for(int i = 0; i < size; i++)
right[pattern.charAt(i)] = i;
}
private void countGoodSuffix1()
{
int i = size, j = size + 1;
t[i] = j;
while(i >= 0)
{
while(j <= size && pattern.charAt(i-1) != pattern.charAt(j-1))
{
if(t[j] == 0) goodSuffix[j] = j-i;
j = t[j];
}
i--;j--;
t[i] = j;
}
}
private void countGoodSuffix2() {
int i;
int j = t[0];
for(i = 0; i < size; i++)
{
if(goodSuffix[i] == 0) goodSuffix[i] = j;
if(i == j) j = t[j];
}
}
public int Match(String a)
{
int lena = a.length();
int skip;
for(int i = 0; i <= lena - size; i += skip)
{
skip = 0;
for(int j = size - 1; j >= 0; j--)
{
if(a.charAt(i) != pattern.charAt(j))
{
skip = right[a.charAt(i)] > goodSuffix[j]?
right[a.charAt(i)]:goodSuffix[j];
break;
}
}
if(skip == 0) return i;
}
return -1;
}
}
3.Sunday算法(待续)