早在1975年贝尔实验室的两位研究人员Alfred V. Aho 和Margaret J. Corasick就提出了以他们的名字命名的高效的匹配算法---AC算法。该算法几乎与KMP算法(http://blog.csdn.net/myjoying/article/details/7947119)同时问世。与KMP算法相同,AC算法时至今日仍然在模式匹配领域被广泛应用。
最近本人由于兴趣所致,在学习完KMP和BM单模式匹配算法之后,开始学习多模式匹配算法。看了一些博文和中文文章之后发现学习AC算法的最有效方式仍然是Aho和Corasick的论文《Efficient String Matching: An Aid to Bibliographic Search》。本文算是对这篇文章的一些总结。
AC算法思想
多模式匹配AC算法的核心仍然是寻找模式串内部规律,达到在每次失配时的高效跳转。这一点与单模式匹配KMP算法和BM算法是一致的。不同的是,AC算法寻找的是模式串之间的相同前缀关系。AC算法的核心是三张查找表:goto、failure和output,共包含四种具体的算法,分别是计算三张查找表的算法以及AC算法本身。
构造goto表
goto表本质上是一个有限状态机,这里称作模式匹配机(Pattern Matching Machine,PMM)。下面以论文中的例子来说明goto表的构造过程。对于模式串集合K{he, she, his, hers}
第一步:PMM初始状态为0,然后向PMM中加入第一个模式串K[0] = "he"。
第二步:继续向PMM中添加第二个模式串K[1] = "she",每次添加都是从状态0开始扫描。
第三步:从状态0开始继续添加第三个模式串K[2] = "his",这里值得注意的是遇到相同字符跳转时要重复利用以前已经生成的跳转。如这里的'h'在第一步中已经存在。
第四步:添加模式串K[3] = "hers"。至此,goto表已经构造完成。
构造failure表
failure表作用是在goto表中匹配失败后状态跳转的依据,这点与KMP中next表的作用相似。首先引入状态深度的概念,状态s的深度depth(s)定义为在goto表中从起始状态0到状态s的最短路径长度。如goto表中状态1和3的深度为1。
构造failure表利用到了递归的思想。
1、若depth(s) = 1,则f(s) = 0;
2、假设对于depth(r) < d的所有状态r,已近计算出了f(r);
3、对于深度为d的状态s:
(1) 若g(r,a) = fail,对于所有的a,则不动作;(注:a为字符,g为状态转移函数);
(2) 否则,对于a使得g(r,a) = s,则如下步骤:
a、使state = f(r)
b、重复步骤state = f(state),直到g(state, a) != fail。(注意对于任意的a,状态0的g(0,a) != fail)
c、使f(s) = g(state, a)。
根据以上算法,得到该例子的failure表为:
i 1 2 3 4 5 6 7 8 9
f(i) 0 0 0 1 2 0 3 0 3
构造output表
output表示输出,即代表到达某个状态后某个模式串匹配成功。该表的构造过程融合在goto表和failure表的构造过程中。
1、在构造goto表时,每个模式串结束的状态都加入到output表中,得到
i output(i)
2 {he}
5 {she}
7 {his}
9 {hers}
2、在构造failure表时,若f(s) = s',则将s和s‘对应的output集合求并集。如f(5) = 2,则得到最终的output表为:
i output(i)
2 {he}
5 {she,he}
7 {his}
9 {hers}
AC算法实现
根据上面已经构造好的goto、failure和output表就可以方便地应用AC算法了。
剩下的工作就是将目标串依次输入到PMM(即goto表),然后在发生失配的时候查找failure表实现跳转,在输出状态查找output表输出结果(包括匹配的串的集合和目标串中的位置)。以字符串"ushers"为例。状态转移如下:
u s h e r s
0 0 3 4 5 8 9
2
说明:在状态5发生失配,查找failure表,转到状态2继续比较。在状态5和状态9有输出。
算法改进
值得注意的是在AC算法的以上实现中,对于输入字符a的跳转次数是不确定的。因为有可能在输入a后发生失配,需要查找failure表重新跳转。能不能在PMM的基础上实现确定型的有限状态机,即对于任意字符a,都只用进行一次确定的状态跳转?答案是肯定的。在Aho和Corasick论文中给出了处理的方法:构造一个与KMP算法中相似的next表,实现确定性跳转。
next表的构造需要在goto表和failure表的基础上得到。next表如下所示:
输入字符 下一状态
state 0: h 1
s 3
* 0
state 1: e 2
i 6
h 1
s 3
* 0
state9,7,3: h 4
s 3
* 0
state5,2: r 8
h 1
s 3
* 0
state 6: s 7
h 1
* 0
state 4: e 5
i 6
h 1
s 3
* 0
state 8: s 9
h 1
* 0
注:*表示除了以上字符以外的其他字符。存储next数组所需要的内存空间比存储goto表更大。这样就可以用next表替代goto表和failure表,实现每次字符输入的唯一跳转。