KMP算法
KMP算法就是省略了朴素算法中不必要的回溯。在查找字符串前,先对需要查找的字符串做一个分析,这样可以大大减少我们查找的难度,提高查找的速度。
next数组值的推导
我们把需要查找的字符串j值的变化定义为一个数组next
next[j]=0 当j=1时
next[j]=Max{k|1<k<j,且 当次集合不为空时
next[j]=1 其他情况
举几个栗子
(1)T=“abcdex” (如下表所示)
j | 123456 |
模式串T | abcdex |
next[j] | 011111 |
①当j=1时, next[1]=0。
②当j=2时,j由1到j-1就只有字符“a”,属于其他情况next[2]=1。
③当j=3时,j由1到j-1串是“ab”,显然“a”与“b”不相等,属其他情况, next[3]=1。
④以后同理,所以最终此T串的next[j]为011111。
(2)T=“abcabx” (如下表所示)
j | 123456 |
模式串T | abcabx |
next[j] | 011123 |
①当j=1时, next[1]=0。
②当j=2时,同上例说明, next[2]=1。
③当j=3时,同上, next[3]=1。
④当j=4时,同上, next[4]=1。
⑤当j=5时,此时j由1到j-1的串是“abca”,前缀字符“a”与后缀字符“a”
相等(前缀用下画线表示,后缀用斜体表示),因此可推算出k值为2(由得到p₁=p₄)因此next[5]=2。
⑥当j=6时,j由1到j-1的串是“abcab”,由于前缀字符“ab”与后缀“ab”相等,所以next[6]=3。
我们可以根据经验得到如果前后缀一个字符相等,k值是2,两个字符相等k值是3,个字符相等k值就是n+1。
(3)T=“ababaaaba” (如下表所示)
j | 123456789 |
模式串T | ababaaaba |
next[j] | 011234223 |
①当j=1时, next[1]=0。
②当j=2时,同上next[2]=1。
③当j=3时,同上next[3]=1。
④当j=4时,j由1到j-1的串是“aba”,前缀字符“a”与后缀字符“a”相等,next[4]=2。
⑤当j=5时,j由1到j-1的串是“abab”,由于前缀字符“ab”与后缀“ab”相等,所以next[5]=3。
⑥当广=6时,j由1到j-1的串是“ababa”,由于前缀字符“aba”与后缀“aba”相等,所以next[6]=4。
⑦当j=7时,j由1到j-1的串是“ababaa”,由于前缀字符“ab”与后缀“aa”并不相等,只有“a”相等,所以next[7]=2。
⑧当j=8时,j由1到j-1的串是“ababaaa”,只有“a”相等,所以next[8]=2。
⑨当j=9时,j由1到j-1的串是“ababaaab”,由于前缀字符“ab”与后缀“ab”相等,所以next[9]=3。
(4)T=“aaaaaaaab” (如下表所示)
j | 123456789 |
模式串T | aaaaaaaab |
next[j] | 012345678 |
①当j=1时, next[1]=0。
②当j=2时,同上next[2]=1。
③当j=3时,j由1到j-1的串是“aa”,前缀字符“a”与后缀字符“a”相等,next[3]=2。
④当j=4时,j由1到j-1的串是“aaa”,由于前缀字符“aa”与后缀“aa”相等,所以next[4]=3。
⑤⋯⋯
⑥当j=9时,j由1到j-1的串是“aaaaaaa”,由于前缀字符“aaaaaaa”与后缀“aaaaaaa”相等,所以next[9]=8。
其实我看完了公式和样例还是一脸懵,没搞懂是怎么推导的,然后搞到了这张动图
计算匹配T的next数组
/*通过计算返回子串T的next数组*/
void get_next(String T,int *next)
{
int i,k;
i=1;
k=0;
next[1]=0;
while(i<T[0])
{
if(k==0||T[i]==T[k])
{
++i;
++k;
next[i]=k;
}
else
k=next[k];//若字符不相同,则k值回溯
}
}
匹配查找
/*返回子串T在主串S中的第pos个字符之后的位置。若不存在则返回0*/
/*T非空,1<=pos<=strlen(S)*/
int Index_KMP(String S,String T,int pos)
{
int i=pos;//i用于主串S中当前位置的下标,从pos位置开始匹配
int j=1;//j用于子串T中当前位置的下标值
int next[200];
get_next(T,next);
while(i<=S[0]&&j<=T[0])//当i小于S的长度并且j小于T的长度
{
if(j==0||S[i]==T[j])//两字母相等继续
{
++i;
++j;
}
else
{
j=next[j];//j退回合适的位置i值不变
}
}
if(j>T[0])
return i-T[0];
else
return 0;
}
KMP算法的改进
对next()函数进行了改良
设取代的数组为nextval
/*求模式串T的next函数修正值并存入数组nextval*/
void get_nextval(String T,int *nextval)
{
int i,k;
i=1;
k=0;
nextval[1]=0;
while(i<T[0])//T[0]表示T的长度
{
if(k==0||T[i]==T[k])//T[i]表示后缀的单个字符,T[k]表示前缀的单个字符
{
++i;
++k;
if(T[i]!=T[k])
nextval[i]=k;//如果当前字符与前缀字符不同则k为nextval在i位置的值
else
nextval[i]=nextval[k];//否则就把前缀字符的nextval值赋给nextval在i位置的值
}
else
k=nextval[k];//如果字符不相同就回溯
}
}
P3375 【模板】KMP字符串匹配
题目描述
给出两个字符串 s1 和 s2,若 s1 的区间 [l,r] 子串与 s2 完全相同,则称 s2 在 s1 中出现了,其出现位置为 l。
现在请你求出s2 在 s1 中所有出现的位置。
定义一个字符串 s 的 border 为s 的一个非 s 本身的子串 t,满足 t 既是 s 的前缀,又是 s 的后缀。
对于 s2,你还需要求出对于其每个前缀 s'的最长 border t'的长度。
输入格式
第一行为一个字符串,即为 s1。
第二行为一个字符串,即为 s2。
输出格式
首先输出若干行,每行一个整数,按从小到大的顺序输出 s2 在 s1 中出现的位置。
最后一行输出 ∣s2∣ 个整数,第 ii 个整数表示 s2 的长度为 i 的前缀的最长 border 长度。
输入输出样例
输入 #1
ABABABC
ABA
输出 #1
1
3
0 0 1
说明/提示
样例 1 解释
。
对于 s2 长度为 3 的前缀 ABA,字符串 A 既是其后缀也是其前缀,且是最长的,因此最长 border 长度为 1。
数据规模与约定
本题采用多测试点捆绑测试,共有 3 个子任务。
Subtask 1(30 points):∣s1∣≤15,∣s2∣≤5。
Subtask 2(40 points):∣s1∣≤10^4,∣s2∣≤10^2。
Subtask 3(30 points):无特殊约定。
对于全部的测试点,保证 1≤∣s1∣,∣s2∣≤10^6,s1,s2 中均只含大写英文字母。
#include<stdio.h>
#include<string.h>
char s1[1000010],s2[1000010];
int next[1000010];
int len1,len2;
void get_next()
{
int i,j;
i=0;
j=-1;
next[0]=-1;
while(i<len2)
{
if(j==-1||s2[i]==s2[j])
{
i++;
j++;
next[i]=j;
}
else
j=next[j];//若字符不相同,则k值回溯
}
}
void Index_KMP()
{
int i=0;
int j=0;
while(i<len1&&j<len2)//当i小于S的长度并且j小于T的长度
{
if(j==-1||s1[i]==s2[j])//两字母相等继续
{
i++;
j++;
}
else
{
j=next[j];//j退回合适的位置i值不变
}
if(j==len2)
{
printf("%d\n",i-len2+1);
j=next[j];
}
}
}
int main()
{
scanf("%s %s",s1,s2);
len1=strlen(s1);
len2=strlen(s2);
get_next();
Index_KMP();
for(int i=1;i<=len2;i++)
printf("%d ",next[i]);
}
我一直尝试用我上面写的代码,但是一直错误,然后我发现了错误就是在定义字符串的时候模板用的是string而我用的是char;并且可以去除Index_KMP()函数中的pos,可以直接在while循环中添加
if(j==len2)
{
printf("%d\n",i-len2+1);
j=next[j];
}
这样就可以从第一个找到的位置继续下一次寻找。