KMP算法是一种效率很高的有关串匹配问题的算法
给定两个字符串S和T,在主串S中查找子串T的过程称为串匹配
举个例子:我们想在“aaabbbccc”
这样一个字符串中查询“bbb”
所在的位置,那么这个过程就叫做串匹配,且在这个例子中“aaabbbccc”
就是主串,而“bbb”
就是子串
传统的串匹配一般采用的是BF算法,直接上图(图中i,j 分别对应主串和子串的循环变量)
这种算法效率低下的原因就在于它进行了多次重复比较
就拿图片的情况举例,第1躺除了最后一个字母没有匹配上,其它的字母都已经匹配成功了,(以下设主串为S,子串为T),且我们已知S[1]=T[1]!=T[0](即b=b!=a),那么第二趟的比较就毫无意义,因为第二趟最开始的比较就是T[0]与S[1]比较,而这两个字母如上文所说一定是不相等的(也就是说i回溯到1,j回溯到0是完全没有必要的)
其余重复的步骤同理,而KMP的出现就成功解决了匹配过程中步骤冗余的问题
KMP算法主要分为两大部分,它们分别是:
不了解len值和仍然发懵的同学先不要着急,讲解还没正式开始呢,我们目前只需要知道串匹配是什么、传统BF算法的缺点以及我们接下来的目标就是掌握KMP的这两个部分即可
在之前的BF算法中,j 变量(即子串的循环变量)在比较失败后都是直接回溯的整个子串的开头,然后进行新一轮的比较
但是在每一轮的匹配过程中,除了知道最后不匹配的节点外,在它之前的若干个字母相等的匹配信息我们也是知道的
还是拿这张图的第一趟举例,我们除了知道主串S[4]!=子串T[4]外,我们还知道S[0]——S[3] = T[0]——T[3]这样一组信息(即a=a,b=b,c=c,a=a)
而GetNext()存在的意义就是充分利用这部分信息,并通过这组信息帮助子串的变量回溯到指定的位置来减少比较量,而非直接回到最开始从头比较
(听到这还是懵?千万别急,这才刚刚开始,博主自己在了解到这个阶段时也是一头雾水,且听我继续道来)
法则又可以理解为几何中的公理,本函数的核心逻辑都是围绕这两大法则展开的(当然,这两大法则是我出于理清思路的目的所创建的,书上并没有明确写出)
连等顾名思义就是连续相等的意思,在平时写代码时,我们经常会遇到a=b=c
,这就是连等。
而本题中的连等就是指连续相等的几个字符或字符串,比如’a’=‘a’='a’或者“ab”=“ab”=“ab”,而这也是本题的核心
还是拿书上的例子的第一趟说明一下这项法则:(在下文中,主串和子串的称呼还是为S和T,且出现的“!=”就是不等于的意思)
(第一趟)在经过比较发现S[4]!=T[4](即a!=c)后,我们可以知道T[4]前的四个字母即abab
是和S[4]前的四个字母逐个对应匹配的(否则不会到S[4]才发现匹配失败)。那么我们就找到了第一个等式,即图中的 part1 = part2;
而在子串自身中寻找(不看主串),我们找到了第二个等式part2 = part3
因此我们就找到了连等式part1 = part2 = part3(下文中part1,part2,part3对应的位置与这里相同,不再赘述)
进而得出结论part1 = part3(等式的传递性)
有了这个结论 ,我们在下一次比较前,就可以先将part1与part3对齐,然后开始比较part1与part3的下一个位置
第二次比较如下图(大家先不要看第二次的比较过程,只需要看第二次比较前主串和子串的相对位置,即蓝框部分)
对齐后只需比较对齐部分的下一个位置,而对齐部分本身由于已经相等,所以无需比较
在这个找连等的过程中,第一个等式是不重要的,或者说你能随便找到很多个对应相等的part1和part2,拿下图来说,除了ca
和ca
,还有bca
和bca
甚至是abca
和abca
,这是一定能找到的(因为如上文所说在匹配失败的位置前,所有字母都是对应相等的)
但是第二个等式却无法保证一定能找的到,因为如下图所示,part1和part2的确相等,但是子串的开头并没有ca,所以根本没有part3,进而导致连等式无法成立
所以我们可以得出结论:连等式的核心是子串本身,而非子串与主串之间的联系;换言之,当我们在子串中找到了对应相等的part2和part3,part1和part2的等式自然可以随之建立(或者说part1和part2的等式本身就存在)
所以在这个我自己作图的例子中,我们在子串中能找到的就是part2=part3=‘a’,进而将原先就存在的part1=part2=‘a’的等式拿出来形成连等式(见下图)
然后将part1与part3对齐,并从对齐部分的下一个位置开始新一轮的比较(见下图)
可能会有同学提出这样几个问题:为什么part3的第一个位置一定是子串开头的位置?为什么part2的最后一个位置一定是在子串匹配失败位置的前一个位置?在子串的随意位置找到两个对应相等的字符串不可以吗?
先回答第三个问题:当然是不可以的。
我们还是用例子说话:
可以看到,在第一次比较过程中,我们(按照随意位置)的确找到了对应的part1,part2,part3形成了连等。然后在第二次比较前我们将part1与part3对齐如图,但因此出现了两个错误(红框1和红框2),这两个错误正好与上面的前两个问题相对应,我逐一说一下
第一个错误如上图红框1所示,在对齐后,红框1位置的两个字母是匹配失败的
第二个错误如上图红框2所示,在对齐后,红框2位置的两个字母也是匹配失败的
而上述的两个错误我们都是可以从第一次匹配所获得的信息中提前得知的,所以这样的随意地选取字符串会导致时间和比较次数的浪费,也违背了KMP算法的初衷
通过分析找连等——再对齐——最后比较的三个步骤,我们可以发现part3的长度越大,剩余需要比较的字符就越少
比如子串为abcab
,假设我们已经将子串前面的abca
(即part3部分)与主串part1部分对齐,我们就只需要再比较一次(即将子串最后位置的‘b’与主串对应位置比较)就能知道这一次的比较是否成功;而part3的长度越小,我们在新一轮的比较中需要比较的次数就越多
所以得出结论,我们要在各个part符合要求的前提下,一定要确保它的长度是最长的,这样就能减少新一轮比较的次数
首先第一步:如图所示,第一次我们分别从长度等于6-1=5(6是从子串开头到匹配失败的前一个位置的长度,而之所以减一是因为长度为六必然相等,对齐和不对齐是一样的)的字符串找起,得出两种情况,分别对应图中的情况1和情况2,那么根据法则二,我们肯定是要选择第一种情况的
其次第二步:接着根据法则一,我们通过找连等——对齐的方法确定了第二次开始比较的位置如上图
如果比较失败,就按照上面的两步循环往复直至比较成功或比较到最后仍未成功为止
part2与part3是可以交叉的,只不过之前的例子中没有提到,我在此补一张图
先看GetNext()的完整代码
void GetNext(char T[],int next[])
{
int i,j,len;
next[0]=-1;
for(j=1;T[j]!='\0';j++) //依次求next[j]
{
for(len=j-1;len>=1;len--) //part2、part3的最大长度为j-1
{
for(i=0;i<len;i++)
if(T[i]!=T[j-len+i]) break; //依次比较T[0]~T[len-1]与T[j-len]~T[j-1]
if(i==len)
{
next[j]=len;break;
}
}
if(len<1) next[j]=0;
}
}
我们依次解析一下:
next[0]=-1;
这行代码表示了子串第一个位置(即T[0])就和主串当前循环变量 i 所处位置(S[i])不相等的特殊情况,这个情况会在KMP主函数中特别处理,在此就不详述了
for(i=0;i<len;i++)
if(T[i]!=T[j-len+i]) break; //依次比较T[0]~T[len-1]与T[j-len]~T[j-1]
if(i==len)
{
next[j]=len;break;
}
len值就是我们得到的对应part的字符串长度,或者说就是对齐部分的长度
同样,len值也是下一次比较时子串的循环变量 j 所要回溯的位置,这里我们拿之前的例子说明
在这个例子中,part部分对应的字符串长度为2,即len值为2
也就是说下一次比较开始前,子串的循环变量 j 要回溯到 2,即从位置2开始比较
因为子串是从0开始的,这也就意味着位置2前的对齐部分(即位置1和位置0)正好是两个字母,也就是不需要比较的两个位置。
if(len<1) next[j]=0;
最后这行代码表示如果在子串中没有找到对应相等的part2与part3,就给对应的next赋值0(即让子串的循环变量回到最开始)
int KMP(char S[],char T[]) //求T在S中的序号
{
int i=0,j=0;
int next[80]; //假定子串最长为80个字符
GetNext(T,next);
while(S[i]!='\0'&&T[j]!='\0')
{
if(S[i]==T[j])
{
i++;j++;
}
else
{
j=next[j];
if(j==-1) {
i++;j++; }
}
}
if(T[j]=='\0') return(i-strlen(T));
//返回本次匹配开始的位置,这里返回的位置是相对于数组来说的(即从0开始)
//如果是按照实际位置返回,这里的返回值要加1
else return 0;
}
j=next[j];
这一行代码对应着上文所说的子串循环变量的回溯
if(j==-1) {
i++;j++; }
这一行则对应着2.6.1所说的特殊情况,即子串第一个位置(即T[0])就和主串当前循环变量所处位置(S[i])不相等的特殊情况
在这种情况下,因为此时的 j 的值为-1,所以 j++ 就是让 j 变为0从而在下一轮时从子串的开始位置进行比较,当然 i 也要随之加1
大家可以跳转到我的这篇文章——理解了KMP算法却不知道代码怎么写?看看这道题你会有收获!(jmu-ds-实现KMP),这里给出了一道关于串匹配的问题以及其答案,我使用的就是KMP算法
以上就是博主对于KMP算法的全部讲解了
博主已经尽自己所能把这个知识掰开揉碎了给大家讲解,并且尽可能地使用浅显易懂的语句并列举了大量的例子帮助理解,所以大家都看到这了觉得还不错,能不能给我一个免费的赞呢?
最后非常感谢大家如此有耐心地看到这里,也相信大家也有了一定的收获,希望大家在看完我的文章后能对于KMP算法有着更深层次的理解