数据结构/ 串的模式匹配法 / kmp算法与next数组的构造
模式匹配的基本思想:存在主串S和模式串T,从S的第pos个字符起和T的第一个字符相比较,若相等,逐个比较后续字符;若不相等,从S的pos+1个字符旗重新依次匹配, 直到T中的每个字符和S中的一个连续 字符序列相等,即为匹配成功。
可以据此情景,有这么一道题:
题目描述
如题,给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置。
输入格式:
第一行为一个字符串,即为s1(仅包含大写字母)
第二行为一个字符串,即为s2(仅包含大写字母)
输出格式:
若干行,每行包含一个整数,表示s2在s1中出现的位置
输入输出样例
输入样例#1:
2222bokeyuan111
bokeyuan
输出样例#1:
5
输入样例#2
acabaabaabcacaabc
aabcac
输出样例#2
6
通常的做法是:
#include<bits/stdc++.h> using namespace std; int main() { string str1,str2; cin>>str1; //主串 cin>>str2; //模式串 int len1=str1.size(); //取长 int len2=str2.size(); int j=0; for(int i=0; i<len1; i++) { if(str1[i]==str2[j]) //相等,往后匹配 { j++; if(j==len2) //全部匹配,输出出现的初始位置 { cout<<i-j+2<<endl; j=0; } } else //不相等,换新的起点 { j=0; } } }
运行正常:
这个算法的时间复杂度是O(M*N),效率较低,只是简单地不断重复循环和判断。
然后引入一种优化算法,这个算法是D.EKnuth、V.R.Pratt、J.H.Morris同时发现的,因此称为克努特——莫里斯——普拉特算法,KMP算法。此算法可以在O(M+N)的时间数量级上完成串的模式匹配操作。这种算法不太容易理解,最关键的是构造next数组
eg.我们引入模式串[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 |
计算某字符的next时,看该字符的前一个字符T[j-1],和以这个前字符的next为下标的字符T[netx[j-1]]是否相等,若相等,则该字符的next=这两字符的next之和。
如:在计算next[6]时,T[j-1]=T[6-1]=T[5]=b, T[next[j-1]]=T[next[5]]=T[2]=b, b==b, 所以next[6]=next[5]+next[2]=1+2=3
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 |
若不相等,则按这个方法继续向前寻找,直到找到相同的
若找到起始字符也不相同,则该字符的next=1
如:在计算next[7]时,c!=a
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 |
上述一种理解貌似比较麻烦,而且我好像在某个地方出错了,下面换一个简单一点的理解方法:前缀和后缀
next[i](i从1开始算)代表着,除去第i个数,在一个字符串里面从第一个数到第(i-1)字符串前缀与后缀最长重复的个数+1。
前缀:除去最后一个字符的剩余字符串。
后缀:除去第一个字符的后面全部的字符串。
如,在串"abcd"中,"abc"是前缀,"bcd"是后缀
关于"最长重复子串":首先保证重复,然后是最长,与重复的次数无关。
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 |
还是以这串数组为例,并且默认next[1]=0,next[2]=1。下面开始分析:
next[1] = 0 ,默认值
next[2] = 1 ,默认值
next[3] = 1,即"ab",前缀是"a",后缀是"b",没有最长重复子串,next[3]=0+1;
next[4] = 2 ,即"aba",前缀是"ab",后缀是"ba",都有最长重复子串“a",next[4]=1+1;
next[5] = 2 ,即"abaa",前缀是"aba",后缀是"baa",都有最长重复子串”a",next[3]=1+1;
next[6] = 3 ,即"abaab",前缀是"abaa",后缀是"baab",都有最长重复子串“ab",next[4]=2+1;
next[7] = 1,即"abaabc",前缀是"abaab",最长重复子串“ab";后缀是"baabc",最长重复子串“a",ab!=a,故没有最长重复子串,netx[7]=0+1;
……
这个方法好理解,但不好根据这个思路写代码
代码:
#include<bits/stdc++.h> using namespace std; void oldnext(char p[],int next[],int len) { next[1]=0; int i=1; int j=0; while(i<p[0]) { if(j==0||p[j]==p[i]) { ++i; ++j; next[i]=j; } else j=next[j]; }//while //输出 int k=1; for(k=1; k<len+1; k++) cout<<next[k]<<" "; cout<<" "<<endl; } void kmp(char s[],char p[],int next[],int mlen)//主/模式/next/主长 { for(int i=1,j=0; i<=mlen; i++) { while(s[i]!=p[j+1]&&j>0) j=next[j]; if(s[i]==p[j+1]) ++j; if(j==next[0]) { cout<<i-next[0]+1<<endl; j=next[0]; break; } if(i==mlen) { cout<<"-1"<<endl; } } } int main() { char m[100]; cin>>m+1; int mlen=strlen(m+1); char p[100]; cin>>p+1; int len=strlen(p+1); p[0]=len; m[0]=mlen; int next[len+1]; int nnext[len+1]; next[0]=len; oldnext(p,next,len); kmp(m,p,next,mlen); } //主串m[0]=mlen; //模式串p[0]=len; //n函数 next[0]=len;
void getnext(string T,int next[]) { i=1; next[1]=0; j=0; while(i<T[0]) { if(j==0||T[J]==T[I]) { ++i; ++j; next[i]=j; } else j=next[j]; } }
采纳:https://blog.csdn.net/you_will_know_me/article/details/77102567 https://blog.csdn.net/suguoliang/article/details/77460455 https://oi-wiki.org/string/kmp/