串的简单模式匹配
给出主串S和模式串T,要求找出T出现在S中的第一个位置。很容易想到的枚举法,就是从第一个字符开始,逐个比较S和T的字符,若相等就继续比较后续字符,否则从S当前与T匹配的第一个字符的下一个字符起再重新和T的第一个字符比较。即初始化 i = j = 0,若S[i] == T[j],则 i++; j++; 否则 i = i - j + 1; j = 0;
代码如下:
int Match(string const &S, string const &T)
{
/* 找出T在S中出现的第一个位置,若不存在,则返回-1
*/
int i = 0, j = 0, n = S.length(), m = T.length();
while(i < n && j < m) {
if(S[i] == T[j]) {i++; j++;}
else {i = i - j + 1; j = 0;} // 不相等,i和j都回溯
}
if(j == m) return i - m;
else return -1;
}
方法虽然简单,且在一般情况下实际执行时间近似于O(m + n),但是它的时间复杂度是O(m * n)的,例如 S = “aaaaaaaaaaaaaaaaaaaaaaaaaab”,T = “aaaaaaaab”。
模式匹配的改进算法—KMP
KMP算法可以在O(m + n)的时间数量级上完成串的模式匹配操作。改进之处在于:每当一趟匹配过程中出现字符比较不等时,不需回溯i,而是利用已经得到的“部分匹配”的结果将模式串T向右“滑动”尽可能远的一段距离后,继续进行比较。
假设S = ‘s1s2…sn’, T = ‘p1p2…pm’,由上面的讨论可知,当匹配过程中产生“失配”(即si != pj)时,T应该向右“滑动”多远。换句话说,si应该与T中的哪个字符再比较?
假设此时应与第k(k < j)个字符比较,则T中的前k - 1个字符必须满足
'p1p2...p(k-1)' == 's(i-k+1)s(i-k+2)...s(i-1)' (1)
而已经得到的“部分匹配”的结果是
'p(j-k+1)p(j-k+2)...p(j-1) ' == 's(i-k+1)s(i-k+2)...s(i-1)' (2)
由(1)和(2)可推得下列等式
'p1p2...p(k-1)' == 's(i-k+1)s(i-k+2)...s(i-1)' (3)
因此,若T中存在满足(3)式的两个子串,则S中的第i个字符与T中的第j个字符不等时,只需将T向右“滑动”至T[k],再与S[i]继续比较即可。因为此时’p1p2…p(k-1)’ == ‘s(i-k+1)s(i-k+2)…s(i-1)’。
若令next[j] = k表示当模式中的第j个字符与主串中的相应字符“失配”时,在模式串中重新和主串进行比较的字符的位置。则next函数的定义为 next[j] =
0 当 j == 1 时
max{ k | 1 < k < j 且 'p1...pk-1' == 'p(j-k+1)...p(j-1)'} 当此集合不空时
1 其它情况
例如:S = “acabaabaabcacaabc”, T = “abaabcac”,按上述next的定义可知
j 1 2 3 4 5 6 7 8
T a b a a b c a c
next 0 1 1 2 2 3 1 2
KMP的工作过程:i和j分别指向S和T中的待比较字符,初始化i = j = 1。若S[i] = T[j] ,则 i和j都自增1,否则i不变,而j退回到next[j]的位置再比较,若依旧不相等,j再退回到下一个next[j]的位置,直至j=0,i和j都自增1。
i 1 i 3 4 5 6 7 8 9
S a c a b a a b a a b c a c a a b c
T a b
j 1 j
此时,S[i] != T[j],j = next[2] = 1,i不变:
i 1 i 3 4 5 6 7 8 9
S a c a b a a b a a b c a c a a b c
T a
j j
此时,S[i] != T[j],j = next[1] = 0,i和j都自增1,即i = 3, j = 1:
i 1 2 3 4 5 6 7 i 9
S a c a b a a b a a b c a c a a b c
T a b a a b c
j 1 2 3 4 5 j
此时,S[i] != T[j],j = next[6] = 3,i不变:
i 1 2 3 4 5 6 7 8 9 i
S a c a b a a b a a b c a c a a b c
T a b a a b c a c
j 1 2 3 4 5 6 7 8 j
此时,i = 14, j = 9 ,已经匹配得到答案。
KMP和本文一开始所述算法的不同之处在于:当匹配过程中产生“失配”时,指针i不变,指针j退回到next[j]所指示的位置上重新进行比较,并且当指针j退至0时,指针i和指针j需同时增1。即若主串的第i个字符和模式的第1个字符不等,应从主串的第i+1个字符起重新进行匹配。
代码如下:
/*
* 上述讨论的是假设下标从1开始,这里从0开始
* 找到就返回原下标的位置,否则返回-1
*/
int KMP(string const &S, string const &T, int *next)
{
int i = 0, j = 0, m = T.length(), n = S.length();
while(i < n && j < m) {
if(-1 == j || S[i] == T[j]) {i++; j++;}
else j = next[j];
}
if(j == m) return i - m;
return -1;
}
KMP算法是在已知模式串的next函数值的基础上执行的,那么如何求得模式串的next函数值呢?
因为next函数值仅取决与模式串本身而和主串无关,所以可以从定义出发用递归的方法求出next函数的值:
由定义可知:
next[1] = 0
设next[j] = k,则:
'p1...p(k-1)' = 'p(j-k+1)...p(j-1)'
此时next[j+1]的值有两种情况:
若 k == 0 || pk == pj,则 next[j+1] = k + 1 (1)
若 pk != pj,则 k = next[k],跳(1) (2)
代码如下:
void get_next(string const &T, int *next)
{
next[0] = -1, next[1] = 0;
int j = 1, m = T.length();
int k = next[j];
while(j < m) {
if(-1 == k || T[k] == T[j]) next[++j] = ++k;
else k = next[k];
}
}
上述求得的next函数在某些情况尚有缺陷.例如:S = ‘aaabaaaab’,T = ‘aaaab’时,当 i = 4, j = 4时,S[i] != T[j].根据上述方法求得的next的指示,还需要进行i = 4, j = 3; i = 4, j = 2; i = 4; j = 1这3次比较.实际上,因为T中的前3个字符和第4个字符相等,所以应该将T一次向右滑动4个字符直接进行i = 4, j = 1时的字符比较
j 1 2 3 4 5
T a a a a b
next 0 1 2 3 4
newnext 0 0 0 0 4
newnext的代码如下:
/*
*下标从0开始
*/
void get_next(string const &T, int *next)
{
next[0] = -1, next[1] = 0;
int j = 1, m = T.length();
int k = next[j], tmp = 0;
while(j < m) {
if(-1 == k || T[k] == T[j]) {
if(-1 == k) next[++j] = ++k;
else if(T[k+1] != T[j+1]) {k = tmp + 1; next[++j] = k; tmp = 0;}
else {++tmp; next[++j] = k;}
}
else k = next[k];
}
}