[转]Aho-Corasick算法实践

Technorati 标签:

原文地址:http://www.cppblog.com/yefeng/archive/2009/12/06/102671.html

Aho-Corasick算法实践

摘要:

    Aho-Corasick算法可以在文本串中识别一组关键字,所需时间和文本长度以及所有关键字的总长度成正比。该算法使用了一种称为“trie”的特殊形式的状态装换图。Trie是一个树形结构的状态装换图,从一个结点到它的各个子结点的边上有不同的标号。Trie的叶子结点表示识别到的关键字。

    在这里,将着重讨论算法的实现。算法包含两个部分,一是经典的KMP算法,二是KMP的扩展算法Aho-Corasick算法。前者实现单关键字的模式匹配,后者实现多关键字的匹配。(参考龙书词法分析部分内容)

   【源代码:http://www.cppblog.com/Files/yefeng/ACKMP.rar(vc9.0下测试通过) 】

<!--[if !supportLists]-->一、<!--[endif]-->经典KMP算法
当初,初学KMP算法时,总是通过反复的举例去理解,没有一种好的表达方式,而龙书描述这个算法使用了trie树,也就是一个单链的状态转换图。如模式 <!--[if !vml]-->b0b1...bn-1<!--[endif]-->,trie树如下:


<!--[endif]-->

    对模式串定义失效函数 <!--[if !vml]-->f:x->y,x,y in S<!--[endif]-->,描述状态转移, <!--[if !vml]--><!--[endif]-->f(s)表示在状态s处,当下一个字符不是bs<!--[endif]-->时转向状态 <!--[if !vml]-->f(s)<!--[endif]-->继续匹配。因此设置 <!--[if !vml]-->f(s)<!--[endif]-->成为关键问题。

<!--[if !vml]--><!--[endif]-->    f(s)的存在其实主要是为了消除回溯。细节就不再多说了,这里只从原理上简单说明。

    设模式串为W,用文法描述,U、V表示W的一部分,w表示一个字符:

    W -> UwV,

    当U识别完成后,进入状态s,识别w时,发现到来的字符不等于w,则需要转向状态 <!--[if !vml]-->f(s)<!--[endif]-->,f(s)<!--[endif]-->到哪里去找呢?

    那就要看U是什么样子了。不管什么情况,只要U非空串,总可以表示成:

       U -> uXu,或 U -> u,或U-> uXx,(x != u)

    可以发现,前缀u是,如果后缀也是u,意味着主串中u已经被识别,如果还从模式串头匹配u无疑是多余的,所以 <!--[if !vml]-->f(s)<!--[endif]-->应该是识别前缀u后进入的状态。然后再匹配下一个字符。而满足条件的u可能会有多个,所以总是选择最长的那个。伪代码如下:

<!--[if !vml]-->   
<!--[endif]-->

    到此为止,应该算是可以结束KMP了,但实际情况下还可以对f函数进行优化。很多书本上描述的next数组就可以从f函数推导过来。

    其实也显然,设状态s接收字符w,当与输入字符c不等于c时,转向状态t,倘若t状态也只接收字符w,显然再次比较w与c是多余的,之后必然再次转向状态f(t)。在运行的时候,这些状态转换时没有意义的,可以在构造f之后,直接将f(s)设置为f(t)提高运行效率(不过此时f函数的意义已经不同了)。f优化如下:

<!--[if !vml]-->   
<!--[endif]-->

<!--[if !supportLists]-->二、<!--[endif]-->多关键字匹配与Aho-Corasick算法

Aho和Corasick对KMP算法进行了推广,使它可以在一个文本串识别一个关键字集合中的任何关键字。在这种情况下,trie是一棵真正的树,从其根结点开始就会出现分支。如果一个字符串是某个关键字的前缀,那么在trie中就又一个和该字符串对应的状态。如关键字集合{he,she,his,hers},trie树如下:

<!--[if !vml]-->   
<!--[endif]-->

    类似的,仍然构造类似KMP算法中那样的实效函数。对于上面的例子,失效函数如下:

s

0

1

2

3

4

5

6

7

8

9

f(s)

-1

0

0

0

1

2

0

3

0

3

<!--[if !supportLists]-->  1.<!--[endif]-->构造失效函数

类似KMP算法,同样采用实效实效函数推进的方法,假设当前状态为s,s的一个孩子结点的根结点根节点t状态,如果当前的失效函数已知为f(s),则显然地,f(t)必定是f(s)的孩子结点状态,所要做的就是在状态f(s)处寻找接受字符同s->t下一个状态,如果能找到,那就是f(t),否则说明到s处匹配串的前缀长度太长,需缩减,所以需要找到更短的后缀,于是就到f(s)处继续,如果仍然找不到,则转到f(f(s))处,形成状态的递归转移。构造中需要遍历之前结点的所有孩子,所以需采用广度优先遍历,伪代码如下:

<!--[if !vml]-->   
<!--[endif]-->

具体的构造如下:

<!--[if !vml]-->   
<!--[endif]-->

<!--[if !supportLists]-->  2.构造Trie树

    具体实现当然需要用到树形结构了,显然采用静态链表应该是最适合的,因为树构造完就不需要改变,而且当模式串比较多的时候可以减少内存碎片。

    每一个结点有5个域:接受字符,下一个兄弟结点,第一个孩子结点,失效函数值,结点状态。

但是有一种特殊情况,如上面的第二个图,在进行匹配时,hers是永远不会被匹配,因为he总是先于hers被匹配。这里就不考虑在内点状态结束,这个问题暂时无法解决。于是可以做个特殊处理,只使用4个域,因为此时匹配成功后状态就到了叶子结点,叶子结点不存在孩子域,这个域被浪费了,这里就可以借用一下,比如此域值为x,当x<0时,使用x xor 0x80000000表示识别到的模式串编号。

    另一个棘手的问题是结点个数问题,这个数组到底多大?如何确定?

    可以使用分值算法计算,先把模式串按字典顺序排好序,设想n个排好序的模式串第i位排在一起,相同字符的组成一组,如AiBi…Xi,再把每组下一个字符,也就是第i+1位排在一起,相同字符的组成一组,如A’iB’I…X’i,以此递归运算。伪代码如下:


<!--[endif]-->

<!--[if !supportLists]-->  3.缺点

    水平有限,程序缺点很多,很多问题都没有解决。

<!--[if !supportLists]-->    1.<!--[endif]-->如果存在两个模式串,一个是另一个的子串,那么后者将无法被匹配。

<!--[if !supportLists]-->    2.<!--[endif]-->无法处理动态决定大小写敏感性

<!--[if !supportLists]-->    3.<!--[endif]-->不够完整,只能向后匹配

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值