字符串的模式匹配是以下的一种常见问题:给定字符串$s$(主串)和$t$(模式串)$(|s|=n,|t|=m,m\leq n)$,求$t$在$s$中第一次出现的位置。如:
$s=”ababcabcacbab”$
$t=\ \ \ \ \ \ \ \ \ \ “abcac”$
则$t$首次出现在对齐处,即主串的第6位。很容易想到一种枚举的方法(Brute Force):
BF算法(Brute Force)
BF算法即从$i=1$开始,$i$至多到$n-m+1$,每次将模式串$t$的开始与$s$的第$i$位对齐并进行对照,即比较$s[i…i+m-1]$与$t[1…m]$是否相等。显然,这种算法的最差复杂度当$t$每次都在最后一位与$s$失配时出现,总比较次数$=m(n-m+1)=O(nm)$.
KMP算法
KMP算法通过计算模式串$t$的一些额外信息$(next$数组$)$来避免不必要的必定失配的情况,从而优化时间复杂度。它由Knuth、Morris和Pratt同时提出,故得名KMP算法。
现考虑BF算法在匹配失配时的情况。设$s$与$t$在$s[i],t[j]$处失配,即:
$s[i-j+1…i-1]=t[1..j-1]$
$s[i]\neq t[j]$
按照BF算法,失配后应将$t$右移一位,使得$s[i-j+2]与t[1]$对齐进行下一次匹配。此时,若对于模式串$t$有
$t[1…j-2] \neq t[2…j-1]\ \ \ \ \ \ \ \ \ \ \ \ \ \ (1)$,
那么就有
$t[1…j-2] \neq t[2…j-1] = s[i-j+2…i-1]$,
因此
$t[1…j-2] \neq s[i-j+2…i-1]$(可参照下图,相同颜色表示串相同)
图中$s$中橙色框一定与$t_2$(下一次匹配)中的绿框不相等(式$(1)$),因此下一次匹配一定会在模式串的$1…j-2$处失败$(*)$。
同理,当有$t[1…j-3] \neq t[3…j-1]$时,下下次匹配一定会失败。那么什么时候匹配有可能成功呢?我们需要找到从大到小第一个出现的值$k$,使得$t[1…k] \neq t[j-k…j-1]$而$t[1…k-1]=t[j-k+1…j-1]$,即子串$t[1…j-1]$的最长的相同的前后缀长度+1。
如:$t=”abaabcac”$
当$j=6$时,$t[1…5]$最长相同的前后缀为$”ab”$长为$2$,因此此时$k$值为$3$.
当我们能求出这个$k$的值后,我们就可以推断出下一次可能匹配成功的位置,见下图。
我们已经断定跳过的几次匹配一定会失配,因此我们可以直接将$t[1]$与$s[i-k+1]$对齐,而由相同前后缀的性质,标红框的部分完全相同,因此还可以跳过这一次匹配的$1…k-1$部分,直接将$s[i]$与$t[k]$进行比对。
通过上面的分析,我们可以总结出$k$的一些性质:
$(1)k$只与模式串$t$自己有关,而且是一个关于下标$j$的函数,即$k=next(j);$
$(2)0 \leq next(j) \leq j-1$,它表明$t[1…j]$有长为$next(j)-1$的相同前后缀$;$
$(3)$当不存在相同前后缀时,$next(j)=1;$
$(4)$定义$next(1)=0.$
next数组的求法
$next$数组可通过归纳求得。以下是具体分析:
$(1)next[1]=0;$
$(2)$假设$next[1],next[2],…,next[j]$均已知,现求出$next[j+1].$
设$next[j]=k,$这说明$t[1…j-1]$有长为$k-1$的相同前后缀。
①当$t[k]=t[j]$,那么$t[1…j]$便有长为$k$的相同前后缀。那么$t[1…j]$能否有比$k$更长的相同前后缀呢?答案是不能。如图:
红框圈起来的部分代表$next(j)$所表示的相同前后缀;紫色框表示$t[k]=t[j]$所带来的$t[1…j]$的长为$k$的前后缀。左边的紫框一定不等于绿框,否则$next(j)$便可取到$k+1,$因此两个黄框一定不相等,因为它们前$k$位一个是紫框,一个是绿框。因此当$t[k]=t[j],next(j+1)=k+1=next(j)+1$.
②当$t[k] \neq t[j]$,此时视为模式串$t$与自身匹配,而且$t[k]$和$t[j]$失配。由①可知,只能寻找比$k$更短的相同前后缀。
此时将下面的串不断向右移动,判断重叠部分(即绿框)是否相等,找出最长的相同前后缀。由KMP$(*)$部分可知,可以直接将下面的串的第$k’=next[k]$与上面串的第$j$位比较(否则一定会失配)。如果移动后$t[j]=t[k’],$那么便有$next(j+1)=k’+1=next(k)+1=next(next(j))+1;$如若不然,不断令$k^{(n)}=next(k^{(n-1)})$,直到:
$(a)$$t[j]=t[k^{(n)}]$,则$next(j+1)=k^{(n)}+1.$
$(b)$直至迭代到$k^{(n)}=1$时,$t[j] \neq t[k^{(n)}]$,即上下串仅一位重叠仍不相等(下图),说明没有公共前后缀,此时令$next(j+1)=1.$
代码实现
-求$next$数组
1 | void GetNext(char *t,int *next) |
代码中$i$表示当前要求的$next$下标,$j$表示下面的串的第$j$位与上面的串(上一部分图解)尝试进行匹配。
$j == 0$表示当迭代到最后仍然没有公共前后缀的情况,此时$next[++i]=1.$
$t[i]=t[j]$说明当前匹配成功,记录下$next[++i]=j+1$,否则迭代$j$为$next[j]。$
-KMP
1 | int KMP(char *s,char *t) |
与求$next$数组很相似,当$j=0$时,说明$s[i]$与$t[1]$不相等,此时模式串右移一位将$j$置为$1$,同时$i++$使得模式串从头开始与主串下一位匹配。
当$s[i]==t[j]$时,说明当前位匹配成功,$i,j$均自增。否则$j$迭代为$next[j]$.
当$j>m$时,说明模式串匹配完成,匹配成功位置为$(i-1)-m+1=i-m$(由于$i$在匹配成功时自增了$1$,需要减掉)
KMP算法的复杂度为$O(n+m)=O(n).$