使用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

相关推荐
Code成立2 分钟前
《Java核心技术 卷I》用户图形界面鼠标事件
java·开发语言·计算机外设
赛丽曼13 分钟前
Python中的HTML
python·html
luky!21 分钟前
算法--解决熄灯问题
python·算法
Xiao Fei Xiangζั͡ޓއއ22 分钟前
一觉睡醒,全世界计算机水平下降100倍,而我却精通C语言——scanf函数
c语言·开发语言·笔记·程序人生·面试·蓝桥杯·学习方法
记录无知岁月24 分钟前
【MATLAB】目标检测初探
开发语言·yolo·目标检测·matlab·yolov3·yolov2
鸽鸽程序猿27 分钟前
【算法】【优选算法】二分查找算法(下)
java·算法·二分查找算法
深度学习lover35 分钟前
<项目代码>YOLOv8 番茄识别<目标检测>
人工智能·python·yolo·目标检测·计算机视觉·番茄识别
IT古董35 分钟前
【机器学习】机器学习中用到的高等数学知识-1.线性代数 (Linear Algebra)
人工智能·python·线性代数·机器学习
远望清一色38 分钟前
基于MATLAB身份证号码识别
开发语言·图像处理·算法·matlab
生信与遗传解读39 分钟前
基于python的线性代数相关计算
python·线性代数·机器学习