前言
最早前写过一篇关于如何寻找子串在主串中的出现位置,详见如何在主串中寻找子串的出现次数
具体就是调用strstr函数,可以做到O(n+m)的线性复杂度,那么内部如何实现呢?
答案是KMP
继续下去之前请思考这样一个问题,给两个非空字符串str1, str2 如何寻找str2在str1中出现的第一个位置?所有位置呢?
暴力求解
- 引入两个概念,文本串 模式串。一般说的是求模式串在文本串中的出现位置,了解这个说法就行
- 其次我们在下文中会使用指针这个说法,不要在意,它并不是c语言中那个令人头疼的指针,把他理解为一个箭头就行
如上图中,txt代表文本串,S代表模式串,而P1,P2分别指向两字符串中的某一个字符
在暴力求解时,每当 TXT[ P1 ] == S[ P2 ] 时,P1,P2指针同时向后移动 , 当然最好的情况是该条件一直成立
- 当P2移动到模式串的结尾时(并且上述式子成立),则代表了模式串在文本串中出现了一次,此时P2 - S.length 就是模式串在文本串中的第一次出现位置
如果P2没有移动到模式串结尾,但是上述式子不成立怎么办?我们称这个为:失配
此时就要移动P1 P2指针到正确位置重新匹配, p1 = p1 - p2 + 2 , p1 = 1
void findPos(string txt, string s){
int txtLen = txt.length();
int sLen = s.length();
int p1 = 0, p2 = 0;
while(p1 < txtLen){
//开始匹配
while(p2 < sLen && txt[p1] == s[p2]){
p1++,p2++;
}
if(p2 == sLen){ //找到了一个位置
//对应操作
} else{ //失配了,重新调整两个指针
p1 = p1 - p2 + 2;
p2 = 0;
}
}
}
//暴力求解代码不保证正确性
那么他的复杂度怎么样呢?因为每一次失配后p1指针都要移动到匹配模式串时的下一个位置,也即是p1 = p1-p2+2,相当于p1在匹配前的基础上+1,那么复杂度为O(txtLen*sLen) = O(mn)
KMP
经过上面的暴力写法可以看出,想要优化时间,必须想办法解决失配之后p1,p2指针向前移动的距离
下面给出一个例子,我们来看看他的p1,p2指针该如何移动:
可以看到前面abcx…都匹配成功了,但是好可惜,它在p1,p2的下一个位置失配了!
3秒中思考p1, p2指针最佳移动是什么?
3
2
1
…
最佳移动如上图!
发现p1并没有移动,而p2移动到了一个特别的地方,为什么特别呢?从图中可以看出p2往前是ab,而没移动之前p2的前面也是ab,我们称这个为最长前缀匹配 想到了嘛?
移动之后我们要看p1,p2的下一个位置是否匹配,如果还是不匹配,是不是也是失配了?那么移动和上述过程一样!移动到他的最长前缀匹配处!但是看图发现p2在的地方貌似没有匹配到的前缀了,那就直接把p2设置为0,这里我们假设字符串的第一个位置是1
现在假设我们移动p2到了位置0,现在p2已经不能往哪里移动了,p1开始往下移动一位,然后和p2的下一个位置进行比较
这时发现p2的下一个位置和p1指向的字符匹配成功了!,那么p2移动到下一个位置也即是p2++
然后再次判断p2下一个位置和p1指向字符是否匹配
发现没?我们回到了最初的情况. 接下来就是重复上诉过程了
如果在其中p2奇迹般移动到了S的结尾,则说明匹配到了S在txt中的一个位置
构造失配数组 fail[ ]
上诉过程中P1指针的移动并没有什么特色,就只是p1++而已
但是p2在失配之后,需要根据最长前缀来选择移动位置,称为失配之后移动到的位置,个个位置弄一起就是失配数组了。 那么如何构造它呢?
无可厚非失配数组只和模式串有关,下面我们开始只关注模式串
当前p指向的位置的最长匹配前缀为2 (ab)
如果我们要构建p的下一位(记为位置 i)字符‘c’的最长匹配前缀呢?判断S[ fail[ p ] + 1 ] 是否也是‘c’就行,
- 如果是则 fail [ i ] == fail [ p ] + 1
- 如果不是?那么我们能不能理解为不符合S[fail[p]+1] == S[i]而叫做失配呢?,那么这时操作和文本串和模式串匹配时的操作一样,p 当前位置失配,p移动到他的最长前缀匹配处, 也即是 p = fail[ p ] , 接着再次判断上述式子是否成立,或者p是否移动到了0
那么构造失配指针代码如下:
char str[maxn], s[maxn]; //文本串,模式串
int fail[maxn]; //失配指针
void getFail(){ //求出失配指针
int len = strlen(s+1); //假设s初始位置为1
int tmp = fail[1] = 0; //tmp则是上面讨论的p指针, 第一个位置当然最长匹配前缀为0
for(int i=2;i<=len;i++) {
while(tmp != 0 && s[tmp+1] != s[i]) tmp = fail[tmp]; //失配,并且p指针不指向 0
if(s[i] == s[tmp+1]) tmp++;
fail[i] = tmp;
}
}
寻找位置
构造完成失配数组之后就可以开始和文本串匹配了,过程就不在赘述了,代码如下:
void findPos(){ //在文本串中寻找位置
int lenStr = strlen(str+1); //一样的,假设串的初始位置为 1
int lenP = strlen(p+1);
int tmp = 0; //tmp 也就是指向模式串位置的指针 P2
for(int i=1;i<=lenStr;i++) { // i 就是指向文本串位置的指针 P1
while(tmp !=0 && p[tmp+1] != str[i]) tmp = fail[tmp]; //失配
if(p[tmp+1] == str[i]) tmp++;
if(tmp == lenP){ // 匹配成功!!!
cout<<i-tmp+1<<endl;
tmp = fail[tmp];
}
}
}
多位置匹配
看匹配成功之后的那小段代码, 第一句是输出出现位置毫无疑问
那么第二句呢? tmp = fail [tmp] 这貌似是失配才会发生的操作
请考虑如何找出模式串在文本串中出现的所有位置?
egs:
文本串: aabaabaaba
模式串: aabaa
假设字符串位置从1开始,那么所有位置应该是1 , 4
上图可以看出,p2已经到了S的结尾,说明找到了一个位置,那么p2 = fail [p2]会移动到S的第二个位置
最后可以思考一下: 为什么要tmp = fail [ tmp ] 如果tmp = 0又是一个什么情况
end
练习题地址:洛谷-KMP模板题
感谢看到最后 😃 😃