算法介绍
字符串匹配是编程常遇到的一个问题,最朴素简单粗暴的匹配方法需要 O(n2) 的时间复杂度,这显然满足不了算法大神的要求。
KMP算法是一种改进的快速的字符串匹配算法,是由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,算法的时间复杂度只需要O(n)。
算法思想其实很简单,但是有时候会被人们解释的很复杂,因此我想根据我的经验来简单的总结一下KMP算法以便我自己更好地理解。
字符串匹配过程
我们的目标是
- 判断能否在主串S中找到一段和模式串P匹配的子串
现在简单看一下传统的字符串匹配的过程
由于绘图工具不给力暂时就提供这样的图,上面是主串S,下面是模式串P
分别从S和P的第一位开始匹配那么我们看到两个串迅速匹配了三个字符。到第四个字符不匹配的时候,需要将模式串P移位,先看朴素简单粗暴的移位方式
也就是移动了一位从S的第二位和P的第一位重新开始匹配,这也符合我们最直观的想法,不过结果很显然A和B不匹对..需要继续讲P串后移
那么对于KMP算法来说,直观的高效体现就是不匹对的时候移位会更多一些,对于以上情况移位如下
模式串P会自动的跳过B和C直接与S串的第四位A开始匹配,这其实是模式串P对自身的字符内容有着足够的了解,它知道当不匹配的时候利用之前匹配信息来决定如何进行下一步匹配。
KMP算法
KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。 —— [ 百度百科 ]
那么如何利用之前匹配失败的信息呢?
首先就是求字符串子串中既是前缀串同时又是后缀串中最长的那个。这句话很绕口,但是理解了就很简单,比如说对于字符串ABABA,A是该串的前缀串同时又是后缀串,但是AB只是前缀串不是后缀串,因为长度为2的后缀串蔚BA,那么同时BA就只是后缀串不是前缀串,那么对于字符串ABABA的最长的既是前缀串又是后缀串的子串为 ABA。
那为什么利用这个信息能达到快速匹配的效果呢?看一下如下图:
此时ABAB子串被匹配,但是第五个字符不匹配需要移动,那么该移动到哪里呢,ABAB中最长既是前缀串又是后缀串的子串为AB,那么我保证移动两位后字符串A和模式串P前两位还是能够匹配的。
若是只移动一位字符串A和模式串P还要能匹配,那么ABAB的最长既是前缀串又是后缀串的子串长度起码要3!
我们为了用上之前匹配过的信息,那么我们就希望移动模式串P之后利用之前的信息尽可能让之前的字符串更多的匹配,也就是减少不必要的匹配过程。(这句话能懂吗)
求字符串子串中既是前缀串同时又是后缀串中最长的那个就是求解next[]数组。
代码
以下是求解KMP中next数组的过程:
string par
int par_size=par.size();
vector<int> next(par_size,0);
next[0]=-1;
int k=-1;
for(int i=1;i<par_size;i++)
{
while(k>-1 && par[k+1]!=par[i])//不好理解?自己写个例子就明白了
k=next[k];
if(par[k+1] ==par[i])
k++;
next[i]=k;
}
求解完next数组我们就可以利用该数组的信息进行快速的字符串匹配,
int result=0;//匹配成功的次数
int m=org.size();
int q=-1;
for(int i=0;i<m;i++)
{
while(q>-1 && par[q+1] != org[i])
q= next[q];
if(par[q+1] == org[i])
q++;
if(q == par_size-1)//匹配成功
{
result++;
q=next[q];
}
}
其中q的最多增长m-1次,而造成q值降低的原因都在while循环中,因此用聚合思想分析while循环最多执行m-1次,因此整个算法用时复杂度还是O(m)。