持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
昨天写了前缀树,今天就来看看它的一个应用吧——AC自动机
AC自动机作用:
一篇大文章 str,一个包含若干敏感词的词典 str[],收集文章中所有出现的敏感词
实现:
建立一个 AC 自动机有两个步骤:
- 基础的 Trie 结构:将所有的模式串构成一棵 Trie。
- KMP 的思想:对 Trie 树上所有的结点构造失配指针。
而铅笔部分就是失配指针fail,每个节点都有适配指针,没画的fail指针的节点都是指向root的,通过它就实现了KMP的效果(匹配到后面匹配不上了也能尽量利用前面匹配上的部分)
接下来废话少说,上代码,相信配合注释和上一文的前缀树基础你应该能很容易的看懂什么下面的代码了,如果还没看前缀树,那点这 前缀树
class ACAutomation {
static class Node {
// end 用于一个字符串的末尾节点存放当前字符串
// 路上经过的节点的 end 都是为 null 的
private String end;
private boolean endUse; // 用于记录是否已经收集过该字符串了
private Node fail; // 失配指针
private Node[] nexts = new Node[26];
}
Node root = new Node();
// 插入敏感词
// 在不调整fail指针的情况下先构造好Trie树
public void insert(String s) {
char[] str = s.toCharArray();
Node cur = root;
for (char c : str) {
int index = c - 'a';
if (cur.nexts[index] == null) {
cur.nexts[index] = new Node();
}
cur = cur.nexts[index];
}
cur.end = s;
}
// 设置好fail指针
// 如果对这里不是很理解,可以网上找个动态图看看就明白了,
// 光口头表达对于这个来说确实不好表达清楚,
// 虽然我理解这过程但我现在还不会画动图,感觉对不起大家呀,有空一定学学咋画
public void build() {
LinkedList<Node> list = new LinkedList<>(); // 用于层序遍历
list.add(root);
while (!list.isEmpty()) { // 经典的层序遍历
Node cur = list.poll();
// 在当前节点时去设置nexts中节点的fail指针
for (int i = 0; i < 26; i++) {
if (cur.nexts[i] != null) {
// 先让它指向root节点
cur.nexts[i].fail = root;
// 去看 cFail 节点的子节点能否成为 cur.nexts[i]失配指针指向的节点
Node cFail = cur.fail;
while (cFail != null) {
if (cFail.nexts[i] != null) {
cur.nexts[i].fail = cFail.nexts[i];
break;
}
cFail = cFail.fail;
}
list.add(cur.nexts[i]);
}
}
}
}
// 收集content中出现过的敏感词
public List<String> containWords(String content) {
char[] str = content.toCharArray();
ArrayList<String> res = new ArrayList<>();
Node cur = root;
for (char c : str) {
int index = c - 'a';
while (cur.nexts[index] == null && cur != root) {
cur = cur.fail;
}
cur = cur.nexts[index] != null ? cur.nexts[index] : root;
Node follow = cur;
while (follow != root) {
if (follow.endUse) { // 如果已经收集过了就不再收集了
break;
}
if (follow.end != null) {
res.add(follow.end);
follow.endUse = true;
}
follow = follow.fail;
}
}
return res;
}
}