半年前接触到了这个小算法,当时想了足有一整天,看了很多高手的博客,结果一直没理解,直到个人的任督二脉被打通,才发现原来这么简单。
过了半年,突然又想起这个算法,忘了个七七八八,又迷茫了一会,还是写出来免得再忘掉。
KMP算法用处是从一条源字符串中寻找与目标字符串匹配的位置。我这里设源字符串为src,目标字符串为dst。
一般的算法一般是对src每一个位置i对dst头部进行匹配,之后分别用src[i + n]与 dst[n]进行匹配,如果完全匹配就可以获得位置,其时间复杂度是src.length * dst.length;
而KMP算法则要效果好上不少,下面就按我的思路讲解一下
src 0 1 2 3 4 ……. n-2 n-1 n
dst 0 1 2 3 4 …… m-2 m-1 m
假设 dst[ 0 ] ~ dst[ t ] 当前与src[ s ] ~ src[ s + t ]相匹配,dst[ t + 1 ]与src[ s + t + 1 ] 不匹配时,这时候按照一般的方法是检索src[ s + 1 ],并从dst[ 0 ]重新开始。
再做一个假设,假设dst[ 0 ] ~ dst [ x ] (x < t) 与 dst [ t - x ] ~ dst[ t ]逐一匹配(原谅我制作的丑)
dst 0 1 2 3 4 …… x-2 x-1 x
dst t-x t-x+1 t-x+2 t-x+3 t-x+4 …… t-2 t-1 t
src s+t-x s+t - x+1 t-x+2 t-x+3 t-x+4 …… t-2 t-1 t
看到这个,大家想起什么。我们在dst[ t + 1 ]与src[ s + t + 1 ] 不匹配时,完全可以直接跳过中间的步骤,继续检索dst[ x + 1]与
src[ s + t + 1 ] 是否匹配。(因为我们完全可以证明dst[ 0 ] ~ dst [ x ]与src[ s + t - x ] ~ src[ s + t ]相匹配)
我们假设dst[ 0 ] ~ dst [ x ] (x < t) 与 dst [ t - x ] ~ dst[ t ]是最长的匹配字符,即不存在dst[ 0 ] ~ dst [ x + 1 ] (x < t) 与 dst [ t - x - 1 ] ~ dst[ t ]匹配,则当前匹配为最优匹配。我们假设当前匹配皆为最优匹配,至于获得最优匹配的方法后面再讲。
那么我们证明这样可以获得不会错失一些可能存在的解。是否有可能存在一个解y其开始位置位于0~x之间且被我们错过呢。这是不可能的,假如存在,
即dst[ 0 ] ~ dst [ t - y ] 与 src[s + y] ~ src[s + t ]相匹配。则我们同样可以证明dst[ 0 ]~dst[t - y] 与 dst[ y ] ~ dst[ t ]是相匹配的,
由于t - y > t - x (y < x);我们可以发现这与前面的最优匹配不符。因此可以证明最优匹配的匹配开始位置之前不可能存在新的匹配开始位置。
继续下一步,我们如何才能获取最优的跳转位置呢。
我们需要维护一个与dst等长的int型数组jumpPosTable;
jumpPosTable中保存当此次匹配失败,dstPos将要跳转的位置。
遍历dst数组,维护两个位置,分别命名为scanPos和jumpPos,scanPos为扫描位置,jumpPos位置为跳转位置。
具体细节见下面的代码(初学java,练练手)
//KMP类
final class KMP
{
//获取字符串src与dst的匹配度表
public static int[] getMateString(String dst, String src)
{
//获取dst的跳转表
int dstLen = dst.length();
int srcLen = src.length();
int []jumpTable = KMP.getJumpTable(dst);
int []mateTable = new int[src.length()];
char []dstChTable = dst.toCharArray();
char []srcChTable = src.toCharArray();
//进行匹配操作
for(int dstPos = 0, srcPos = 0; srcPos < srcLen; )
{
//如果dstPos小于0
if(dstPos < 0)
{
dstPos++;
srcPos++;
}
//如果dstPos溢出
else if(dstPos >= dstLen)
{
dstPos = jumpTable[dstLen - 1] + 1;
if(dstChTable[jumpTable[dstLen - 1]] != dstChTable[dstLen - 1])
dstPos--;
}
//如果srcPos位置与dstPos位置字符相同
else if(dstChTable[dstPos] == srcChTable[srcPos])
mateTable[srcPos++] = ++dstPos;
//如果不同
else
dstPos = jumpTable[dstPos];
}
return mateTable;
}
//获取字符串str的跳转表
private static int[] getJumpTable(String str)
{
char []chTable = str.toCharArray();
int chLen = str.length();
int []jumpTable = new int[chLen]; //跳转表
//生成跳转表
jumpTable[0] = -1;
for(int scanPos = 1, jumpPos = 0; scanPos < chLen; scanPos++, jumpPos++)
{
jumpTable[scanPos] = jumpPos;
//如果扫描位置和跳转位置的字符不同
if(chTable[scanPos] != chTable[jumpPos])
jumpPos = -1;
}
return jumpTable;
}
}