使用AC自动机实现敏感词过滤(java)

主要分成2部分

  1. trie树的构建(前缀树,字典树)
  2. fail指针的构建

1. trie 树

  • 同一层级不会有重复的字符
  • 敏感词的最后一个字符会标记,并携带敏感词的长度

2. fail 指针的构建

fail 指针是指在某个分支匹配失败后,重新指向关联的其他分支上

  • 构建fail指针的遍历为层次遍历(广度优先)
  • root节点的fail指针指向null
  • 如果当前节点的父节点的fail指针指向的节点下存在与当前节点一样的子节点,则当前节点的fail指针指向该子节点,否则指向root节点
  • 如果当前节点的失败节点也是end节点,则将失败节点的长度信息合并到当前节点
java 复制代码
package com.xx.xxx.匹配算法;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.util.CollectionUtils;

import java.util.*;

/**
 * 多模匹配算法,AC自动机
 * 给出一个字符串,匹配多个敏感词
 * demo:
 * 敏感词库    he say her shr she
 * 被检测字符  sherhsay
 * 检测结果    she her he say
 */
public class AC {
    @Data
    @NoArgsConstructor
    public static class ACNode {
        Character am;
        // 子节点
        Map<Character, ACNode> children = new HashMap<>();
        ACNode failNode;
        // 存储匹配到的敏感字符长度
        List<Integer> wordLength = new ArrayList<>();
        // 是否是结束字符
        private boolean endOfWord;

        public ACNode(Character am) {
            this.am = am;
        }

        public String toString() {
            return "ACNode{" +
                    "am=" + am + "," +
                    "children=" + children +
                    ",wordLength=" + wordLength +
                    '}';
        }


        // 构建字典树
        public static void insert(ACNode root, String s) {
            ACNode temp = root;
            char[] chars = s.toCharArray();
            for (int i = 0; i < chars.length; i++) {
                if (!temp.children.containsKey(chars[i])) {
                    temp.children.put(chars[i], new ACNode(chars[i]));
                }
                temp = temp.children.get(chars[i]);
                // 如果是最后一个字符,则设置为结束字符
                if (i == chars.length - 1) {
                    temp.setEndOfWord(true);
                    temp.getWordLength().add(chars.length);
                }
            }
        }

        // 构建失败指针
        public static void buildFailPoint(ACNode root) {
            // 第一层的失败指针都是执行root,直接让第一层进入队列,方便 BFS
            Queue<ACNode> queue = new LinkedList<>();
            Map<Character, ACNode> childrens = root.getChildren();
            for (ACNode acNode : childrens.values()) {
                queue.offer(acNode);
                acNode.setFailNode(root);
            }
            // 构建剩余节点的失败指针,按层次遍历
            while (!queue.isEmpty()) {
                ACNode pnode = queue.poll();
                childrens = pnode.getChildren();
                Set<Map.Entry<Character, ACNode>> entries = childrens.entrySet();
                for (Map.Entry<Character, ACNode> entry : entries) {
                    // 当前节点的字符
                    Character key = entry.getKey();
                    ACNode cnode = entry.getValue();
                    // 如果当前节点的父节点的fail指针指向的节点下存在与当前节点一样的子节点,则当前节点的fail指针指向该子节点,否则指向root节点
                    if (pnode.failNode.children.containsKey(key)) {
                        cnode.setFailNode(pnode.failNode.children.get(key));
                    } else {
                        cnode.setFailNode(root);
                    }
                    // 如果当前节点的失败节点的wordLength不为空,则将当前节点的失败节点wordLength 合并到到当前节点的wordLength中
                    if (!CollectionUtils.isEmpty(cnode.failNode.wordLength)) {
                        cnode.getWordLength().addAll(cnode.failNode.wordLength);
                    }
                    queue.offer(cnode);
                }
            }

        }

        public static void query(ACNode root, String s) {
            ACNode temp = root;
            char[] chars = s.toCharArray();
            for (int i = 0; i < s.length(); i++) {
                // 如果这个字符串在当前节点的孩子节点找不到,且当前节点的fail指针不是null,则去失败指针去查找
                while (!temp.getChildren().containsKey(chars[i]) && temp.failNode != null) {
                    temp = temp.failNode;
                }
                // 如果当前节点有这个字符,则将temp替换为下面的孩子节点
                if (temp.getChildren().containsKey(chars[i])) {
                    temp = temp.getChildren().get(chars[i]);
                } else {
                    // 如果temp的failNode==null,则为root节点
                    continue;
                }
                // 如果检测到节点是结束字符,则将匹配到的敏感字符打印
                if (temp.isEndOfWord()) {
                    handle(temp, s, i);
                }
            }
        }

        public static void handle(ACNode node, String word, int curPoint) {
            for (Integer wordLen : node.wordLength) {
                int start = curPoint - wordLen + 1;
                String mathStr = word.substring(start, curPoint + 1);
                System.out.println("位置信息:[" + start + "," + curPoint + "),敏感词=" + mathStr);
            }
        }


        public static void main(String[] args) {
            ACNode root = new ACNode('-');
            root.failNode = null;
            insert(root, "黑社会");
            insert(root, "色情");
            insert(root, "黑暗任务");
            insert(root, "黑色会");
            insert(root, "国民党");
            insert(root, "国民");
            buildFailPoint(root);
            query(root, "按计划多久啊是德国 按时间大概是国民党卡的韩国阿克苏接电话ask接电话ask的话asks对话框,节点哈桑打算离开的机会撒的撒" +
                    "旦和了色情垃圾上单拉萨的黑色会啊是的噶时间大概时间大概是孤岛惊魂过去问工业国国民党");
        }


    }
}

参考B站大神 LDLD是程序员 的视频

【全程干货】程序员必备算法!AC自动机算法敏感词匹配算法!动画演示讲解,看完轻松掌握,面试官都被你唬住!!_哔哩哔哩_bilibili

相关推荐
成都犀牛几秒前
LlamaIndex 学习笔记
人工智能·python·深度学习·神经网络·学习
lpfasd1234 分钟前
状态模式(State Pattern)
java·设计模式·状态模式
代码老y8 分钟前
前端开发中的可访问性设计:让互联网更包容
java·服务器·前端·数据库
jakeswang10 分钟前
Java 项目中实现统一的 追踪ID,traceId实现分布式系统追踪
java·后端·架构
猛犸MAMMOTH11 分钟前
Python打卡第53天
开发语言·python·深度学习
寒山李白15 分钟前
Java 传输较大数据的相关问题解析和面试问答
java·开发语言·面试·传输
白总Server27 分钟前
Golang dig框架与GraphQL的完美结合
java·大数据·前端·javascript·后端·go·graphql
lightgis43 分钟前
个人支出智能分析系统
java
春生野草1 小时前
MyBatis中关于缓存的理解
java·缓存·mybatis
thinking-fish1 小时前
提示词Prompts(2)
python·langchain·提示词·提示词模板