T[0...n-1]
;P[0...m-1]
;T
和P
中的元素都属于有有限的字母表 Σ \Sigma Σ表;假设当前文本串S匹配到i
位置(初始为0),模式串P匹配到j
位置(初始为0),则有:
//使用暴力匹配的方法返回子串P在主串S中第pos个字符后的位置;
//若不存在,则函数值返回-1
//其中S[0...n-1],P[0...m-1]
int ViolentMatch(string s, string p,int pos) {
int slen = s.length();
int plen = p.length();
int i = pos;
int j = 0;
while (i < slen&&j < plen) {
if (s[i] == p[j]) { i++; j++; } //如果匹配成功,则匹配下一位
else { i = i - j + 1; j = 0; } //如果匹配失败,则i回溯,j置为0
}
if (j == plen) return i - plen;//匹配成功
else return -1; //匹配不成功
}
"部分匹配"
的情形,则s的指针i会很容易出现回溯。"部分匹配"
的结果将模式尽可能远的向右"滑动"尽可能远的一段距离后,再继续进行比较。这就需要我们下面介绍的KMP算法
。最好的情况下:
O ( n + m ) O(n+m) O(n+m),其中n和m分别是主串和模式的长度
最差的情况下:
O ( n ∗ m ) O(n*m) O(n∗m)(经常出现"部分匹配",导致指针 i i i不断回溯)
如2.3
所说的一样,在不匹配的时候,我们可以利用部分匹配
的那部分来获得一些文本的信息以减少回溯。
如文本串T=“ababcabcacbab”,模式P=“abcac”。
第一趟匹配:匹配到i=2,j=2时,S[2]!=P[2]。
如果按照暴力匹配方法,则i需要回溯到1但是我们一定会有S[1]!=P[0],
即如果我们利用已经匹配的那部分信息,我们会有S[1]=P[1]!=P[0]。
这就表明利用已经匹配的部分,我们可以不回溯i而是把j对应"滑动"到相应位置,即第一趟匹配后i=2,j=0
。
第二趟匹配:匹配到i=6,j=4时,我们有S[6]!=P[4].
但是,利用已匹配的部分,我们会有
所以,i是不用回溯的,即我们可以直接让i=6,j=1
,再继续进行匹配。
这就是,我们KMP算法解决的问题。
KMP算法由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,所以以他们的名字来命名,叫Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”。
算法流程:
假设当前文本串S匹配到i
位置(初始为0),模式串P匹配到j
位置(初始为0),则有:
j-next[j]
就相当于我们在3.1
所述的利用"已匹配"
部分得出i不回溯但是j相对向右滑动j-next[j]位。前缀后缀
。例如next [j] = k,代表j 之前的字符串中有最大长度为k 的相同前缀后缀。代码:
//使用KMP算法返回子串P在主串S中第pos个字符后的位置;
int KmpMatch(string s, string p, int pos) {
int slen = s.length();
int plen = p.length();
int i = pos;
int j = 0;
int* nextP = new int[plen];
get_next(p, nextP); //获得模式串P的next函数并存入数组next
while (i < slen&&j < plen) {
if (s[i] == p[j] || j == -1) { i++; j++; }
else j = nextP[j];
}
delete[] nextP;
if (j == plen) return i - plen;
else return -1;
}
以3.1
的第二趟匹配举例:i=6,j=4
此时,我们有S[6]≠P[4]。但是我们有S[2,3,4,5]=P[0,1,2,3],同时j=4之前,模式串有长度k=1的前缀,即P[0]=P[3]=‘a’,且P[3]=S[5],所以P[0]=P[5],得出我们不用匹配S[3]和P[0/1/2/3]而直接匹配S[6]和P[1]。
这里,我们找的是紧在
j
(也就是i
)之前的前缀,而没有考虑在i的起始位置 i 0 i_{0} i0到位置 i − 1 i-1 i−1中间位置的前缀,因为即使有前缀,因为如果不能接着匹配完模式串也是徒劳。
例如下面这种情况:
正如我们看到的,在 j = 0 j=0 j=0到 j = 6 j=6 j=6之间,我们能找到两个前缀:
如果我们选择P[0,1]和P[2,3],这下次匹配 i i i=6, j j j=2 (因为我们有S[4,5]=P[2,3]=P[0,1]),所以就省略了一些匹配.但是我们会发现由于前缀
并不完整,这样的选择方式,虽然能匹配P[0,1]但是却肯定不能接着把模式串P匹配完。
所以,KMP算法
选择的是P[0,1]和P[5,6]这样,我们的 i i i不用回溯,即 i = 9 i=9 i=9,同时有S[7,8]=P[5,6]=P[0,1],所以 j j j就直接等于2,即 i = 9 , j = 2 i=9,j=2 i=9,j=2。此时,是有可能将模式串匹配完的,因为 i = 9 i=9 i=9之后的S我们是不知道的。
假设主串为 s 0 s 1 . . . s n − 1 s_{0}s_{1}...s_{n-1} s0s1...sn−1 ,模式串为 p 0 p 1 . . . p m − 1 p_{0}p_{1}...p_{m-1} p0p1...pm−1,当前匹配位置为 i 和 j i和j i和j,且出现失配,即 s i ≠ p j s_{i}≠p_{j} si̸=pj。
KMP算法求解的问题的是 i i i指针不动, j j j应该向右滑动多远,即 i i i和哪个 j j j比较?
假设下次与模式串中的第 k ( k < j ) k(k<j) k(k<j)个字符比较,则模式串前 k − 1 k-1 k−1个字符必满足下式,且不存在 k ′ > k k^{'}>k k′>k满足下式:
p 0 p 1 . . . p k − 2 = s i − k + 1 s i − k + 2 . . s i − 1 p_{0}p_{1}...p_{k-2} = s_{i-k+1}s_{i-k+2}..s_{i-1} p0p1...pk−2=si−k+1si−k+2..si−1
而通过已匹配的部分有:
p j − k + 1 p j − k + 2 . . . p j − 1 = s i − k + 1 s i − k + 2 . . s i − 1 p_{j-k+1}p_{j-k+2}...p_{j-1} = s_{i-k+1}s_{i-k+2}..s_{i-1} pj−k+1pj−k+2...pj−1=si−k+1si−k+2..si−1
则,我们有等式:
p 0 p 1 . . . p k − 2 = p j − k + 1 p j − k + 2 . . . p j − 1 p_{0}p_{1}...p_{k-2} = p_{j-k+1}p_{j-k+2}...p_{j-1} p0p1...pk−2=pj−k+1pj−k+2...pj−1 (*)
也就是说,当 i i i和 j j j失配时,下次 S [ i ] S[i] S[i]和 P [ k ] P[k] P[k]进行匹配,且 k k k满足 ( ∗ ) (*) (∗)式。
即失配
时,next函数的定义:
n e x t [ j ] = k next[j]=k next[j]=k表示 j j j 之前有长度为 k k k 的前缀后缀,注意 j j j 从
0
开始。如下图所示:
其中 p 0 p 1 . . . p k − 1 ( 前 缀 ) = p j − k p j − k + 1 . . . p j − 1 ( 后 缀 ) , 长 度 为 k 。 p_{0}p_{1}...p_{k-1}(前缀)=p_{j-k}p_{j-k+1}...p_{j-1}(后缀),长度为k。 p0p1...pk−1(前缀)=pj−kpj−k+1...pj−1(后缀),长度为k。
举例说明P=“abaabcaca”
j | 1 2 3 4 5 6 7 8 |
---|---|
模式串 | a b a a b c a c |
next[j] | -1 0 0 1 1 2 0 1 |
再求得next数组后,匹配如下进行:
假设 i i i和 j j j分别指示主串和模式中正比较的字符
失配
),则此时模式继续向右滑动一个位置,即 j = 0 j=0 j=0,同时从主串的下一个字符s_{i+1}和模式重新开始匹配。注意, j j j退到-1表示主串的第 i i i个字符和模式串的第一个字符(P[0])不等,应该从主串的第 i + 1 i+1 i+1起重新匹配。
方法:利用递推公式
由3.4.2
可知,next函数值只取决于模式串P本身,而我们可以通过3.4.2
的定义出发利用递推的方法求得next函数值。
模式匹配
问题,即整个模式串既是主串又是模式串,而且在当前匹配过程中有 p 0 p 1 . . . p k − 1 = p j − k p j − k + 1 . . . p j − 1 p_{0}p_{1}...p_{k-1} = p_{j-k}p_{j-k+1}...p_{j-1} p0p1...pk−1=pj−kpj−k+1...pj−1 ( n e x t [ j ] = k next[j]=k next[j]=k)。注意,理解递推思想的关键是
next[j]=k
代表j
之前的字符串中有最大长度为k
的前缀后缀。
代码实现:C++
//求模式串P的next函数并存入数组next[p.size()]。
void get_next(string p, int* next) {
int plen = p.length();
int j = 0; //当前求next[j]
int k = -1;//已求得next[j]=k
next[j] = k;
while (j < plen) {
//p[j]表示后缀,p[k]表示前缀
if (k == -1 || p[j] == p[k]) { j++; k++; next[j] = k; } //next[j+1]=next[j]+1;next[1]=0;
else k = next[k];
}
}
next函数求解优化:
前面定义的next函数在某些情况下有缺陷:
例如模式串P="a a a a b"和主串S="a a a b a a a a b"进行匹配时,
当 i = 3 , j = 3 i=3,j=3 i=3,j=3时, s [ 3 ] ≠ p [ 3 ] s[3]≠p[3] s[3]̸=p[3],此时, s [ 3 ] s[3] s[3]还要和 p [ n e x t [ 3 ] ] = p [ 2 ] , p [ n e x t [ 2 ] ] = p [ 1 ] , p [ n e x t [ 1 ] ] = p [ 0 ] p[next[3]]=p[2],p[next[2]]=p[1],p[next[1]]=p[0] p[next[3]]=p[2],p[next[2]]=p[1],p[next[1]]=p[0]进行比较,但是因为 p [ 3 ] = p [ 2 ] = p [ 1 ] = p [ 0 ] = ′ a ′ p[3]=p[2]=p[1]=p[0]= 'a' p[3]=p[2]=p[1]=p[0]=′a′,所以,这3次比较多是多余的。
问题的关键是,不应该出现 p [ j ] = p [ n e x t [ j ] ] {\color{Red} p[j] = p[next[j]]} p[j]=p[next[j]]。因为失配时,有 s [ i ] ≠ p [ j ] s[i]≠p[j] s[i]̸=p[j],也同样会造成 s [ i ] ≠ p [ n e x t [ j ] ] s[i] \neq p[next[j]] s[i]̸=p[next[j]],所以应该避免这种情况,即出现之后要再次对next数组进行递归。
代码实现:C++
//求模式串P的next函数优化值,并存入数组nextval
void get_nextval(string p, int* nextval) {
int plen = p.length();
int j = 0;
int k = -1;
nextval[j] = k;
while (j < plen) {
//p[j]表示后缀,p[k]表示前缀
if (k == -1 || p[j] == p[k]) {
j++; k++;
if (p[j] != p[k]) nextval[j] = k;
//因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归,k = next[k] = next[next[k]]
else nextval[j] = nextval[k];
}
else k = nextval[k];
}
next数组求解复杂度: O ( m ) O(m) O(m),其中m是模式串的长度
KMP搜索的复杂度: O ( n ) O(n) O(n),因为指针 i i i是不回溯的
优点:主串的指针不回溯,可以边读入主串边匹配,对处理外设输入的文件很有效。
KMP的匹配是从模式串的开头开始匹配的,而1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹配算法:Boyer-Moore算法
,简称BM算法
。该算法从模式串的尾部开始匹配,且拥有在最坏情况下O(N)
的时间复杂度。在实践中,比KMP算法的实际效能高。
未完待续。。。。。。
Sunday算法
由Daniel M.Sunday在1990年提出,比BM算法效率更高,且更容易理解。
未完待续。。。。。。
1.《数据结构 c语言版》 严蔚敏著
2.从头到尾彻底理解KMP. 作者:v_JULY_v
https://blog.csdn.net/v_july_v/article/details/7041827