AC自动机算法
前言
AC自动机算法(构建模式串,类似KMP的模式串(短的字符串作为模式串),用于字符串匹配
1、构建字典树
static class AcNode {
//孩子节点用HashMap存储,能够在O(1)的时间内查找到,效率高
Map<Character, AcNode> children = new HashMap<>();
AcNode failNode;
//使用set集合存储字符长度,防止敏感字符重复导致集合内数据重复
Set<Integer> wordLengthList = new HashSet<>();
}
public static void insert(AcNode root, String s) {
AcNode temp = root;
char[] chars = s.toCharArray();
for (int i = 0; i < s.length(); i++) {
//如果不包含这个字符就创建孩子节点
if (!temp.children.containsKey(chars[i])) {
temp.children.put(chars[i], new AcNode());
}
temp = temp.children.get(chars[i]);//temp指向孩子节点
}
//一个字符串遍历完了后,将其长度保存到最后一个孩子节点信息中
temp.wordLengthList.add(s.length());
}
2、构建fail指针
public static void buildFailPath(AcNode root) {
//第一层的fail指针指向root,并且让第一层的节点入队,方便BFS
Queue<AcNode> queue = new LinkedList<>();
Map<Character, AcNode> childrens = root.children;
Iterator iterator = childrens.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Character, AcNode> next = (Map.Entry<Character, AcNode>) iterator.next();
queue.offer(next.getValue());
next.getValue().failNode = root;
}
//构建剩余层数节点的fail指针,利用层次遍历
while (!queue.isEmpty()) {
AcNode x = queue.poll();
childrens = x.children; //取出当前节点的所有孩子
iterator = childrens.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Character, AcNode> next = (Map.Entry<Character, AcNode>) iterator.next();
AcNode y = next.getValue(); //得到当前某个孩子节点
AcNode fafail = x.failNode; //得到孩子节点的父节点的fail节点
//如果 fafail节点没有与 当前节点父节点具有相同的转移路径,则继续获取 fafail 节点的失败指针指向的节点,将其赋值给 fafail
while (fafail != null && (!fafail.children.containsKey(next.getKey()))) {
fafail = fafail.failNode;
}
//回溯到了root节点,只有root节点的fail才为null
if (fafail == null) {
y.failNode = root;
} else {
//fafail节点有与当前节点父节点具有相同的转移路径,则把当前孩子节点的fail指向fafail节点的孩子节点
y.failNode = fafail.children.get(next.getKey());
}
//如果当前节点的fail节点有保存字符串的长度信息,则把信息存储合并到当前节点
if (y.failNode.wordLengthList.size() != 0) {
y.wordLengthList.addAll(y.failNode.wordLengthList);
}
queue.offer(y);//最后别忘了把当前孩子节点入队
}
}
}
3、开始查询
public static void query(AcNode root, String s) {
AcNode temp = root;
char[] c = s.toCharArray();
for (int i = 0; i < s.length(); i++) {
//如果这个字符在当前节点的孩子里面没有或者当前节点的fail指针不为空,就有可能通过fail指针找到这个字符
//所以就一直向上更换temp节点
while (temp.children.get(c[i]) == null && temp.failNode != null) {
temp = temp.failNode;
}
//如果因为当前节点的孩子节点有这个字符,则将temp替换为下面的孩子节点
if (temp.children.get(c[i]) != null) {
temp = temp.children.get(c[i]);
}
//如果temp的failnode为空,代表temp为root节点,没有在树中找到符合的敏感字,故跳出循环,检索下个字符
else {
continue;
}
//如果检索到当前节点的长度信息存在,说明模式串走到了最后一个节点,则代表搜索到了敏感词,打印输出即可
if (temp.wordLengthList.size() != 0) {
handleMatchWords(temp, s, i);
}
}
}
//利用节点存储的字符长度信息,打印输出敏感词及其在搜索串内的坐标
public static void handleMatchWords(AcNode node, String word, int currentPos) {
for (Integer wordLen : node.wordLengthList) {
int startIndex = currentPos - wordLen + 1;
String matchWord = word.substring(startIndex, currentPos + 1);
System.out.println("匹配到的敏感词为:" + matchWord + ",其在搜索串中下标为:" + startIndex + "," + currentPos);
}
}
4、测试
public static void main(String[] args){
AcNode root = new AcNode();
insert(root, "xx");
insert(root, "***");
buildFailPath(root);
query(root, "你是xx吗?两会期间代理模式是常用的java设计模式,他的特征是代理类与委托类有**同样的接口**,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。简单的说就是,我们在访问实际对象时,是通过代理对象来访问的,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种xx在没学ac自动机之前,觉得ac自动机是个很神奇,很高深,很难的算法,学完之后发现,ac自动机确实很神奇,很高深,但是却并不难。我说ac自动机很神奇,在于这个算法中失配指针的妙处(好比kmp算法中的next数组),说它高深,是因为这个不是一般的算法,而是建立在两个普通算法的基础之上,而这两个算法就是kmp与字典树。所以,如果在看这篇博客之前,你还不会字典树或者kmp算法,那么请先学习字典树或者kmp算法之后再来看这篇博客。好了,闲话扯完了,下面进入正题。在学习一个新东西之前,一定要知道这个东西是什么,有什么用,我们学它的目的是什么,如果对这些东西没有一个清楚的把握,我不认为你能学好这个新知识。那么首先我们来说一下ac自动机是什么。下面是我从百度上找的。Aho-Corasick automaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法。从上面我们可以知道,ac自动机其实就是一种多模匹配算法,那么你可能会问什么叫做多模匹配算法。下面是我对多模匹配的理解,与多模与之对于的是单模,单模就是给你一个单词,然后给你一个字符串,问你这个单词是否在这个字符串中出现过(匹配),这个问题可以用kmp算法在比较高效的效率上完成这个任务。那么现在我们换个问题,给你很多个单词,然后给你一段字符串,问你有多少个单词在这个字符串中出现过,当然我们暴力做,用每一个单词对字符串做kmp,这样虽然理论上可行,但是时间复杂度非常之高,当单词的个数比较多并且字符串很长的情况下不能有效的解决这个问题,所以这时候就要用到我们的ac自动机算法了。对于上面的文字,我已经回答了什么是多模匹配和我们为什么要学习ac自动机那就是ac自动机的作用是什么等一系列问题。下面是ac自动机的具体实现步骤以及模板代码。把所有的单词建立一个字典树。在建立字典树之前,我们先定义每个字典树上节点的结构体变量***(2000)");
}
5、输出结果
匹配到的敏感词为:xx,其在搜索串中下标为:2,3
匹配到的敏感词为:xx,其在搜索串中下标为:244,245
匹配到的敏感词为:***,其在搜索串中下标为:1036,1038
总结
关于如何构建fail指针?从根节点的children开始,所以后面都是从children开始匹配,当某个节点的children没有匹配上的时候,需要找和某个节点值一样的节点,然后从他的children去匹配。
当前节点(右e)的父亲节点(右h)的失败指针(左h)的儿子中有当前节点(右e),那么当前节点指向那个儿子(左e)