最近准备接下来的校招面试,一直没学过数据结构的我,也不得不准备准备这方面的知识。对于字符串匹配这个课题,现在有很多的方法,然KMP确实其中经典的一种方法,这两天找了很多的相关资料,却发现理解很简单,而实现却有点小困难,为了永久解决这个问题,苦心钻研了下,现把我的理解记录如下,供有相同疑惑的童鞋一起学习,探讨。
1.KMP算法的引入
问题:有一个目标文本串T,和一个模式串P,现在要查找P在T中的位置,怎么查找呢?
首先我们还是看下最基本的暴力求解(相信这个方法大家都了解了):
假设现在目标文本串T匹配到 i 位置,模式串P匹配到 j 位置;
1.如果当前字符匹配成功(即T[i]== P[j]),则i++,j++,继续匹配下一个字符;
2.如果当前字符不匹配(即T[i]!=P[j] ),则令i=i - (j - 1),j = 0。即每次失配时,模式串都要从第一位开始重新匹配,i需要回溯,j被置为0;
C++代码如下:
//暴力求解字符串匹配B-F算法
//szTarget表示目标字符串,szPattern表示模式字符串(即最终需要查找到的字符串)
void match(const string& szTarget,const string& szPattern)
{
int iTagetLength=szTarget.size();
int iPatternLength=szPattern.size();
for (int i=0;i<iTagetLength-iPatternLength+1;i++)
{
int j=0;
while (j<iPatternLength)
{
if(szTarget[i+j]==szPattern[j])
j++;
else
break;
}
if (j==iPatternLength)
cout<<i<<endl;
}
}
举个例子,理解下暴力匹配的过程如下所示:
①.
首先,目标文本串"BBC_ABCDAB_ABCDABCDABDE"(其中_表示空格)的第一个字符与模式串"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以模式串后移一位。
②.
因为B与A不匹配,模式串再往后移。
③.
就这样,直到字符串有一个字符,与模式串的第一个字符相同为止。
④.
接着比较目标文本串和模式串的下一个字符,还是相同。
⑤.
直到目标文本串有一个字符,与模式串对应的字符不相同为止。
⑥.
这时,最自然的反应是,将模式串整个后移一位,再从头逐个比较。这时,我们发现,这样比较是多余的,因为在第4步时,我们已经知道T[5] = P[1] = B,而P[0] = A,即P[1] != P[0],故T[5]必定不等于P[0],所以回溯过去必然会导致失配。
KMP算法就是去除这些多余步骤,提高效率的。
2.KMP算法
对于上述所阐述的问题,我们该怎么解决呢?
上述第⑤步时,我们发现T[10]为空格字符,P[6]为字符D,所以不匹配,而我们也看到P[0]==P[4]==T[8],P[1]==P[5]==T[9],我们接下来的一步是不是直接可以拿P[2]与T[10]比较,我们是不是可以建立一个数组:存放目标文本串第i位与模式串第j位字符失配时,下一次与第i位比较的模式串中的字符位置。这就是大部分文献中的Next数组(本文中会利用vector)。
那么我们该怎么求next呢?首先我们上代码:
//计算Next数组
//Next[i]表示模式串的第i位与目标串不匹配时(前面i-1位都匹配),下一步i的值
void GetNext(const string& szPattern,vector<int>& viNext)
{
int iLen = szPattern.size();
viNext.resize(iLen);
viNext[0] = -1;
int iIndex = 0;
while (iIndex < iLen-1)
{
if (-1 == viNext[iIndex] || szPattern[iIndex] == szPattern[viNext[iIndex]] )
viNext[iIndex+1] = viNext[iIndex] + 1;
else
viNext [iIndex+1] = 0;
++iIndex;
}
}
或者如下:
//计算Next数组
//Next[i]表示模式串的第i位与目标串不匹配时(前面i-1位都匹配),下一步i的值
void GetNext(const string& szPattern,vector<int>& viNext)
{
int iPatternLen = szPattern.size();
viNext.resize(iPatternLen);
viNext[0] = -1;
int iIndex = 0;
int iTemp = -1;
while(iIndex<iPatternLen-1)
{
if (-1==iTemp || szPattern[iTemp]==szPattern[iIndex])
{
++iIndex;
++iTemp;
viNext[iIndex] = iTemp;
}
else
iTemp = viNext[iTemp];
}
}
现详细叙述:
a.对于模式串“ABCDABD”,若第0位’A’与目标文本串中的第j位字符不匹配,那么j+1,i=0;此时我们令Next[0]=-1;(表示j+1,i=0;)
b.若第1位’B’与目标文本串中的第j位字符不匹配,此时表明第0位与第j-1位已经匹配,那么我们就要将整个模式串后移一位,用模式串中的第0位与目标字符串中第j位比较,即Next[1]=0;
.若第2位’C’与目标文本串中的第j位字符不匹配,此时表明第0、1位分别与第j-2、j-1位已经匹配,由于前两位组成的字符串没有前缀子串和后缀子串相等,下一步直接用第0位与目标字符串中第j位比较,即Next[2]=0;
d. 若第3位’D’与目标文本串中的第j位字符不匹配,此时表明第0、1、2位分别与第j-3、j-2、j-1位已经匹配,由于前三位组成的字符串中没有前缀子串和后缀子串相等,下一步直接用第0位与目标字符串中第j位比较,Next[3]=0;
e. 若第4位’A’与目标文本串中的第j位字符不匹配,此时表明第0、1、2、3位分别与第j-4、j-3、j-2、j-1位已经匹配,由于前四位组成的字符串中没有前缀子串和后缀子串相等,下一步直接用第0位与目标字符串中第j位比较,Next[4]=0;
f. 若第5位’B’与目标文本串中的第j位字符不匹配,此时表明第0、1、2、3 、4位分别与第j-5、j-4、j-3、j-2、j-1位已经匹配,由于前五位组成的字符串中有前缀子串”A”(第0位)和后缀子串”A”(第4位)相等(P[m]==P[i-1],此时m=0,i=5),故第j-1位和模式串中的第0位就不需比较了,肯定相等,下一步只需将模式串中的第1位与目标文本串中的第j位比较就可以了,即Next[5]=1;
g. 若第6位’D’与目标文本串中的第j位字符不匹配,此时表明第0、1、2、3 、4、5位分别与第j-6、j-5、j-4、j-3、j-2、j-1位已经匹配,由于前六位组成的字符串中有前缀子串”AB”(第0、1位)和后缀子串”AB”(第4、5位)相等(P[m+1]==P[i],m、i与上一步相同),故第j-2、j-1位和模式串中的第0、1位就不需比较了,肯定相等,下一步只需将模式串中的第2位与目标文本串中的第j位比较就可以了,即Next[6]=2;
故综上所求的Next[]={-1,0,0,0,0,1,2}。
同理可得模式串“ababcabaa”的Next[]={-1,0,0,1,2,0,1,2,3};
接下来我们根据Next数组,我们求解文章开始的问题,首先上C++程序:
void KMPMatcher(const string& szTarget,const string& szPattern,vector<int>& viNext)
{
int iTargetLen = szTarget.size();
int iPatternLen = szPattern.size();
//首先从第一位开始匹配
int iPatternIndex = 0;
int iTargetIndex = 0;
while(iTargetIndex<iTargetLen)
{
//若目标文本串第iTargetIndex位和模式串第iPatternIndex位
//匹配,则比较下一位,条件A
if(szTarget[iTargetIndex] == szPattern[iPatternIndex])
{
++iTargetIndex;
++iPatternIndex;
}
//若目标文本串第iTargetIndex位和模式串第iPatternIndex位
//不匹配时,且当前Next数组值为-1时,将用文本串
//第iTargetIndex+1位和模式串第0位匹配,条件B
else if(-1 == viNext[iPatternIndex])
{
++iTargetIndex;
iPatternIndex = 0;
}
//若目标文本串第iTargetIndex位和模式串第iPatternIndex位
//不匹配时,且当前Next数组值不为-1时,将用文本串
//第iTargetIndex位和模式串第viNext[iPatternIndex]位匹配
else
iPatternIndex = viNext[iPatternIndex];
if (iPatternIndex == iPatternLen)
{
cout<<iTargetIndex-iPatternLen<<endl;
iPatternIndex = 0;
}
}
}
从上述程序中,可以发现条件A和条件B可以合并为:
if (-1==iPatternIndex || szTarget[iTargetIndex] == szPattern[iPatternIndex])
3.KMP改进算法
然而,上述求解模式串“ABABCABAA”的Next[]={-1,0,0,1,2,0,1,2,3}时,若第j=2位’A’与目标文本串第i位不匹配时,下一步将会用第j=0位’A’与目标文本串第i位比较,但此时明显是匹配的,那我们是不是也可以更改Next的值:令Next[2]= Next[0]=-1 表示呢?显然是可以的,
按照此更改后我们可得新的Next[]={-1,0-1,0,2,-1,0,-1,3}.
那我们怎样更改程序呢?如下://计算Next数组
//Next[i]表示模式串的第i位与目标串不匹配时(前面i-1位都匹配),下一步i的值
void GetNext(const string& szPattern,vector<int>& viNext)
{
int iPatternLen = szPattern.size();
viNext.resize(iPatternLen);
viNext[0] = -1;
int iIndex = 0;
int iTemp = -1;
while(iIndex<iPatternLen-1)
{
if (-1==iTemp || szPattern[iTemp]==szPattern[iIndex])
{
++iIndex;
++iTemp;
if(szPattern[iTemp] != szPattern[iIndex])
viNext[iIndex] = iTemp;
else
viNext[iIndex] = viNext [iTemp];
}
else
iTemp = viNext[iTemp];
}
}
自此,KMP算法得以理解,不知各位网友可理解了?如有不理解,欢迎相互交流交流。
4.参考文献:
1.《数据结构》第二版,严蔚敏,吴伟明著;
2.《算法导论》第三版中文版第32章;
3.http://blog.youkuaiyun.com/v_july_v/article/details/7041827
4.http://kb.cnblogs.com/page/176818/