前言
KMP算法是我在学习《数据结构与算法》时遇到的一种解决字符串匹配问题的高效算法。面对这个问题,我最开始想到的是暴力算法,也就是一位一位的往下比对。
BF算法
/*Brute-Force(BF算法)穷举思路*/
int index_BF(SString S,SString T)
{
int i = 1;
int j = 1;
while(i<=S.length&&j<=T.length)
{
if(S.ch[i]==T.ch[j])
{
i++;
j++;
}
else
{
i=i-j+2;
j=1;
}
}
if(j>T.length)
return i-T.length;
else return 0;
}
然而在这过程中指针下标的回溯次数会非常多,所以匹配速度很慢,最坏的情况下空间复杂度为O(m*n)。
KMP算法
我们从这个例子来看,此时e和c出现不匹配,按照BF算法我们应该将子串后移一位重新进行比较,但其实是没有必要的,因为中间并没有相同的部分。我们观察到子串红色代表已匹配部分(下面称它为子子串),其中开头和结尾都有相同的ab,所以我们想能不能将子子串开头ab直接移动到其结尾ab处呢,这样就可以跳过中间不匹配的情况,同时保证主串的指针(下标)无需回溯,可以保持在e的位置继续往后比对。
所以KMP算法的核心思路就是,当子串与主串出现不匹配时,我们就移动子串(注意真正移动的是下标,这里说子串移动是为了方便理解),直接跳过中间不可能相同的部分,然后继续往下比对。现在的问题是,我们如何知道子串需要移动多少个位置呢?
开头和结尾相同的部分(也就是上图中的ab)我们称作子串的前后缀。
此时我们需要找到这个子子串的最大前后缀,然后就将前缀移动到后缀处。这时我们可能会有疑惑,为什么前缀可以直接移动到其后缀处,会不会出现移动到中间某个位置就匹配的情况?这是不可能的,因为如果出现了中间就匹配的情况,说明当前的前后缀不是最大前后缀。
由上述原理,假设子串与主串在下标为i处出现了不匹配,那么我们就去查找前面长度为i-1的子子串的最大前后缀(设长度为x),然后从子串的x+1位置与主串的当前位置起,开始往后继续比较。翻译为代码如下:
int KMPIndex(SString S,SString T)
{
int next[MAXSIZE],i=0,j=0;
GetNext(T,next);
while(i<S.length && j<T.length)
{
if(j==0 || S.ch[i]==T.ch[j])
{
i++;
j++;
}
else j=next[j]; //子串的下标直接跳转到next[j]处
}
if(j>T.length)
return (i-T.length);
else return -1;
}
其中,我们将以子串每一位为结尾的子子串的最大前后缀长度储存在一个名为next的数组当中。next数组,顾名思义它指示了下一步比对时下标所在的位置,而且next数组只由子串决定。
如何求得next数组呢,我们假设next[j] = t,表示由前j-1个字母组成的子子串的最大前后缀的长度为t-1。此时对于第j+1个字母来说,有两种情况:
- 第j个字母与第t个字母相同,那么最大前后缀的长度+1,next[j+1] = t+1
- 第j个字母与第t个字母不同,所以前后缀就无法再变长,但是可能组成新的最大前后缀。如下图所示 :此时两个红色部分已经匹配了,说明2=4,如果红色部分之中还有更小的前后缀如1和2,那么1=2,所以1=4,如果1后的字母与蓝色部分相同,则可以形成新的最大前后缀。具体操作为反复令t = next[t],直到t=0也就是第一个字母,或者找到下一个字母可以和蓝色部分匹配。
翻译为代码如下:
void GetNext(SString T,int next[])
{
next[1]=0; int j=1,t=0;
while(j < T.length)
{
if(t==0 || T.ch[j]==T.ch[t])
{
next[j+1]=t+1;
j++;t++;
}
else t=next[t];
}
}
总的来说KMP算法 原理不难理解,但是需要注意下标移动的位置,以及边界问题。上文中的图片来源于博主@哈顿之光《数据结构KMP算法配图详解》。