KMP算法,以为一个简简单单的算法,看了我一天时间竟然没有看懂...果然图样图撕破了,三位大师提出的算法岂是我等屌丝能够迅速的理解的?不过话说看了这次算法才知自己的智力有多么的吃紧,还是要努力学习呀~智力不行就要加把劲了。(接下来字符串匹配算法均参考算法导论)
字符串匹配,算法的模型不用提出大家都知道,仅仅是在文本T字符串中精确匹配模式串P,简简单单,轻轻松松的就知道这么一个模型,自然而然的能够想到一个最笨最实用的算法,朴素算法,朴素算法就是逐个比对,然后在文本串中下移一位在进行逐个比对,算法复杂度O((n-m+1)m),
不过这种简单的方法的时间复杂度不是我们能够容忍的,提高一点有一个Rabin-Karp的算法,此算法的最坏时间复杂度没有提高,但是平均情况比较好,而且它这个思想可以借鉴,就是把匹配当做算术运算,后来取模比对,针对剩余的少部分精确比对~不过这不是重点,
接下来的就是重量级的算法,KMP算法,它在预处理时间和匹配时间均达到了线性时间~O(m)的预处理时间,O(n)的匹配时间,非常完美的理论,不是么?网上很多资料都是关于next函数的,这里我参考算法导论里面的KMP算法,其思想虽然一致,但是不同于next函数的原理,其原理如下:(匹配过程)
i 1 2 3 4 5 6
a b a b a b a a b a b
a b a b a c b
a b a b a c b
j 1 2 3 4 5 6 7
j 1 2 3 4 5 6
这里比对过程是i和j的变化,如果i和j对应的地方相同,则同时+1,继续走下去,当出现T[i]和P[j]出现不同时,如上图中的P[6]!=T[6],此时KMP考虑的是i不进行回溯,而是考虑挪动j,使得j变化为一个更小的值,小于j,这样的变化效果是P整体向后移,但是此时不能够导致T[6]前面的i的匹配性质,及挪动后T[6]前面的元素和P对齐之后还是要相同的,所以这里j=5改为j=3,挪动后效果如红色标注的P所示,这是T[6]=P[4],两个串的表示i和j可以挪动下去;
i 1 2 3 4 5 6 7 8 9 10 11
a b a b a b a a b a b
a b a b a c b
a b a b a c b
3
a b a b a c b
1
a b a b a c b
0
j 1 2 3 4 5 6 7
但是接下来挪动的时候又产生了问题,T[8]!=P[6],这时候如同上面的方式,挪动j=5的位置,首先挪动至最近的保持T[8]前保持性质的部分,其实这里保持性质及P[5]的后缀的最长前缀...说到概念可能就不知道了,还是理解保持性质吧,挪动至红色后发现T[8]!=P[4],所以这里还要挪动j=3的位置,挪动成j=1,还是不能满足,所以还需要挪动,这时j=0了,及这时候T[8]和P[1]比较了,这里刚好一样,所以i可以继续向后挪动了;
以上的移动均是参考了一个数字,该数字是保证某个位置的时候P和T能够保持性质,但是这个性质仅仅和P有关,及上面说的P的后缀的最长前缀,因为以上匹配过程用到了这个数据,所以接下来就是这个数据的计算的算法了,其实这个算法才是让我纠结的地方,纠结了好久,这里发现一个可以很容易理解的地方,分享给大家;
上面算法的线性时间需要用到均摊分析的知识~可以仔细思考一下,线性的时间,
这里图解一下计算后缀的最长前缀吧,这里这个记为PI[j],这里PI[j]可以根据PI[1],PI[2]...PI[j-1]计算得出,
如图所示,PI[j-1]为标记的凸包的长度,这里如果P[lengh[PI[j-1]]+1]=P[j],这里PI[j]及为PI[j-1]长度加1,如果P[lengh[PI[j-1]]+1]!=P[j],则这个需要在更加前面找,这里及递归的找,画图如下,
及黑色线标注的位置是否与P[j]相同选择是否继续递归,黑色标注的位置的选择为PI[ PI[j-1] ] + 1,PI[j-1]的位置为红色的那段,因为不可能为红色,所以要缩小一下,这里示意为绿色的圈,绿色的圈挪动到前边的红色部位,所以绿色的圈在红色的部位找到前缀为黑色的部分,所以PI[ PI[j-1] ]是这样得出的,这样递归下去就能够得出PI[j]的值了,这样就可以根据计算得出的PI[j]值运用匹配算法了,
不知道上面的解释是否能够解释的清楚,一个递归的过程,分析算法复杂度也是一个均摊分析的方法,同样是线性的,
模式串的预处理的伪代码如下:
COMPUTE-PREFIX-FUNCTION(P) m=length(P) PI[1]=0 //初始化 k=0 //初始化 for q=2 to m while k>0 and P[k+1]!=P[q] //结束条件至0或者相等 k=PI[k] //递归的过程 if P[k+1]=P[q] // 增加的过程 k=k+1 PI[q]=k //赋值为k,下一个计算已k为起点向下递归 return PI
KMP匹配过程执行的伪代码如下:
KMP-MATCH(T,P) n = length[T] m = length[P] PI=COMPUTE-PREFIX-FUNCTION(P) j=0 for i=1 to n while j>0 and P[j+1]!=T[i] //如果不等,挪动P,根据计算好的PI j=PI[j] if P[j+1]=T[i] //如果相等,则向后继续匹配 j=j+1 if j=m //匹配m个后则成功 printf(匹配成功) j = PI[j]
以上是伪代码描述,上面说描述的参考算法导论的KMP思想,网上还有一种next方式的,有空研读一下比较差别;
除了上述算法还有一些优秀的字符串匹配算法,基于自动机的,BM算法等等,
如果想要了解一下有限自动机方法可以继续看下去,这种方法可KMP算法可以看做等价,
有限自动机字符串匹配算法简单介绍如下:
给定模式P[1...m],其对应的字符串匹配自动机定义如下:
1、状态集Q为{0,1,...,m},初始状态q0为0状态,状态m是唯一的接受状态。
2、对于任意的状态q和字符a,变迁函数δ由如下等式定义:δ(q,a)=σ(Pq a)
这里的解释一下σ(x)的定义,其为相应P的后缀函数。σ(x)为x的后缀 P的最长前缀的长度:σ(x)=max{k:Pk是x的后缀}
简单的举例如下:
i 1 2 3 4 5 6 7 8 9 10 11
T[i] a b a b a b a c a b a
状态 1 2 3 4 5 4 5 6 7 2 3
上述是匹配的过程,下面是转移表:
a b c P
0 1 0 0 a
1 1 2 0 b
2 3 0 0 a
3 1 4 0 b
4 5 0 0 a
5 1 4 6 c
6 7 0 0 a
7 1 2 0
自动机的过程与KMP可以转换,只不过自动机考虑了T[i]不匹配过程中的具体字母表的转移,然后KMP只是不匹配的情况下先进行挪动,挪动后发现不匹配之后再进行挪动?直至挪动为0的时候,抑或是一直不匹配i++,此时忽略j;自动机只是具体考虑了此时如果T[i]如果不等于P[j],并且此时T[i]的具体取值已经指定,在此值指定的情况下应该怎样的转移~
a b a b a c a
0 0 1 2 3 0 1
上面是KMP计算出的模式函数,首先a值是T[i]和P[1]如果相等,则j++,向后移动,但是如果不匹配,此时如同自动机转移表第一行,为b or c,此时就应该i,j同时++了,此时KMP如果发现T[i]和P[1]不相等,则上面算法i++,j取值仍为0,等价于i和j同时++了;
这时考虑b,如果a匹配了,P[2]和T[i]匹配,此时j++=2了,这时候移动两位了,自动机表现匹配两个字母,所以为状态2,但是如果不相等呢?此时考虑了b,必定T[i]和P[1]已经匹配了,KMP中不匹配则考虑移动P,根据值j重新改为0,及向右移动两位,然后自动机此时读入不等的可能为a或者c,如果为a,则KMP移动两位之后匹配上了一位,j++=1,如果为c,移动之后仍然不能匹配,所以i和j同时仍然移动,KMP表现i++,j重新赋值为0;
...
这次考虑一个中间的值,比如c的位置,如果知道T[i]值为多少,则可以知道具体的移动多少,然而KMP中是根据P计算的模式值,所以这里不知道T[i]的值,它有可能是最坏的值,所以这里取得的是最坏的值;所以这里计算自动机转移函数可以考虑KMP中已经计算好的结果,这样提升时间复杂度,
...
所以自动机匹配和KMP算法有一定的等价性,上面的描述比较凌乱,希望能够对你有所启发,