KMP算法
把KMP看成改进的模式匹配算法就够了。
赘述(可不看):
对于串的定位(也叫串的模式匹配),于人而言,把两个串进行逐一比较是最方便和直观的方法,这种方法不依赖其它的串操作;但如果把这种方法用在计算机上,那么就会浪费很多的时间,显然这是不够明智的,所以才有了kmp,记住单纯就KMP模式匹配而言,它并不是很高大上的东西,只是结合了串的其他操作的串的定位算法而已,难点是其实际应用。
基本思想:
首先:这里会涉及串的前后缀的应用,前缀指的是一个串不包含最后一个字符,字符串的所有头部子串,如abcd的前缀是a,ab,abc;后缀指的是除第一个字符外,从最后一个字符开始的所有尾部子串,如abcd的后缀是d,cd,bcd。而这种匹配方法,大体上是一个铺路的过程,即后缀为前缀铺路,例如:如果abacfaba的后缀匹配了2个字符,就说明在跳转的时候就可以忽略这两个字符而进行第3个字符的比较,而且这样还实现了准确的跳转到下一个串开始的位置,如果不这样的话,跳转会出错或失去其该有的优点。
其次:记住一个公式
移动位数 = 已匹配的字符数 - 对应的部分匹配值
已匹配的字符数指匹配失败时最大匹配数, 对应的部分匹配值指的是指在这已经匹配的串中,其前缀=后缀的最大值(也就是next数组中的值),由于方便,next[]中值可以后移一位,还可以在原来基础上加一,这里定义看自己喜好。
例子:比如现在有主串ababcabaacbab 需要定位的子串为abcac
对于abcac而言,由其前后缀可得出next数组
于是生成以下表格
对于表格里的内容的应用:
第一次匹配:
a b a b c a b c a c b a b
a b c
由结果知,第一次匹配在第3个字符处失配,此时已经匹配的字符长度为2,接着查刚才得出的next表可知,此时对应的部分匹配值为0,由此可知2-0=2 得出下一次直接从第3个字符开始,跳2个字符,由1->3。(当然字符串是从0开始的,这里从1是为了方便理解)
第二次匹配:
a b a b c a b c a c b a b
a b c a c
可知第二次匹配时,在第5个字符处失配(指需要定位的子串的第5个字符,毕竟next是由子串得到的)通过查表可知对应的部分匹配值为1,已经匹配的字符长度为4,所以4-1=3将子串后移动3个位置,从3到6。对了这里的 对应的部分匹配值是看失配字符的前一个字符的值,即4a的对应的部分匹配值为0, 5c的对应的部分匹配值才为1。其实很好理解,当前位置失配表示当前的字符并不能为后续的操作提供有利的条件,所以选上一个成功匹配的字符。
第3次匹配:
a b a b c a b c a c b a b
a b c a c
第3次成功匹配,所以子串定位为6。而kmp总体复杂度为o(m+n)。.
对于next的求法,记住next[j]=next[j-1]+1 这个式子 -->如果有字符匹配时 j 保存的最长前后缀与j-1次保存的有关
Next[j]已知 求next[j+1]两步:
1 若串中字符tj =ti ,则next[i+1]=j+1 ,j为当前最长相等前后缀长度(不是全局)
2若tj != ti 将 ti-j+1........ti作为主串,t1......tj作为子串,类比于失配让j=next[j] 继续比较,若满足1则求得next[j+1]。 如abcdcd 串中每次前缀都是从a开始的,所以只要每次不断失配后j能跳到a,则表明回跳是对的,后缀一样。
//获取next数组
//记住当不匹配的时候 我们要找的是上一次完成匹配的情况下的最大匹配值
//目的: 这样就不用把i回溯来匹配了,所以next的存储方式就出来了
void getnext(char *ptr){
int i,n,k; //个人习惯用k,k可改回j 方便理解
n=strlen(ptr);
i=1,k=0; //这里已经实行了右移 再加1 如不加1 i=0, k=-1,next[0]=-1
next[1]=0;//
while(i<n)
{
if(k==0 || ptr[i]==ptr[k])// k=0用于指针i的移动 结合k=next[k]看
{
i++;
k++;
next[i]=k;
}
else k=next[k];
}
}
为了直观一点,我写一下关于串的next的模拟:
如求一个串 ababaa的next
首先默认 next[1]=0 ,next[2]=1 因为移位后是-1 0 再加1 就是0 1 -1是移位后添加的,0是第一个字符没有前后缀
之后当 i=3 时 k=next[i-1] =next[2]=1 此时比较 str[k]与 str[i-1] 即比较 str[1] 与str[2] 明显不相等——> 由k=next[k]
知k=0 ,所以不用比较 ,next[3]=k+1=1
当 i=4 时 k=next[i-1]=next[3]=1 由此比较str[1] 与str[3] 可知 str[1] =a str[3]=a str[1]=str[3] , 此时next[4]=k+1 =2
当 i=5 时 k=next[i-1]=next[4]=2 由此比较str[2] 与str[4] 可知 str[2] =b str[4]=b str[2]=str[4] , 此时next[5]=2+1 =3
当 i=6 时 k=next[i-1]=next[5]=3 由此比较str[3] 与str[5] 可知 str[3] =a str[5]=a str[3]=str[5] , 此时next[6]=3+1 =4
所以结果为011234 这是移位并且加1 后的结果
为什么要移位再加1? 因为move =已匹配的字符数 - 对应部分的匹配值
move= ( j-1 ) -next[j-1]
而移位的位置标识j为 j = j-move ,这是字符串的移动公式结合kmp的 j 看 。
j-move=j - ((j-1) -next[ j ] )=next[j]+1 注意这个next[j]是移位后的next 原来是next[ j-1] 。
综上,再将next 数组加1 就可以得到next[ j ] 然后得到 j= next[ j ] (KMP中的)
应该看得懂吧
结合了《算法导论》和《数据结构》的思想,而在数据结构中。数据结构中会把str[0]用于存放串长度。
int kmp(char a[],char b[])//匹配ab两串,a为父串,b为带定位串
{
int i=-1,j=-1;
int len1=strlen(a);
int len2=strlen(b);
getnext(b);
while(i<len1&&j<len2)
{
if(j==-1||a[i]==b[j])
++i,++j; //继续比较后续字符
else
j=next[j]; //待定位串向右移动
}
if(j==len2)
return i-j;// 匹配成功
else
return -1;
}
ABCDABTABCDABC
ABCDABC
把上面的样例放入程序动手模拟一下就可以明白大概了
这里next数组的作用就显现出来了。最后返回的是i-j,也就是说,是从i位置前面的第j位开始的,也就是上面说的,next数组也可以说是开始比较的位数。也就是说,在父串的i位比的时候已经是在比子串的第j位了。(看不懂也没关系)
一个完整的代码://懒癌犯了 细节以后再补
#include <iostream>
#include <cstring>
#include <string>
using namespace std;
int next[50]={-1};
void getnext(char *ptr){
int i,n,k; //个人习惯用k,k可改回j 方便理解
n=strlen(ptr);
i=1,k=0; //这里已经实行了右移 再加1 如不加1 i=0, k=-1,next[0]=-1
next[1]=0;//
while(i<n)
{
if(k==0 || ptr[i]==ptr[k])// k=0用于指针i的移动 结合k=next[k]看
{
i++;
k++;
next[i]=k;
}
else k=next[k];
}
}
int kmp(char a[],char b[])//匹配ab两串,a为父串,b为带定位串
{
int i=-1,j=-1;
int len1=strlen(a);
int len2=strlen(b);
getnext(b);
while(i<len1&&j<len2)
{
if(j==-1||a[i]==b[j])
++i,++j; //继续比较后续字符
else
j=next[j]; //待定位串向右移动
}
if(j==len2)
return i-j;// 匹配成功
else
return -1;
}
int main()
{
char a[20],b[20];
cin>>a;
cin>>b;
int num= kmp(a,b);
if(num)cout<<"NOT FIND!"<<endl;
else cout<<num<<endl;
return 0;
}
借鉴:https://www.cnblogs.com/yjiyjige/p/3263858.html
不过这东西,真的在题中出现时用的是求next等思想。