AC自动机算法,全称是Aho-Corasick算法。Trie树跟AC自动机之间的关系,就像单串匹配中朴素的串匹配算法,跟KMP算法之间的关系一样,只不过前者针对的是多模式串而已。
AC自动机实际上就是Trie树之上,加了类似KMP的next数组,只不过此处多的next数组是构建在树上罢了。
AC自动机的构建,包含两个操作:
(1):将多个模式串构建成Trie树;
(2):在Trie树上构建失败指针(相当于KMP中的失效函数next数组)
构建好Trie数之后,如何在它之上构建失败指针?
①:Trie树中的每个节点都有一个失败指针,他的作用和构建过程,跟KMP算法中的next数组极其相似。
假设沿Trie树走到p节点(紫色节点),那p的失败指针就是从root走到紫色节点形成的字符串abc,跟所有模式串前缀匹配的最长可匹配后缀子串,就是箭头指的bc模式串。
最长可匹配后缀子串,字符串abc的后缀子串有两个bc,c我们拿它们于其他模式串匹配,如果某个后缀子串可以匹配某个模式串的前缀,就把那个后缀子串叫做可匹配后缀子串。
②:从可匹配后缀子串中,找出最长的一个,将p节点的失败指针指向那个最长匹配后缀子串对应的模式串的前缀的最后一个节点。
③:计算每个节点的失败指针这个过程看起来有些复杂,其实,如果把树中中相同深度的节点放到同一层,那么某个节点的失败指针只有可能出现在它所在层的上一层。
④:可以像KMP算法那样,当要求某个节点的失败指针时,通过已经求得的,深度更小的那些节点的失败指针来推导。即可以逐层依次来求解每个节点的失败指针。所以,失败指针的构建过程,是一个按层遍历树的过程。
当已经求得某个节点p的失败指针之后,如何寻找他的子节点的失败指针?
①:假设节点p的失败指针指向节点q,看节点p的子节点pc对应的字符,是否也可以在节点q的子节点中找到。如果找到了节点q的一个子节点qc,对应的字符跟节点pc对应的字符相同,则将节点pc的失败指针指向节点qc。
②:如果节点q中没有子节点的字符等于节点pc包含的字符,则令q=q->fail,继续上面的查找,直到q是root为止,如果没有找到相同字符的子节点,就让节点pc的失败指针指向root。
如何在AC自动机上匹配主串?
①:在匹配过程中,主串从i=0开始,AC自动机从指针p=root开始,假设模式串是b,主串是a。
- 如果p指向的节点有一个等于b[i]的子节点x,我们就更新p指向x,这个时候我们需要通过失败指针,检测一系列失败指针为结尾的路径是否是模式串。
- 如果p指向的节点没有等于b[i]的子节点,那失败指针就派上了用场了,我们让p=p->fail,然后继续这两个过程。
public void match(char[] text) { // text是主串
int n = text.length;
AcNode p = root;
for (int i = 0; i < n; ++i) {
int idx = text[i] - 'a';
while (p.children[idx] == null && p != root) {
p = p.fail; // 失败指针发挥作用的地方
}
p = p.children[idx];
if (p == null) p = root; // 如果没有匹配的,从root开始重新匹配
AcNode tmp = p;
while (tmp != root) { // 打印出可以匹配的模式串
if (tmp.isEndingChar == true) {
int pos = i-tmp.length+1;
System.out.println("匹配起始下标" + pos + "; 长度" + tmp.length);
}
tmp = tmp.fail;
}
}
}
AC自动机做匹配的时间复杂度是多少?
For循环依次遍历主串的每个字符,for循环内部最耗时的部分也是while循环,而这一部分的时间复杂度也是O(len),所以总的时间复杂度就是O(n*len)。因为敏感词不会很长,而且这个时间复杂度只是一个非常宽泛的上限,实际情况下,可能近似于O(n),所以AC自动机做敏感词过滤,性能非常高。
因为失效指针可能大部分情况下都指向root节点,所以绝大部分情况下,在AC自动机上做匹配的效率要远高于刚刚计算出的比较宽泛的时间复杂度,只有在极端情况下,AC自动机的性能才会退化的根Trie树一样。