字符串模式匹配KMP算法解析
一、串的模式匹配
模式匹配问题:有两个字符串T和P,称串T为目标(Target),P为模式(Pattern),要在串T中查找是否有与串P相等的字串。
二、BF模式匹配
具体算法如下:

BF模式匹配简单来说,就是将P[0]与T串逐字符比较,每次比较都从P串首个字符一直比较至最后一个字符,若遇到不匹配的字符则失败,移动P[0]与串T下一个字符比较,重复上述过程。
三、KMP模式匹配算法
(一)基本思想
KMP算法是无回退的算法,这里的回退针对T串而言,具体指若用变量i扫描T串,在BF算法中,每次比对失败而进行下一次比对时,i都会向前回退一些字符。而在KMP算法中,i将从T[0]一直扫描至T[n-1],不会向前回退。下述情况中仍用变量i扫描T串,算法主要思想如下。

按照上述BF算法,若在第s趟扫描中失败,在P[j]位匹配失败,此时P[0]-P[j-1]与T[s]-T[s+j-1]完全一致,变量i指向T串s+j位置,第s+1趟扫描需要将P[0]右移一位,从T[s+1]开始比较。这里我们集中观察失配的P[j]之前的情况。此时,注意第s+1趟的P[0]-P[j-2]和第s趟的P[1]-P[j-1],现在这两段子串相对应,若P[0]-P[j-2]与P[1]-P[j-1]不完全逐位一致,则P[0]-P[j-2]与T[s+1]-T[s+j-1]无法匹配,因此第s+1趟必然匹配失败。按照BF算法再右移,进行第s+2趟扫描,此时若P[0]-P[j-3]与P[2]-P[j-1]不完全逐位一致,则第s+2趟也必然匹配失败。

设找到的前缀子串为P[0]-P[k],则当s趟匹配失败,此时我们可以令扫描串T的变量i不动,仍指向第s趟中失败的的s+j位置,令扫描P串的变量指向k+1位置,直接从T[i]与P[k+1]开始向后比对,而P[0]-P[k]由于与第s趟中的后缀子串完全相同,不用再次扫描比对。
因此,现在问题就转化为当某一趟P[j]匹配失败,如何确定k。这里引入next数组,next[j]的值代表当P[j]与T[i]匹配失败,下一次比较应该从P[next[j]]和T[i]开始进行比较。很明显,当j=0,即串P首位都未匹配成功,此时令next[j]=-1,即需要i++,串P仍然从j=0位置开始比对。这里先给出求得next数组之后,进行KMP匹配过程的算法。
代码中i和j分别为扫描串T和串P的变量。当扫描完串T都未找到完全匹配的子串,跳出while循环,此时j<P.num(串P长度),返回-1。当扫描到串P匹配子串,j==P.num跳出while循环,此时返回i-P.num,也即串P在串T中起始位置。
这里给出串P“abaabcac”的next数组,并给出其与一个串T匹配的过程。


(二)next数组的确定
next数组定义如下。next[j]的值代表当P[j]与T[i]匹配失败,下一次比较应该从P[next[j]]和T[i]开始进行比较。(也代表了P[0]-P[j-1]中找到最大的相匹配的前缀子串和后缀子串的长度)

当j==0,串P首位都未匹配成功,此时令next[j]=-1,即需要i++,串P仍然从j=0位置开始比对。当能从P[0]-P[j-1]中找到最大的相匹配的前缀子串和后缀子串,且前缀子串为P[0]-P[k]时,直接从k的下一位置k+1处开始继续比对。这里要求0<=k<j-1,因为若k==j-1,则前后缀子串均为P[0]-P[j-1],k+1仍为j,而此时我们已经知道P[j]匹配失败,因此k必然<j-1。对于其他情况,也即无法在P[0]-P[j-1]找到相匹配的前缀子串和后缀子串时,下一次令P[0]与T[i]比对,即next[j]=0。
那么当某一趟P[j]匹配失败,如何确定k呢。假设现在要求next[j],那么我们先观察next[j-1]。令a=next[j-1],即对于P[0]-P[j-2]之中,有长度为a的最大的相匹配的前缀子串和后缀子串,即下图蓝色区域,当P[j-1]匹配失败,下一次直接从P[a]和T[i]开始比较,如下图所示。

现在,若两块蓝色区域右侧的字符相同,即若P[a]==P[j-1],则在P[j]之前的串P[0]-P[j-1]之中,有长度为a+1的最大的相匹配的前缀子串和后缀子串,则next[j]=a+1=next[j-1]+1。若P[a]!=P[j-1],此时我们希望在两块蓝色区域内,分别在左侧蓝色区域的左侧和右侧蓝色区域的右侧找到完全匹配的子串,然后再次进行两个子串右侧下一符号的比对。

如上图所示,假设我们在长度为a的蓝色区域中找到两块最大的橙色区域相匹配,其长度为b,那么接下来我们只需要比较P[b]和P[j-1],若其相等,则在P[j]之前的串P[0]-P[j-1]之中,有长度为b+1的最大的相匹配的前缀子串和后缀子串,则next[j]=b+1,若其不等,则继续在橙色区域内搜索。以此类推。此时我们发现,由于两块橙色区域处于两块蓝色区域中,而两块蓝色区域完全相同,因此右侧橙色区域和左侧蓝色区域内部右侧长度为b的区域重合,因此找到最大的两个橙色区域,也就变成了在左侧长度为a的蓝色区域内,找到最大的相匹配的前缀子串和后缀子串,而这一最大长度也即next[a]。因此,在上一步我们发现P[a]!=P[j-1]之后,可以直接进行P[next[a]](在这里也即P[b])与P[j-1]的判断,若二者相等,则next[j]=next[a]+1(b+1),若不等,则继续进行P[next[next[a]]]与P[j-1]的判断,以此类推。
下面给出计算next数组的代码。
代码中首先令next[0]=-1,for循环进行P.num-1次,依次给next[1],next[2]...赋值。在计算next[j]时,在while循环中,当P[k]==P[j-1],也即找到P[0]-P[j-2]中相匹配的长度为k的前缀子串和后缀子串,且此时两个子串的下一位也相同(P[k]==P[j-1]),则跳出while循环,next[j]=k+1。若未找到相匹配的前缀子串和后缀子串,其满足前缀子串的下一位与P[j-1]相同,则最终k==-1,跳出while循环,令next[j]=0,下一次串P从头开始比较。