串的模式匹配算法(BF和KMP)
确定主串中所含子串(字串又称为模式串)第一次出现的位置。
BF:暴力破解法,速度慢,使用穷举法思想,容易理解
KMP: 速度快。
BF算法
BF算法设计思想: 函数Index(S, T, pos)
-
将主串S的第pos个字符串和模式串T的第一个字符比较.
若相等,继续逐个比较后续字符。
若不相等,从主串的下一个字符起,重新与模式串T的第一个字符比较。
-
直到主串S的一个连续字串字符序列与模式串T相等,返回值为主串S中与模式串T匹配的子序列第一个字符的位置索引,即匹配成功。
-
否则,匹配失败,返回值0
BF算法时间复杂度:o(n*m)
引例:
有两个串:
S : a a a a b c d T : a b c
S为主串,称作正文串。 T为子串,称为模式串。现在要看主串当中是否出现子串。
BF算法如何做?
将子串与主串逐个比较。
S : a a a -> S: a a a a -> S: a a a a b -> S: a a a a b c
T: a b c -> a b c -> a b c -> a b c
在第四次比较时,发现匹配成功。
BF算法思路:从S的每一个字符开始依次与T的子字符进行匹配,直到找到匹配或者到S尾
例子:
有目标串 S : a a a a a b 模式串T : a a a b。 S的长度为n(n = 6) , T的长度为m(m = 4)
BF算法的匹配过程如下。
S : a a a a a b
T: a a a b
现有i是S的索引, j 是T的索引。从第一个字符开始进行比较。串存储在一维数组中,为了处理方便,将数组0位置不存,直接从1位置开始存储串。
那么开始 i = 1, j =1 。 此时S[ i ] = a T[ j ] = a, 即 S[ 1 ] = T[ 1 ] 。 则继续往下移动。此时已经比较位置 S : a a a a a b T: a a a b
再次比较 i = 2, j =2 此时S[ i ] = a T[ j ] = a, 即 S[ 2 ] = T[ 2 ] 。 则继续往下移动。此时已经比较位置 S : a a a a a b T: a a a b
再次比较 i = 3, j =3 此时S[ i ] = a T[ j ] = a, 即 S[ 3 ] = T[ 3 ] 。 则继续往下移动。此时已经比较位置 S : a a a a a b T: a a a b
再次比较 i = 4, j =4 此时S[ i ] = a T[ j ] = b, 即 S[ 4 ] != T[ 4 ] 。 这里不再相同。此时已经比较位置 S : a a a a a b T: a a a b
在第四次比较时,出现不同,此时需要让 i 回溯, 回溯到 i = 2这个字符的位置。然后再进行比较。此时有一个公式 i = i - j + 2 完成回溯。
这里 i = i - j + 2 = 2 - 2 +2 = 2。此时 i从2开始。理解一下: i - j + 1让 i 回到该轮开始位置,但是我们已经向前走一步, 所以还需要 +1 ,变为 i - j + 1。从而到达下一个初始位置。
此时 i = 2, j = 1。 此时S[ i ] = a T[ j ] = a, 即 S[ 2 ] = T[ 1 ]。则继续往下移动 , 此时已经比较位置 S : a a a a a b T: a a a b
此时 i = 3, j = 2。 此时S[ i ] = a T[ j ] = a, 即 S[ 3 ] = T[ 2 ]。则继续往下移动 , 此时已经比较位置 S : a a a a a b T: a a a b
此时 i = 4, j = 3。 此时S[ i ] = a T[ j ] = a, 即 S[ 4 ] = T[ 3 ]。则继续往下移动 , 此时已经比较位置 S : a a a a a b T: a a a b
此时 i = 5, j = 4。 此时S[ i ] = a T[ j ] = a, 即 S[ 5 ] != T[ 3 ]。这里不再相同 , 此时已经比较位置 S : a a a a a b T: a a a b
然后再需要回溯,i = i - j + 2 = 5 - 4 + 2 = 3。此时i从3开始。j 从 1开始。继续这样下去匹配。
。。。。。。
匹配成功时 i = 7, j = 5。因为匹配成功则两个索引都索引到各自字符的最后一个元素。则**返回匹配位置为: i - T.length = 3, 就是 i 的值 - 子串长度。**此时 i 的值代匹配成功下,T的末尾元素在S中的位置。
代码:SString 中包含 字符数组ch[maxsize] 和 length 两个成员。一个存放字符串,一个存放字符串长度。
//从初始位置
int Index_BF(SString S, SString T){
int i = 1, j = 1;
while(i <= S.length && j <= T.length){
if(S.ch[i] == t.ch[j]){ //主串和子串元素匹配,则进行下一个元素比较
++i;
++j;
}
else{
i = i - j + 2; //到不匹配位置,则主串索引回溯,子串索引归一。
j = 1;
}
}
if(j >= T.length){ //当j到达T的尾部说明已经成功,否则没有成功。
return i - T.length //匹配成功
}
else{
return 0 //匹配失败。
}
}
//从中间位置
int Index_BF(SString S, SString T,int pos){
int i = pos, j = 1;
while(i <= S.length && j <= T.length){
if(S.ch[i] == t.ch[j]){ //主串和子串元素匹配,则进行下一个元素比较
++i;
++j;
}
else{
i = i - j + 2; //到不匹配位置,则主串索引回溯,子串索引归一。
j = 1;
}
}
if(j >= T.length){ //当j到达T的尾部说明已经成功,否则没有成功。
return i - T.length //匹配成功
}
else{
return 0 //匹配失败。
}
}
时间复杂度: 若n为主串长度,m为子串长度。
最坏的情况:主串前n - m 个位置部分匹配到子串的最后一位才不匹配,此时前n-m位,每个位各比较了m次。
最后m位正好匹配,也比较了m次。
则总次数:(n-m)*m + m = (n - m + 1) * m。 若n >> m, 则算法复杂度为 o(n * m)
KMP算法
KMP算法相对BF算法速度有了提高,因为主串S的索引 i 不必回溯。时间复杂度变为o(n+m)
例子:
S = a b a b c a c b c a b c a c b a b
T = a b c a c
当S = a b a b c a b c a c b c a c b a b T = a b c a c。 S和T在第三个位置不匹配。此时不需要回溯,从哪失败从哪开始,可以从 i = 3 ,j = 1开始比较。不需要 i 单独前移一个元素。因为前面已经确定不匹配。
S = a b a b c a b c a c b c a c b a b T = a b c a c。 i从3开始,此时i = 7时不一样。
但是因为T : a b c a c,前后两个a一样,故此时,j可以从2开始。此时S = a b a b c a b c a c b c a c b a b T = a b c a c。至于为何不从i = 7 j = 1开始。个人觉得因为此时,i = 6(a)和i = 7(b) 的位置,与j = 1(a)和j =2(b)位置的元素其实是相同的,特别是,i = 6的位置元素已经和j =4 的位置元素已经匹配过了。但是实际上,i = 7的位置为元素匹配失败位置,所以从i =7 ,j回溯到2,因为j = 1的元素与j = 4的元素相同,已经匹配不需要j回溯到1。这样没有违背,i 从失败位置开始的规则。
总结,i可以不用回溯,j可以不用回到固定位置1。
那么在匹配失败后 j 从哪个位置开始呢。
使用next数组存放下一个j的位置,称为next[ j ]。它的计算如下。
当 j = 1, 则next[ j ] = 0。
j != 1,需要在模式串中,从头开始的k-1个元素, 如果和当前模式串的位置 j 的前 k - 1个元素匹配了。那么这个next[j] = k。就是说找到这个关键的k值在哪。其实就是在模式串中找,当前 j的位置元素,前几个元素,与模式串开头几个元素,是否一样,如果一样,此时模式串首部开始,直到与j位置不一样的位置 ,就是我们 j 回溯的位置。
a b c d e b b a b c. 当J位于最后一个位。与开头第一个是不一样的。
a b c d e b b a b c. 再往前一位, a b 与 b c不一样
a b c d e b b a b c 再往前一位, a b c 与 a b c一样,所以这里的k = 4。在第四位不一样,j 回溯到4处。
主要是为了找到,在模式串不匹配的位置处元素,有没有和模式串开头元素一样的。这样就可以跳过这段开头位置,从直到与末尾处元素不一样的开始。
比如有一个串 S: a b c d a b c d a b g c 和一个串T a b c d a b c e f 在 i j = 7位置不匹配。那么此时 j 应该回溯到哪呢,
因为 T :a b c d a b 在这些元素之后才不匹配,所以看已经匹配的位置,首尾有部分相同,那么此时从下一处开始就不用从 1开始,因为1, 2由于和3,4 相同,在上一轮匹配中已经和S匹配,所以不需要从1,2 开始了,它留在了上一轮的匹配成果中,j直接从3开始。
所以 J 的回溯规则是,找到目前已经匹配的部分下,首尾一样的部分,回溯到首部不一样的部分之处,因为首部一样的部分,已经在上一轮匹配中,由尾部匹配过。
因为j的回溯只与模式串有关,所以在匹配开始之前,我们可以通过模式串求出模式串每一位的next[j].*
KMP代码:
int Index_KMP(SString S, SString T, int pos){
i = pos, j = 1;
while(i < S.length && j < T.length){
if(j == 0 || S.ch[i] == T.ch[j]){
i++;
j++;
}
else{
j = next[j]; //i 不变,j后退
}
}
if(j > T.length){
return i - T.length; //匹配成功
}
else{
return 0; //匹配失败
}
}
求取next [j],
求取每个位的next[ j ]。 next[ i ]中存放的是每个位的next[ j ] ; j = next[ j ]是一个回溯,每当不匹配时,next[ j ] 其实为0。这样 j = 0,又从头开始匹配,此时next[ j ] = 0。但是 i 还是继续往前走。走到匹配的,这个时候两个同时前进,此时next[ j ]就会一直产出非0值。
void get_next(SString T, int &next[]){
i = 1; next[1] = 0; j = 0;
while(i < T.length){
if(j == 0 || T.ch[i] == T.ch[j]){
++i;
++j;
next[i] = j;
}
else{
j = next[j];
}
}
}
next函数的改进
问题发现:
当在j = 4时,不匹配。j回退,根据next[ 4 ] , j 回退到 j = 3。但是到 j = 3位置,j = 3位置的a 依旧与 i = 4 位置的b不匹配,那么 j 要继续回退, 再进行比较,直到发现回退到 j = 1 与 i = 4都不匹配。那么此时再回退, next[ j ] = 0。 这个时候,进入 j == 0判断标准, i++, j++。 之后 从 i = 5, j = 1,开始。
问题指出:当 j = 4时,j =1 , j =2, j = 3。都不需要比较,因为 j =4 的元素和前面三个相同。故不需要比较肯定无法与 i =4位置的元素相等。此时根本不要往回退。可以直接 i +1。
优化:对next[ j ]进行修正。设定修正后的next值为nextval。
可以先求出未修正的next[ j ]。 然后根据回退后位置上的元素与回退前的位置元素是否相同进行修正。若相同则该位也无法得出结果,如果回退后位置上还能回退,则继续回退比较,直到回退到位置上元素不一样为止。那么修正的next[ j ]就是最后一次回退的next值。 如果直到最后一次next[ j ] = 0,都一样, 则修正后的next[ j ] = 0。
需要看重第5次,它是一个连环比较。
&emsp;修正的next代码
**
void get_nextval(SString T, int &nextval[]){
i = 1, nextval[1] = 0; j = 0;
while(i < T.length){
if(j == 0 || T.ch[i] == T.ch[j]) //要么匹配,要么回到开头重新匹配
{
++i;
++j;
if(T.ch[i] != T.ch[j]){ //
nextval[i] = j; // 这里是正常的不需要优化的,需要回退的位置。注2
}
else{
nextval[i] = nextval[j]; //这里是优化后的,注1
}
}
else{
j = nextval[j] //都不匹配回到原点,进入注2后会直接到这里,就是说进行下一位的匹配操作。
}
}
}
为何注1,这里就只有一次赋值,没有连续比较的感觉。因为这里是从后面比较来的,没有多余的重复。(个人目前理解)还需要再理解。