AC自动机:如何用多模式串匹配实现敏感词过滤功能?
用字符串匹配算法,通过维护一个敏感词的字典,当用户输入一段文字内容之后,通过字符串匹配算法,来查找用户输入的这段文字,是否包含敏感词,如果有,就用***替代
如何实现一个高性能的敏感词过滤系统?多模式串匹配算法
基于单模式串和Trie树实现的敏感词过滤
BF 、RK、BM、KMP、Trie树中,前四个是单模式串匹配算法,只有Trie树是多模式串匹配算法
所谓的单模式串就是一个模式串和一个主串之间匹配,多模式串就是多个模式串和一个主串之间做匹配
如何用Trie树实现敏感词过滤功能呢?
对敏感词进行预处理,构建Trie树结构,如果敏感词字符动态更新了,只需要动态更新一下Trie树即可,当用户输入一个文本内容后,把用户输入的内容作为主串,从第一个字符开始,在Trie树中匹配,当匹配到Trie树的叶子节点时候或遇到不匹配的字符时候,将主串的开始匹配位置往后移动一位,就是从起始字符的下一个字符开始重新在Trie树中匹配
更高效的经典多模式串匹配算法:AC自动机
Trie和AC自动机之间的关系就像单串匹配中朴素的串匹配算法,跟KMP算法一样,只不过Trie树针对的是多模式串而已,所以AC自动机就是在Trie树之上,加了类似KMP的next数组,只不过现在的next数组是构建在树上罢了
public class AcNode{
public char data;
public AcNode[] children = new AcNode[26];//字节集只包含a~z这26个字符
public boolean isEndingChar = false; //结尾字符为true
public int length = -1; //当isEndingChar = true时,记录模式串长度
public AcNode fail;//失败指针
public AcNode(char data){
this.data = data;
}
}
AC自动机的构建包含两个操作:
- 将多个模式串构建成Trie树
- 在Trie树上构建失败指针(相当于KMP中的失效函数next数组)
构建好Trie树后如何在它之上构建失败指针?
比如有4个模式串 c , bc , bcd , abcd,主串是abcd
root
a b c
b c
c
d
d
Trie树中的每一个节点都有一个失败指针,假如沿Trie树走到p节点,就是红色节点 c,那p的失败指针就是从root走到红色节点形成的字符串abc,跟所有模式串前缀匹配的最长可匹配后缀子串,就是箭头指的bc模式串
关于最长可匹配后缀子串,字符串abc的后缀子串有两个bc,c,拿它们与其他模式串匹配,如果某个后缀子串可以匹配某个模式串的前缀,把这个后缀子串叫做可匹配后缀子串,从可匹配后缀子串中找出最长的一个,就是最长可匹配后缀子串
将p节点的失败指针指向那个最长可匹配后缀子串对应的模式串的前缀的最后一个节点,就是从第一个c指向的第二个c
失败指针的构建过程是一个按层遍历树的过程,如果root的失败指针为null,就是指向自己
当我们已经求个某个节点p的失败指针之后,如何寻找它的子节点的失败指针?
设节点p的失败指针指向节点q,看节点p的子节点pc对应的字符是否可以在节点q的子节点中找到,如果找到了节点q的一个子节点qc,对应的字符跟节点pc对应的字符相同,将节点pc的失败指针指向节点qc,如果节点q中没有子节点的字符等于节点pc包含的字符,令q = q ->fail(fail表示失败指针),继续查找,直到q= root为止,如果没有找到相同字符的子节点,就让节点pc的失败指针指向root
构建失败指针:
public void buildFailurePointer(){
Quene<AcNode> quene = new LinkedList<>();
root.fail = null;
quene.add(root);
while(!quene.isEmpty()){
AcNode p = quene.remove();
for(int i = 0 ; i < 26 ; ++i){
AcNode pc = p.children[i];
if(pc == null) continue;
if(p == root){
pc.fail = root;
}else{
AcNode q = p.fail;
while(q != null){
AcNode qc = q.children[pc.data - 'a'];
if(qc != null){
pc.fail = qc;
break;
}
q = q.fail;
}
if(q == null){
pc.fail = root;
}
}
quene.add(pc);
}
}
}
如何在AC自动机上匹配主串?
过程中,主串从i = 0开始,AC自动机从指针 p = root开始,假设模式串是b,主串是a
- 如果p指向的节点有一个等于b[i]的子节点x,更新p指向x,这时候需要通过失败指针,检测一系列失败指针为结尾的路径是否是模式串,处理后,i+1,继续
- 如果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自动机实现的敏感词过滤系统,是否比单模式串匹配方法更高效?
将敏感词构建成AC自动机,包括构建Trie树以及失败指针
从复杂度上看,AC自动机匹配的效率跟Trie树一样,实际上失效指针大部分指向root,
https://www.cnblogs.com/sclbgw7/p/9260756.html
https://www.cnblogs.com/hyfhaha/p/10802604.html