【字符串匹配】KMP算法之道

【字符串匹配】KMP算法之道

修订于2012-06-18,心急的读者可以着重看“有趣的字符串匹配提示”,这个例子看懂了,KMP也就差不多了。

都是字符串匹配

【字符串匹配】最为朴素的匹配和Rabin-Karp算法

【字符串匹配】有限自动机进行字符串匹配

【字符串匹配】KMP算法之道(即本篇)

《导论》是本好书啊,算法描述思路很清晰,力荐!

闲话

上午算法考试的时候,感觉OK,前一两星期幸好把图算法都吃透了一遍,复习的时候节省了时间:)。前一半考题不理解背书的都可以,有几题没记过,不靠谱地照着理解写下来。最后的吹水题让我想起了之前的比赛,有一题是曹老师给的实验题,刚好比赛上出现了,而且相似度极高。要是高考,曹老师可就红了:)。这也让我捡了便宜。

我们校区2012的招生计划出来了,结果我们校区悲催到只招30个法语本科生,也就是说2012的本科孩子只有30人。不知道法语的怎么看,但对这个校区的未来,我是看不到什么希望。“坑爹啊…”

有趣的字符串匹配“提示”

对于T=abacabab,P=abab,从T的第一个字符开始匹配:

  a b a a b a b
  a b a b      
第一次匹配 1 2 3 0      

可以看到,第四个字符已经匹配失败了。此时如果采用最朴素的算法,也就是重新从第二字符开始匹配(不画表了)。

KMP是这样做的:既然上面第四个字符已经匹配失败了,那么可以试着从已经匹配成功的前三个字符(即上面的“aba”)找到既是“aba”的后缀又是“aba”的前缀的字串,要求是此字串长度应该是所有满足条件中最大的,暂且记为π(“aba”)。很显然,π(“aba”)=1,因为

a b a 
      a b a

因此猜测从第三个字符开始匹配可能会成功(其实应该是从第四个字符开始匹配,因为π(“aba”)=1已经暗示第三个字符“a”匹配成功)(猜测,只是猜测而已)

  a b a a b a b
第一次匹配 a b a b      
第二次匹配     a
a b  

好吧,结果是不成功,因为出现了T中的第四个字符匹配失败的情况。不过可以发现,KMP算法没有像朴素算法那样,从T的第二个字符开始匹配,转而从T的第三个字符开始匹配,那为什么不从第二个字符开始匹配呢,因为从T的第三个字符开始匹配才有可能是成功的。如果你认为(或者说你有足够的证据证明)从第二字符开始匹配会成功,那么上面找“既前缀又后缀”的结果:

b a 
   a b a

即π(“aba”)=2,很明显不是。

好吧,这里不成功,用上面一样的策略,此时π(“a”)=0。因为

a  
   a

这逼着从T的第四个字符开始匹配,也是不成功:

  a b a a b a b
第一次匹配     a b      
第二次匹配      
b a b

于是匹配成功。你将看到KMP也是这么做的,关键是如何计算上面的说的“既前缀又后缀”的结果——其实就是帮助匹配的辅助表。

KMP算法之道

问题定义:

字符串匹配问题:T=“www.daoluan.net”,P=“daoluan”,问P是否在T中出现?答:是。

之前遇到的字符串匹配算法效率不是很看好,有限自动机之于最为朴素的穷举法有一定的提高,但是初始化过程仍不乐观,总体效率不高。奇葩的是,KMP算法初始化和匹配过程分别可以达到O(n)和O(m),实在是神奇。本篇文章目的就是吃透KMP。

纵观KMP,它无非就基于三个核心的结论,吃透这个三个结论,将KMP踩在脚下。

KMP和有限自动机字符串匹配一样,借助了一个辅助一维表,但KMP的辅助表计算时间在O(m)内。这个辅助表是关于匹配内容P的前缀表。

在提及这些结论之前,先允许我啰嗦一下:

对于字串P,(k)P表示长度k的P的前缀;P(k)表示长度为k的P的后缀。比如P=abcdef,(3)P=abc,P(3)=def。

π(q)表示P的前缀(q)P的最长后缀(k)P的长度(也就是k要取最大值)。比如:P=ababababca,π(8)即(8)P=abababab的最长后缀(k)P的长度,k最大为6,因为

abababab|ca 
ababab|abca

∴π(8)=6。

上述的辅助表中元素i即为π(i),比如:

匹配内容P=“daodaodaodaoluan”,那么关于P的前缀表即为: 
P d a o d a o d a o d a o l  u a n  
π 0 0 0 1 2 3 4 5 6 7 8 9 0 0 0 0

π(q)有了定义,π*(q)可以有。通常加“*”表示所有,在这里也是。π*(q)是一个集合,它的所有成员可以迭代求出:

π*(q)={k|(k)P是(q)P的后缀且k<q}

={π1(q),π2(q),π3(q),π4(q)….},其中πn(q)=π[πn-1(q)]。

比如:同样对P=ababababca,求π*(8), 有下图结果(来自算法导论):

《【字符串匹配】KMP算法之道》

所以π*(8)={0,2,4,6}。对于其他的q值也是这样计算。

假设已经得到了关于P的辅助表,

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 kmp()      m = strlen (P)      n = strlen (T)      π[m]      kmptab(π)    //预处理辅助表      q = -1      for i=[0,n)          while q>0 && P[q+1]!=T[i]              q = π[q]          if P[q+1]==T[i]              q = q+1          if q = m              //    找到啦          q = π[q]    //继续剩余T的寻找

其中π[m]为辅助表。如果P[q]==T[i]能连续成立m次,那么可以找到T中的P。所以如果有辅助表的存在,匹配过程还是很容易理解的。

亮出三把秒杀KMP的“宝剑”,他们主要是用来计算辅助表的:

  1. π*(q)={k|(k)P是(q)P的后缀且k<q}。
  2. 如果π(q)>0,那么π(q)-1∈π*(q-1)。
  3. 若定义Eq-1={k|k<q-1且(k)P是(q-1)P的后缀且P[k+1]=P[q]}。那么有: 
    π(q)=0                           Eq-1是空集 
            =1+max(k∈Eq-1)   Eq-1非空 

第一个结论可以通过“包含反包含”得出,用“显然”描述不为过吧:)。

第二个:

∵π(q)=t,那么t<q,所以π(q)-1=t-1<q-1;

∵π(q)=t,∴(t)P是(q)P的后缀,(去掉(t)P和(q)P的最后一个字符)那么(t-1)P同样也是(q-1)P的后缀;

根据结论一,得到t-1∈π*(q-1),π(q)-1∈π*(q-1)总能成立。

第三个:

可以证明它,但是个人觉得图例更有助于我们理解,下图对第三个结论的理解很有帮助的,里面涉及了上面提到的定义,比如π等,个人认为最好先懂了定义再来看,否则skip吧。 
《【字符串匹配】KMP算法之道》

上面的图中,前缀π(i-1)不断缩短,知道找到第i个字符和第【π(i-1)+1】个字符相等为止。

上面的基础就是为计算辅助表的。有了上面的结论:

?

1 2 3 4 5 6 7 8 9 10 11 kmptab(π)      m = strlen (P)      q = 0      π[0] = 0      for i=[1,m)          while q>0 && P[q]!=P[i]              q = π[q]    //    这里领悟到没有,跟第一个结论很有暧昧;同时,它跟第三个结论也有很大的渊源。            if P[q]=P[i]              q = q + 1    //    与上图中的做法一致          π[i] = q

实在是太短了。

KMP的复杂度

KMP的复杂度一时我也说不清楚,借助了算法导论和Matrix67的手笔才略有领悟。KMP用到了平摊分析。就上面的kmptab(π)函数,从q的值来说,q = π[q]操作只会使的q越来越小,但总能q>0,因为q = π[q]是根据辅助表来得到的,而辅助表中最小为0。P[q]=P[i]条件成立,能使得q+1,也就是说q只有在这里才增加。最坏的情况,q被增加m-1次。所以while循环内的操作最多被执行m-1次的。平摊一下就是O(1)了。所以加上for循环,kmptab的复杂度为O(m)。 
kmp主程序也是用这种平摊分析方法。

补充:KMP匹配过程中利用辅助表跳过了无效的检查,直接将检查过程跳转到将来可能成功匹配的字符上。

 

    原文作者:KMP算法
    原文地址: https://blog.csdn.net/zcabcd123/article/details/7693979
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞