回头看看KMP算法,感觉还是没有有限自动机那么坑!
朴素的字符串匹配算法想必大家懂得(串下标全部从0计):
模式串P与文本串T匹配,假设扫描到串T的i+1处匹配,串P的j+1处失配,那么下次串T又将回溯到i-j+1处试图与P重新匹配。如下图的前缀蛮力匹配算法,T[5]与P[5]失配后,下次又从i=1处开始和串P试图匹配。
朴素的模式串匹配算法中,在失配之前模式串P与文本串T的一段匹配信息在下一次匹配中直接被忽略,这无疑是一种信息资源上的浪费。如上图,在文本串T[0...4]与模式串P匹配后,我们便知道从T[1]开始的串是无法和P匹配的。因为文本串T[1]和模式串的P[1]是匹配的,而P[1]和P[0]是不匹配的!此时,朴素算法盲目的再去从T[1]开始匹配 ,增加了时间复杂度。
于是,我们考虑如何更有效的使用匹配信息。
一个正确的思路是,在让串P滑动能正确匹配到当前T所扫描到的位置的基础上,保证一个最小滑动确保不会丢失匹配……
如上图,在T[5]和P[5]失配后,显然模式串向后滑动2、4……都可能匹配成功,因为就我们目前扫描串T所到的位置来说它们暂时是匹配的。当然,我们要选取最小的滑动距离,不然可能丢失匹配。本例中,若模式串向后滑动4与T[4]去匹配,那么由于我们并不知道T[5]之后的信息而有可能丢失可以从T[2]匹配成功的子串T[2...](当然本例中是不可能了)。
现在,我们考虑如何去获取这种最小滑动距离。如上图,显然最小滑动后T[2...4]和P[0...2]会正确匹配(前提条件),而由于之前的匹配信息T[2...4]和P[2...4]是匹配的!现在一下子可以看出,滑动时需要的信息是失配之前的模式串P的子串的可以和其前缀匹配的最大后缀长度!如上例,P[0...4]子串的最大匹配长度是3,前缀aba和后缀aba!
接下来就是如何计算模式串P的每个子串P[0...j]的最大匹配后缀长度的问题了,我们把结果存入一个Next数组中。如上例模式串P预处理过后的Next数组将是这样:
P: a b a b a c b Next: 0 0 1 2 3 0 0
首先,初始化 Next[0]=0,这个很容易理解。
然后,假设当前计算模式串P的子串P[0...j]的最大后缀匹配长度Next[j]的问题。
已知Next[j-1],令k=Next[j-1]。那么k将表示当前试图继续匹配的前一次成功匹配的前缀串的后一个字符,当然也表示前一次成功匹配的长度。
如上例,假设当前要计算P[4],令k=Next[3]=2,即前一次成功匹配的前缀为P[0...1](与P[2...3]),本次扫描是试图继续匹配P[0...2]与P[2...4]!显然上一次成功匹配长度为k=2!
现在需要分两种情况讨论:
1).如果P[j]==P[k],那么在前一次成功匹配的基础上本次匹配尝试成功!显然成功匹配度加1,即Next[j]=k+1!前缀指针与后缀指针向后移动,继续尝试匹配!
2).如果P[j]!=P[k],那么基于前一次成功匹配的基础再继续匹配必然是失败的!现在一个这正确的思路就是,在前一次成功匹配的前缀串中去寻找可匹配的子串!此时修正k指向上一次成功匹配的前缀串中,最长的与后缀匹配的前缀的后一个字符,即令k=Next[k-1]! 然后重复前面的过程即可!
例如下面这个情况:
P: a b a b a c b Next: 0 0 1 2 3 0 0
当计算子串P[0...4]最大前缀后缀匹配长度时,j=4,k=Next[3]=2。因为当前扫描位置P[4]与前一次匹配的前缀串的下一个字符P[2]匹配成功,那么子串P[0...4]的最大前后缀匹配长度就是前一次子串P[0...3]的匹配长度加1,也即k+1=3!
当计算子串P[0...5]的最大前后缀匹配长度是,j=5,k=Next[4]=3。因为当前扫描位置P[5]与前一次成功匹配前缀串的下一个字符P[3]匹配失败,那么在前一次匹配基础上试图继续匹配将是不可能的!现在考虑这样的处理方式,因为前一次成功匹配后缀串P[2...4]和P[0...2]匹配,那么由于P[2...4]中的后缀将和P[0...2]中的后缀匹配,可以考虑用P[2...4]中的后缀在P[0...2]子串前后缀成功匹配的基础上在P[0...2]前缀后继续试图匹配!本例中,即P[4]和P[0]成功匹配,现在试图用P[5]从P[1]开始继续匹配!(虽然本例并不成功)此时修正k(k=3)值为k=Next[k-1]=1,然后重复前面的过程即可。
注意,此时需要处理一种特殊情况。当去试图匹配第一个字符(k=0)但失配时,也就意味着当前匹配不能建立在任何一个匹配基础上,并且不能和首字符匹配,那么直接将Next[j]置0。如本例中的Next[5]。
预处理的Next数组计算代码如下:
while(j<m) //j指向当前需要处理的最大前后缀匹配长度的位置,m为模式串P的长度
{ //k指向与当前j前的后缀子串匹配的前缀子串的下一个字符,也是当前试图匹配的前一次匹配的最大长度
if(P[j]==P[k]||k==0) //后缀子串的下一个字符与前缀子串的下一个字符继续匹配
//或者匹配到串首,无前一次匹配基础
{
if(P[j]!=P[k]) Next.push_back(0); //当前字符与第一个字符不匹配,后缀前缀匹配长度置0
else Next.push_back(++k); //当前后缀串后的字符与试图匹配的前缀串后的字符匹配,匹配长度为前缀串长度+1,即k+1
j++; //向下扫描
}
else k=Next[k-1]; //当前后缀串后的字符与前缀串后的字符失配,在前缀串中寻找前一个可匹配的前缀子串
//修正前一次匹配长度和试图继续匹配位置
}
字符串匹配过程的代码如下:
while(1)
{
while(i<n&&j<m&&T[i]==P[j]) {i++;j++;} //当前字符匹配,向后扫描
if(j==m) cout<<i-m<<endl; //模式串匹配成功,输出开始匹配子串起始的位置
if(i==n) break; //串扫描结束
if(T[i]!=P[j]) //当前位置失配
{
if(j==0) i++; //文本串与模式串首字符失配,与目标串当前所试图匹配子串不能匹配,扫描下一个子串
else j=Next[j-1]; //模式串向后滑动,试图匹配下一个可匹配的子串
}
}