文章出处:极客时间《数据结构和算法之美》-作者:王争。该系列文章是本人的学习笔记。

1 敏感词过滤场景

在很多支持用户发表内容的网站,都有敏感词过滤替换的功能。例如将一些淫秽、反动内容过滤掉,或者替换为****。在一些社交类网站为了避免做广告之嫌,会把手机号替换为*****。当然有些人为了躲避被替换会写一三七一零六捌这样的手机号。这是后话了。这篇文章我们先学习如何替换字符串类的敏感词。我们可以维护一个敏感词词典,当用户输入评论之后,通过字符串匹配算法,查找是否包含敏感词。如果有,需要找到起始位置和长度,替换为***。
  前面讲的几种字符串匹配算法:BF、RK、BM、KMP、Trie树都可以实现,但是效率不够高。我们使用多模式串匹配算法来提高效率。

2 基于单模式串和Trie树实现敏感词过滤

BF、RK、BM、KMP都是单模式串匹配算法,是在一个模式串和一个主串之间进行匹配。多模式串匹配是多个模式串和一个主串进行匹配。为了解决敏感词过滤问题,用单模式串匹配来做的话,每次匹配一个模式串和主串,匹配多次,也可以实现,只是这样多次扫描主串效率会低。

多模式串匹配算法扫描一次主串实现匹配,这样的效率就很高了。我们可以对敏感词做预处理,构建一棵Trie树。用户输入内容后,把用户输入的内容作为主串,从第一个字符C开始匹配。当匹配 到Trie树的叶子节点或者中途有不匹配的字符的时候,我们将主串的开始位置偏移一位,也就是C字符的下一位,重新在Trie树中匹配。

基于Trie树的这种匹配方式很像单模式匹配的BF算法。我们知道KMP对BF算法的改进就是引入了next数组。当匹配失败的时候,主串位置不动,模式串位置移动。基于这种思路,我们优化Trie树查找的效率,这就是AC自动机算法。

3 AC自动机算法

AC自动机算法是一种经典的多模式串匹配算法,全称是 Aho-Corasick 算法。AC=Trie树+next数组。这里的next数组是基于树的。

构建AC自动机包含两个操作:1 构建Trie树;2 在Trie树上构建失效指针。

AC自动机每个节点的结构如下。

public class AcNode {public char data; public AcNode[] children = new AcNode[26]; // 字符集只包含 a~z 这 26 个字符public boolean isEndingChar = false; // 结尾字符为 truepublic int length = -1; // 当 isEndingChar=true 时,记录模式串长度public AcNode fail; // 失败指针public AcNode(char data) {this.data = data;}
}

关于Trie树的构建看上一篇文章。这里重点描述构建失效指针。

3.1 构建失效指针

假设这里有 4 个模式串,分别是 c,bc,bcd,abcd;主串是 abcd。

我们沿着Trie树走到p节点(下图中的紫色节点),那p的失效指针就是指从根节点到p节点形成的字符串abc,跟所有模式串前缀匹配的最长可匹配后缀子串,就是箭头指向的bc子串。

  

这里解释一下最长可匹配后缀子串。abc的后缀子串有c、bc。我们拿它们与其他模式串的前缀子串去匹配。如果某个后缀子串和其他模式串的某个前缀子串可匹配,就成为可匹配后缀子串。

从可匹配后缀子串中找到最长的那个就是最长可匹配后缀子串。我们将p节点的失效指针指向那个最长可匹配后缀子串对应的前缀子串的最后一个位置。就是上图中箭头所指位置。

计算每个节点的失效指针看似复杂,是不是可以类似KMP,利用已经求得的、深度更小的节点的失效指针来推到呢。如果这样的话,我们可以逐层解决,这就是树的层次遍历的过程。

根节点的失效指针指向自己。如果已知节点p的失效指针指向q,如何推到p的子节点pc的失效指针指向什么位置。

如果q有一个子节点的字符等于pc节点的字符,那么pc的失效指针指向该节点。

如果q的所有子节点的字符都不等于pc节点的字符,那么q=q.失效指针。再继续判断。

代码如下。

public void buildFailurePointer() {Queue<AcNode> queue = new LinkedList<>();root.fail = null;queue.add(root);while (!queue.isEmpty()) {AcNode p = queue.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;}}queue.add(pc);}}}

最终,通过按层级计算每个节点的失效指针,最后构建完成的AC自动机如下图。

3.2 在AC自动机上匹配主串

如何在AC自动机上匹配主串呢?例如主串是a,从i=0开始,AC自动机从p=root开始。

如果p指向的子节点x的字符串等于a[i],则把p=x。这个时候我们判断一下以目前匹配的字符串来说,有哪些是匹配到的字符串(这也是失效指针的含义)。实现方式就是检测p.失效指针是否是一个模式串的结尾。如果是可以得到匹配的字符串的长度和结尾位置。继续检测p.失效指针.失效指针。结合代码看最好理解。处理完成i++。

如果p指向的子节点的字符串都不等于a[i]。则p=p.失效指针。

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;//字符串已经匹配了一部分了,模式串中就到tmp节点。那就判断tmp是不是个字符串,如果是,那就是匹配到了。如果tmp不是个字符串,那已经匹配的这部分如果在下一位发生不匹配的时候,指针应该回退到tmp.fail。那继续看tmp.fail是不是个字符串。//如果是,那就是说已经匹配的部分包含某个字符串。while (tmp != root) { // 打印出可以匹配的模式串if (tmp.isEndingChar == true) {int pos = i-tmp.length+1;System.out.println(" 匹配起始下标 " + pos + "; 长度 " + tmp.length);}tmp = tmp.fail;}}}

3.3 AC自动机的时间复杂度

Trie 树构建的时间复杂度是 O(m*len),其中 len 表示敏感词的平均长度,m 表示敏感词的个数。

构建失效指针的时间复杂度。这里给一个不太精确的上界。假设 Trie 树中总的节点个数是 k,每个节点构建失效指针的时候,(你可以看下代码)最耗时的环节是 while 循环中的 q=q->fail,每运行一次这个语句,q 指向节点的深度都会减少 1,而树的高度最高也不会超过 len,所以每个节点构建失败指针的时间复杂度是 O(len)。整个失败指针的构建过程就是 O(k*len)。

在AC自动机上查询的时间复杂度。与构建失效指针的分析类似。最外层for循环的长度是主串的长度。循环内部耗时的操作是两个while循环,每个while循环的次数最多是len。所以时间复杂度不超过O(n*len)。一般来讲敏感词的长度不会很长,近似O(n),近似主串的长度。

从时间复杂度来讲,AC自动机和Trie树的查找性能是一样的。实际上,因为失效指针大部分指向root节点,所以绝大多数情况下,AC自动机做匹配的效率要远远高于给出的估算。只有在极端情况下才会退化为何Trie树一样的效率。

AC自动机:多模式串匹配实现敏感词过滤相关推荐

  1. 【Filter过滤器案例】登录验证+敏感词过滤

    登录验证 >>> 思路: >>> 先假设拦截所有资源(Servlet, jsp...均不让访问),判断资源是否与登录有关: >>> 1.与登录相关 ...

  2. 字符串匹配算法 -- AC自动机 基于Trie树的高效的敏感词过滤算法

    文章目录 1. 算法背景 2. AC自动机实现原理 2.1 构建失败指针 2.2 依赖失败指针过滤敏感词 3. 复杂度及完整代码 1. 算法背景 之前介绍过单模式串匹配的高效算法:BM和KMP 以及 ...

  3. Trie树实现前缀自动补全 + AC自动机实现敏感词过滤

    文章目录 背景 扩展 AC自动机 背景 最近参与了某业务系统的开发, 需要根据城市的名字简称,找到其官方的完整名称.比如云南的大理,其实其完整的名称是大理白族自治州.可以参考官方的行政区划,点这里. ...

  4. TypeScript:Aho–Corasick算法实现敏感词过滤

    敏感词过滤应该是许多后端同事经常会遇到的需求,无论是评论.弹幕.文章,都需要做敏感词过滤处理来规避风险.在前端开发中,使用replace函数来替换字符串是我们的常规操作,在这之前我思考过如果用Java ...

  5. 对敏感词过滤(DFA算法)的思考与理解

    对敏感词过滤的思考与理解 一.技术概述 1.这个技术是干什么用的? 2.学习这个技术的原因 3.技术的难点在哪 二.技术详述 1.流程图 2.代码 三.技术过程中遇见的问题和解决过程 四.总结 五.参 ...

  6. 敏感词过滤,PHP实现的Trie树

    [转载]敏感词过滤,PHP实现的Trie树 原文地址:http://blog.11034.org/2012-07/trie_in_php.html 项目需求,要做敏感词过滤,对于敏感词本身就是一个CR ...

  7. python中哪些词是敏感字词_python实现敏感词过滤的几种方法

    1.replace过滤 最简单也是最直接的就是直接循环敏感词,然后使用replace过滤关键词,文章和敏感词少的时候还可以,多的时候效率就真的很一般了. 2.使用正则过滤 有两个技术要点, 1.使用P ...

  8. python骂人的程序_Python实现敏感词过滤的4种方法

    在我们生活中的一些场合经常会有一些不该出现的敏感词,我们通常会使用*去屏蔽它,例如:尼玛 -> **,一些骂人的敏感词和一些政治敏感词都不应该出现在一些公共场合中,这个时候我们就需要一定的手段去 ...

  9. python敏感词过滤代码简单_大型企业都在用,Python实现敏感词过滤

    在我们生活中的一些场合经常会有一些不该出现的敏感词,我们通常会使用*去屏蔽它,例如:尼玛 -> **,一些骂人的敏感词和一些政治敏感词都不应该出现在一些公共场合中,这个时候我们就需要一定的手段去 ...

最新文章

  1. AttributeError: Cant get attribute SPPF on module models
  2. 你知道select count(*)底层究竟干了啥么?
  3. BMP图片格式。1,4,8,16,24位与windows分辨率没关系
  4. strcpy和memcpy的区别?
  5. 开发DBA(APPLICATION DBA)的重要性
  6. CRT 入口函数 CRTStartup
  7. python websocket服务器https_Socket与WebSocket以及http与https重新总结
  8. (100)详细描述一个你做过的项目, 面试必问(二十四)(第20天)
  9. oracle 9 创建数据库,Oracle 9i创建数据库(转)
  10. Drools 规则引擎的使用
  11. 西刺代理python_Python四线程爬取西刺代理
  12. 计算机处理系统比人工的优势,人工智能技术的优势及其在计算机网络中的应用...
  13. 小程序里说的冷启动和热启动是什么
  14. 给初学和业余学习中医的朋友
  15. KE-之单机案例分析
  16. Linux的SPI应用(四)----访问Nor Flash(MT25QL01GBBB)
  17. 【Python数据分析】基础入门学习指南到数据分析实战
  18. 如何将源生DrawerLayout满屏显示只覆盖ActionBar
  19. 什么是AI管道和MLOps?
  20. element-plus的el-date-picker日期范围选择控件,根据开始日期限定结束日期的可选范围为开始日期到开始日期+30天

热门文章

  1. Socket常用语法与socketserver实例
  2. 通过Servlet的response绘制页面验证码
  3. 架构重构改善既有代码的设计
  4. 在.net 2.0/3.0程序中使用扩展方法
  5. 好久没发胡说八道的贴了,今天发一贴
  6. 坡度土方计算案例_土石方工程造价中的细节解析(案例+计算式)
  7. smartdeblur有手机版吗_《GTA5》高仿手机版问世,更新高清城市地图后你会喜欢吗?...
  8. PMP读书笔记(第13章)
  9. web项目Servlet配置及jsp访问Servlet
  10. jmeter 入门操作