`
javatgo
  • 浏览: 1123869 次
  • 性别: Icon_minigender_2
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

字符串匹配算法(二)穷举与自动机

阅读更多
穷举法又叫暴力法。大多数程序员眼里,它是幼稚的,但大师们不这么认为。

Rob Pike, 最伟大的C 语言大师之一, 在《Notes on C Programming》中阐述了一个原则:花哨的算法比简单算法更容易出bug、更难实现,尽量使用简单的算法配合简单的数据结构。而Ken Thompson——Unix 最初版本的设计者和实现者,禅宗偈语般地对Pike 的这一原则作了强调:拿不准就穷举(When in doubt , use brute force)。而对于装13爱好者来说,更是自豪的称其使用的是BF算法。

穷举法用在字符串匹配上,简单的描述就是,检查文本从0到n-m的每一个位置,看看从这个位置开始是否与模式匹配。这种方法还是有一些优点的,如:不需要预处理过程,需要的额外空间为常数,每一趟比较时可以以任意顺序进行。

尽管它的时间复杂度为O(mn),例如在文本"aaaaaaaaaaaaaaaaaaaaaaaaaaa"中寻找"aaaaab"时,就完全体现出来了。但是算法的期望值却是2n,这表明该算法在实际应用中效率不低。

C代码如下:
  1. voidBF(char*x,intm,char*y,intn){
  2. inti,j;
  3. /*Searching*/
  4. for(j=0;j<=n-m;++j){
  5. for(i=0;i<m&&x[i]==y[i+j];++i);
  6. if(i>=m)
  7. OUTPUT(j);
  8. }
  9. }
如果我们注意到C库函数是汇编优化过的,并通常能提供比C代码更高的性能的话,我们可以用memcmp来完成每一趟比较过程,从而达到更好的性能:
  1. #defineEOS'\0'
  2. voidBF(char*x,intm,char*y,intn){
  3. char*yb;
  4. /*Searching*/
  5. for(yb=y;*y!=EOS;++y)
  6. if(memcmp(x,y,m)==0)
  7. OUTPUT(y-yb);
  8. }
自动机的方法其实和穷举法有点相似,都是用最简单直白的方式来做事情。区别在于穷举法是在计算,而自动机则是查表。尽管自动机的构造过程有一点点难解,要涉及到DFA的理论,但是自动机的比较过程那绝对是简单到无语。

简单说来,根据模式串,画好了一张大的表格,表格m+1行σ列,这里σ表示字母表的大小。表格每一行表示一种状态,状态数比模式长度多1。一开始的状态是0,也就是处在表格的第0行,这一行的每个元素指示了当遇到某字符时就跳转到另一个状态。每当跳转到最终状态时,表示找到了一个匹配。

语言表述起来还是比较啰嗦,看代码就知道了:
  1. #defineASIZE256
  2. intpreAut(constchar*x,intm,int*aut){
  3. inti,state,target,old;
  4. for(state=0,i=0;i<m;++i){
  5. target=i+1;
  6. old=aut[state*ASIZE+x[i]];
  7. aut[state*ASIZE+x[i]]=target;
  8. memcpy(aut+target*ASIZE,aut+old*ASIZE,ASIZE*sizeof(int));
  9. state=target;
  10. }
  11. returnstate;
  12. }
  13. voidAUT(constchar*x,intm,constchar*y,intn){
  14. intj,state;
  15. /*Preprocessing*/
  16. int*aut=(int*)calloc((m+1)*ASIZE,sizeof(int));
  17. intTerminal=preAut(x,m,aut);
  18. /*Searching*/
  19. for(state=0,j=0;j<n;++j){
  20. state=aut[state*ASIZE+y[j]];
  21. if(state==Terminal)
  22. OUTPUT(j-m+1);
  23. }
  24. }
注:原文的代码使用一个有向图的数据结构,我遵循大师的指引,改用了更简单一点的数组

从代码上我们很容易看出,自动机的构造需要时间是O(mσ),空间也是O(mσ)(严格来说这份代码使用了O((m+1)σ)),但是一旦构造完毕,接下来匹配的时间则是O(n)。

匹配的过程前面已经说了,太简单了没什么好说的,这里就解释一下构造过程吧!

我们构造的目标是对应模式长度,构造出同样多的状态,用0表示初始状态,然后第一个字符用状态1表示,第二个用状态2表示,依次类推,直到最后一个字符,用m表示,也是最终状态。

一开始,数组全都置0,,这个时候的自动机遇到任何字符都转到初始状态。然后给它看模式的第一个字符,假设这是'a'吧,告诉它,状态0遇到'a'应该到一个新的状态——状态1,所以把第0行的第'a'列修改为1。而这个时候状态1还是空白的,怎么办呢?

这时候状态0就想呀,在我被告知遇到'a'要去状态1之前,我原本遇到'a'都要去状态0的,也就是修改之前第'a'列所指的那个状态,称为old状态吧;而现在我遇到'a'却要去一个新的状态,既然以前old状态能处理遇到'a'之后的事情,那么我让新的状态像old状态一样就好了。于是状态0把old状态拷贝到状态1。

现在轮到状态1了,给它看第二个字符,它也如法炮制,指向了状态2,又把old状态拷贝给了状态2。

于是,状态机就在这种代代传承的过程中构造完毕了。

虽然理论上自动机是最完美的匹配方式,但是由于预处理的消耗过大,实践中,主要还是用于正则表达式。

结语:穷举法与自动机各自走了两个极端,因此都没能达到综合性能的最佳,本文之后介绍的算法,可以看成是在穷举和自动机两者之间取舍权衡的结果。


分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics