传统的字符串匹配
如上图所示,如果我们进行人工匹配的话,是不是2、3、4、5步骤都是多余的呢?那我们该如何消灭这种没必要的步骤呢?
KMP算法(KMP分别对应三个研究者的名字首字母)
再来看两个例子:
例子1:
S ababcaaa
T abac
匹配时,在第四位发生不匹配的情况,而前缀a(t1)和后缀a(t3)已经同S进行比较,且这俩相等,所以就没必要再让前缀a和S3再进行比较,而直接让T2和S4进行比较即可。(在这假定S和T的下标均从1开始)
例子2:
S ababcaaa
T ababb
匹配时,在第五位发生不匹配的情况,由于前缀ab(t12)和后缀ab(t34)相同,所以前缀(t12)无需在与t34对应的s34进行比较,故下一次匹配时,t3直接与s5进行比较即可。(在这假定S和T的下标均从1开始)
在朴素模式匹配算法中,每一次发生失配时都重新开始匹配,(i指针在S串上移动,j指针在T串上移动),所以i会回溯,但是经过分析可以发现,这种回溯是没有必要的。既然i值不回溯,那么变化的就只能是j值了。而经过分析,j值的变化与主串其实没什么关系,关键就取决于T串的结构中是否有重复的问题。然后又经过分析得到规律,j值取决于当前字符之前的串的前后缀的相似度。(这些相同的比过了,我就可以直接抄作业,一会儿就不用再比了~)。
故有以下函数定义:【参照书籍《大话数据结构》P138】,后面又经由分析得到,如果前后缀1个字符相等,则k值为2(在当前位置发生不匹配的时候,j变为2,因为第一个刚刚比过了是一样的),如果前后缀2个字符相等,则k值为3......依此类推。
next[j]表示,在j位置发生失配时,j的值变为next[j]。(在j位置发生失配时,回到next[j]重新开始匹配)
0,当j=1时 //第一个就发生失配的话,那回到开始之前,下一次重新开始
next[j] = Max{k|1<k<j,且"p1...pk-1"=="pj-k+1...pj-1"},当此集合不为空时 //找前后缀中有多少个 字符相等,1->2...
1,其他情况 //在第二个发生失配时,或者前后缀中没有相同的字符
//通过计算返回字串T的next数组
void get_next(String T,int* next)
{
int i,j;//i用来指示后缀,j用来指示前缀
i=1;
j=0;
next[1]=0;
while(i<T[0])//此处T[0]表示串的长度
{
if(j==0||T[i]==T[j])//T[i]表示后缀的单个字符,T[j]表示前缀的单个字符
{ //如果j==0,则next[i]=1,
++i; //譬如说:一开始,i==1,j==0时,计算next[2]时,next[2]==1
++j;
next[i]=j;
}
else
j=next[j];//若字符不相同,则j值回溯。
//譬如说:接下来比较i==2,j==1时,如果不相等的话,那j值要变回到0,为了一会儿利用if写上,next[3]=1。因为在j==3处发生不匹配且第一个字符和第二个字符不一样,那就不能抄作业,一会儿又要从1处开始匹配,所以借助next[1]的值为0回到if。
}
}
以两个例子阐述上述算法执行过程:
接下来我们再思考一个问题。
next[]很简便了吗?那我们来看一个新的字符串T="aaaaax"与字符串S="aaaabcde"进行匹配,如果按照我们之前所说的,当在T[5]处发生失配的话,按照之前"前缀和后缀中有1个相同的K==2,“2 3” ",那这里应该是4,所以下一次有T[4]和S[5]进行匹配。但是在现实中,由于T[5]的a和S[5]的b不相等,而T[1、2、3、4]的a又和T[5]的a相等,那么也就是说T[1、2、3、4]肯定和S[5]不匹配了,那就没有必要再接着比较了。也就是说,由于T串的第二、三、四、五位置的字符都与首位的“a”相等,那么可以用首位next[1]的值去取代与它相等的字符后续next[j]的值。所以可以在得到next的基础上再接着判断,如果和前面的某个字符相等,那就直接把该字符对应的next[j]赋给它就好了。但是在算法实现中,我们将这两个过程同时进行。
//求模式串T的next函数修正值并存入数组nextval
void get_nextval(String T,int* nextval)
{
int i,j; //i指示后缀,j指示前缀
i=1;
j=0;
nextval[1]=0;
while(i<T[0])
{
if(j==0||T[i]=T[j])
{
++i;
++j;
if(T[i]!=T[j])
nextval[i]=j;//跟之前的求next一样
else //如果相等的话,把j的next[j]也给i一份
nextval[i]=nextval[j];
}
else
j=nextval[j];
}
}
最后,搜索过程:
//返回子串T在主串S中第pos个字符后的位置,若不存在,则函数返回值为0
int Index_KMP(String S,String T,int pos)
{
int i=pos;//i用于主串S当前位置下标,如果pos不为1,则从pos位置开始匹配
int j=1;//j用于定义字串T中当前位置下标值
int nextval[255];//定义一个nextval数组
get_Nextval(T,nexval);
while(i<=S[0]&&j<=S[0])//限定范围
{
if(j==0||S[i]==T[j])
{
++i;
++j;
}
else
{
j=nextval[j];
}
}
if(j>T[0]) //说明匹配完了,
return i-T[0]; //S=ababcde T=aba 在j==3,i==3时成功匹配,然后++i,++j,那就是j=4,跳 出循环,此时j>T[0],然后要返回子串T在主串S中第pos个字符后的位置(包括第pos个字符),所以就是当前下标减去子串长度,就可以得到子串首字符对应的S串中的字符的pos值。
else
return 0;
}
最后一定要注意的一点是:KMP算法仅当S与T之间存在许多"部分匹配"的情况下才体现出它的优势,否则它与朴素的字符串匹配模式差异并不明显。