用途
如果你想在一篇文章快速的查询含有的所有关键字,可以考虑AC自动机。
例子
- 关键字:“bbbbb”、“aba”、“aab”、“aabba”、“bbaa”
- 文章:aabbbbaa
文章中包含有 “aab”、“bbaa”
效率低的方式,你可以利用KMP算法,然后利用语句去一个关键字一个关键字,但效率差,AC自动机可以一次遍历即可匹配完成
思路
- 将关键字创建成字典树如下图:双圈的表示当前找一个关键字了,如果查找到当前节点,即可返回一个关键字
- 赋fail指针(红色部分)
2.1 0节点和0 节点的子节点 1,6 的fail指针指向0节点
2.2 从第二层,程序的遍历上面的字典树,为每个节点的子节点赋值fail指针
2.3 找到父类的fail指针对应的节点,是否含有和当前一样的节点的字符(建议直接先看例子)
- 如果Fail指针对应的节点,含有和当前一样的字符,则将改字符对应的节点赋给当前的fail指针
- 否则,就再找父节点的fail指针对应节点的fail指针,重复上面的步骤
注意:如果fail指针对应的节点是关键字,则将当前的节点也标记为关键字
例子:比如给节点11 赋fail指针 (重点)
- 找到节点11的父节点10,节点10对应的fail指针是7;
- 但节点7不包含字符‘b’,则继续找节点7的fail指针对应的节点为1;
- 节点1包含字符‘b’,则将节点2赋给节点11的fail指针。
- 查找
从root开始,对应的文章aabbbbaa。走的步骤如下
a: 节点6
aa:节点9
aab:节点10 ,找到一个关键字
aabb:节点11
aabbb::节点3(11后匹配失败,走向了节点2,节点2有b字符)
aabbbb:节点4
aabbbba:节点13(节点4匹配失败,跳转到节点3,节点3再次失败,跳转到节点2,匹配成功)
aabbbbaa::节点14,找到另外一个关键字,匹配成功
代码
public class StreamChecker01 {
public static void main(String[] args) {
StreamChecker01 streamChecker01 = new StreamChecker01(new String[]{"bbbbb", "aba", "aab","aabba","bbaa"});
String str = "aabbbbaa";
for (int i = 0; i < str.length(); i++) {
if(streamChecker01.query(str.charAt(i))){
System.out.println("匹配成功一个关键字");
}
}
}
TrieNode root = new TrieNode();
public StreamChecker01(String[] words) {
// 创建字符字典
for (String word:words) {
TrieNode curNode = root;
for (int i = 0; i < word.length(); i++) {
curNode = curNode.add(word.charAt(i));
}
curNode.setEnd(true);
}
TrieNode[] child = root.child;
root.fail = root;
for (int i = 0; i < child.length; i++) {
if(child[i] != null){
child[i].fail = root;
list.add(child[i]);
}
}
while (list.size() > 0){
TrieNode cur = list.pollFirst();
setFail(cur); // 为他的孩子,设置fail
}
}
TrieNode curNode = root;
public boolean query(char letter) {
while (curNode.getChild(letter) == null && curNode != root){
curNode = curNode.fail;
}
if (curNode == root){
if(curNode.getChild(letter) == null){ // 找到结尾
return false;
}
}
curNode = curNode.getChild(letter);
return curNode.isEnd;
}
LinkedList<TrieNode> list = new LinkedList<>();
private void setFail(TrieNode pNode){
TrieNode[] child = pNode.child;
if(child == null){
return;
}
for (int i = 0; i < child.length; i++) {
TrieNode curPNode = pNode;
if(child[i] != null){
TrieNode[] pfchild = curPNode.fail.child;
while ((pfchild == null || pfchild[i] == null) && curPNode != root){ // 表示没有匹配上
curPNode = curPNode.fail;
pfchild = curPNode.fail.child;
}
if(curPNode == root){
if(root.child[i] != null){
child[i].fail = root.child[i];
} else {
child[i].fail = root;
}
} else {
child[i].fail = pfchild[i];
}
if(child[i].fail.isEnd){
child[i].isEnd = true;
}
// setFail(child[i]);
list.addLast(child[i]);
}
}
}
}
class TrieNode{
boolean isEnd = false; // 是否是一个字符串的结尾
TrieNode fail = null; // 是否是一个字符串的结尾
TrieNode[] child = null;
// 添加功能
public TrieNode add(char c){
if(c == 'c'){
System.out.println(c);
}
if(child == null){
child = new TrieNode[26];
}
if(child[c - 'a'] == null){
child[c - 'a'] = new TrieNode();
}
return child[c - 'a'];
}
// 返回子节点功能
public TrieNode getChild(char c){
if(child == null){
return null;
}
return child[c - 'a'];
}
// 是否是一个字符串
public boolean isEnd() {
return isEnd;
}
public void setEnd(boolean end) {
isEnd = end;
}
}