详解Java中AC自动机的原理与实现

目录
  • 简介
  • 工作过程
  • 数据结构
  • 初始化
  • 构建字典树
  • 构建失败指针
  • 匹配
  • 执行结果

简介

AC自动机是一个多模式匹配算法,在模式匹配领域被广泛应用,举一个经典的例子,违禁词查找并替换为***。AC自动机其实是Trie树和KMP 算法的结合,首先将多模式串建立一个Tire树,然后结合KMP算法前缀与后缀匹配可以减少不必要比较的思想达到高效找到字符串中出现的匹配串。

如果不知道什么是Tire树,可以先查看:详解Java中字典树(Trie树)的图解与实现

如果不知道KMP算法,可以先查看:详解Java中KMP算法的图解与实现

工作过程

首先看一下AC自动机的结构,从造型上看,跟我们之前讲Tire树几乎一样,但是多了红色线条(这里因为画完太乱,没有画完),这个红色线条我们称为失败指针。其匹配规则与KMP一致,后缀和前缀的匹配,不一样的是,KMP是同一个模式串的前缀和后缀进行匹配,而这里是当前模式串的后缀,与另一个模式串的前缀进行匹配。如果能够匹配上,因为这两个模式串的前缀一定不同(相同的前缀已经聚合),将当前已匹配的后缀拿出来,比如abo,后缀为o,bo,abo,这时候我们再找另一个模式串的最长前缀与当前后缀匹配上(对应kmp中的最长前缀后缀子串),这时候我们可以找到out的o,则about中的o节点的失败指针指向out的o节点,这么做的意义就是主串可以一直往后比较,不用往前回溯(比如ab,之前匹配过能匹配上,但是到o是失败了,其他匹配串不可能出现ab前缀,所以不必再匹配,一定失败)。

构建过程:建立一棵Tire树,结尾节点需要标志当前模式串的长度,构建失败指针。

查找过程:从根节点出发,查找当前节点的孩子节点是否有与当前字符匹配的字符,匹配则判断是否为尾节点,是则匹配成功,记录。不是尾节点继续匹配。如果孩子节点没有与字符匹配的,则直接转到失败指针继续操作。

数据结构

一个value记录当前节点的值,childNode记录当前节点的子节点(假设仅出现26个小写字母,空间存在浪费,可使用hash表,有序二分,跳表进行优化),isTail标志当前节点是否为尾节点,failNode表示失败指针,即当前节点的孩子节点与当前字符均不匹配的时候,转到哪个节点接续进行匹配,tailLength,记录模式串的长度,方便快速拿出模式串的值(根据长度以及匹配的index,从主串中拿)。

public static class Node{
       //当前节点值
       private char value;
       //当前节点的孩子节点
       private Node[] childNode;
       //标志当前节点是否是某单词结尾
       private boolean isTail;
       //失败指针
       private Node failNode;
       //匹配串长度,当isTail==true时,表示从root当当前位置是一个完整的匹配串,记录这个匹配串的长度,便于之后快速找到匹配串
       private Integer tailLength;
       public Node(char value) {
           this.value = value;
      }
  }

初始化

初始化一棵仅存在root的根节点,root的失败指针以及长度均为null。

Node root;
   public void init() {
       root = new Node('\0');
       root.childNode = new Node[26];
  }

构建字典树

这个过程之前Tire树中已经讲过,不再赘述,唯一的区别是需要在结尾节点上标志当前模式串的长度。

public void insertStr(char[] chars) {
       //首先判断首字符是否已经在字典树中,然后判断第二字符,依次往下进行判断,找到第一个不存在的字符进行插入孩节点
       Node p = root;
       //表明当前处理到了第几个字符
       int chIndex = 0;
       while (chIndex < chars.length) {
           while (chIndex < chars.length && null != p) {
               Node[] children = p.childNode;
               boolean find = false;
               for (Node child : children) {
                   if (null == child) {continue;}
                   if (child.value == chars[chIndex]) {
                       //当前字符已经存在,不需要再进行存储
                       //从当前节点出发,存储下一个字符
                       p = child;
                       ++ chIndex;
                       find = true;
                       break;
                  }
              }
               if (Boolean.TRUE.equals(find)) {
                   //在孩子中找到了 不用再次存储
                   break;
              }
               //如果把孩子节点都找遍了,还没有找到这个字符,直接将这个字符加入当前节点的孩子节点
               Node node = new Node(chars[chIndex]);
               node.childNode = new Node[26];
               children[chars[chIndex] - 'a'] = node;
               p = node;
               ++ chIndex;
          }
      }
       //字符串中字符全部进入tire树中后,将最后一个字符所在节点标志为结尾节点
       p.isTail = true;
       p.tailLength = chars.length;
  }

构建失败指针

从根节点开始层序遍历树结构,构建失败指针。一个节点的子节点的失败指针可以根据当前节点的失败指针得到,因为我们是用后缀去与前缀匹配,所以如果我们采用层序遍历,与当前后缀的前缀一定在上层,已经匹配出来了。那么当前节点的子节点的失败指针则可以根据当前节点的失败指针,查找失败指针指向的节点的子节点是否有与当前节点的子节点相等的,相等则这个子节点的失败指针直接指向,不相等则继续找,找不到直接指向root。根据上面的图,我们来举一个例子,我们已经找到about中o节点(o1)的失败指针是out中的o节点(o2),接下来我们怎么找u(u1)的失败指针呢?首先根据o1的失败指针我们找到了o2,o2的子节点为u(u2),恰好与我们u1的值相等,此时我们就可以将u1的失败指针指向u2。以此类推,如果访问到最后为空(root的失败指针为空),则直接将失败指针指向root。

public void madeFailNext() {
       //层序遍历,为了保证求解这个节点失败指针的时候,它的父节点的失败指针以及失败指针的失败指针。。。。已经求得,可以完全根据这个找
       Deque<Node> nodes = new LinkedList<>();
       nodes.add(root);
       while (!nodes.isEmpty()) {
           Node current = nodes.poll();
           Node[] children = current.childNode;
           for (Node child : children) {
               if (null == child) {
                   continue;
              }
               Node failNode = current.failNode;
               while (null != failNode) {
                   //找到当前节点的失败指针,查看失败指针子节点是否有==
                   Node[] failChildren = failNode.childNode;
                   Node node = failChildren[child.value - 'a'];
                   if (null == node) {
                       //找当前指针的下一个指针
                       failNode = failNode.failNode;
                       continue;
                  }
                   //已经找到匹配的
                   //将失败指针指向node
                   child.failNode = node;
                   break;
              }
               //如果找完还没有找到,指向root
               if (null == failNode) {
                   child.failNode = root;
              }
               nodes.add(child);
          }
      }
  }

匹配

从首字符,字典树从root节点开始进行匹配,如果字符与节点值匹配,则判断是否为尾字符,如果是匹配上一个违禁词,记录下来,如果不匹配则转移到失败指针继续进行匹配。

/**
    * 匹配出str中所有出现的关键词
    * @param str
    * @return
    */
   public List<String> match(String str) {
       //遍历当前子串串,从根节点出发,如果匹配就一直往下进行匹配,同时需要看匹配的节点是否为结尾节点,如果是,匹配上一个
       //如果不匹配则通过失败指针转移到下一个节点
       this.dfs(root, 0, str);
       return machStr;
  }

   //abcdeasdabcebcd
   List<String> machStr = new ArrayList<>();
   private void dfs(Node node, int chIndex, String chars) {
       if (chIndex >= chars.length()) {
           return;
      }
       //从将当前字符与当前node的孩子节点进行匹配,如果当前字符与node的孩子节点.value匹配,判断当前字符是否为尾节点,是,则记录,匹配到了一个
       //继续匹配(子节点,与下一个元素进行匹配)
       //如果不匹配,则转到失败指针
       Node[] children = node.childNode;
       Node child = children[chars.charAt(chIndex) - 'a'];
       if (null == child) {
           //不匹配,转到失败指针
           //如果当前node==root,从root匹配,root的失败指针是null
           if (node == root) {
               dfs(root, ++ chIndex, chars);
          } else {
               dfs(node.failNode, chIndex, chars);
          }
      } else {
           //匹配到了
           if (child.isTail) {
               //并且是结尾节点,取从child.value到child.tailLength的字符
               machStr.add(chars.substring(chIndex - child.tailLength  + 1, chIndex + 1));
          }
           dfs(child, ++ chIndex, chars);
      }

  }

执行结果

public static void main(String[] args) {
       ACAutomaton acAutomaton = new ACAutomaton();
       //初始化一个仅有根节点的字典树
       acAutomaton.init();
       //构建Tire树
       acAutomaton.insertStr("out".toCharArray());
       acAutomaton.insertStr("about".toCharArray());
       acAutomaton.insertStr("act".toCharArray());
       //构建失败指针
       acAutomaton.madeFailNext();
       System.out.println("ces");
       //匹配
       List<String> result = acAutomaton.match("abcdeasactdaboutcebcd");
  }

到此这篇关于详解Java中AC自动机的原理与实现的文章就介绍到这了,更多相关Java AC自动机内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java编程之AC自动机工作原理与实现代码

    在阅读本文之前,大家可以先参考下<多模字符串匹配算法原理及Java实现代码> 简介: 本文是博主自身对AC自动机的原理的一些理解和看法,主要以举例的方式讲解,同时又配以相应的图片.代码实现部分也予以明确的注释,希望给大家不一样的感受.AC自动机主要用于多模式字符串的匹配,本质上是KMP算法的树形扩展.这篇文章主要介绍AC自动机的工作原理,并在此基础上用Java代码实现一个简易的AC自动机. 1.应用场景-多模字符串匹配 我们现在考虑这样一个问题,在一个文本串text中,我们想找出多个目标字符串

  • Java实现AC自动机全文检索示例

    第一步,构建Trie树,定义Node类型: /** * Created by zhaoyy on 2017/2/7. */ interface Node { char value(); boolean exists(); boolean isRoot(); Node parent(); Node childOf(char c); Node fail(); void setFail(Node node); void setExists(boolean exists); void add(Node

  • 详解Java中AC自动机的原理与实现

    目录 简介 工作过程 数据结构 初始化 构建字典树 构建失败指针 匹配 执行结果 简介 AC自动机是一个多模式匹配算法,在模式匹配领域被广泛应用,举一个经典的例子,违禁词查找并替换为***.AC自动机其实是Trie树和KMP 算法的结合,首先将多模式串建立一个Tire树,然后结合KMP算法前缀与后缀匹配可以减少不必要比较的思想达到高效找到字符串中出现的匹配串. 如果不知道什么是Tire树,可以先查看:详解Java中字典树(Trie树)的图解与实现 如果不知道KMP算法,可以先查看:详解Java中

  • 详解Java 中泛型的实现原理

    泛型是 Java 开发中常用的技术,了解泛型的几种形式和实现泛型的基本原理,有助于写出更优质的代码.本文总结了 Java 泛型的三种形式以及泛型实现原理. 泛型 泛型的本质是对类型进行参数化,在代码逻辑不关注具体的数据类型时使用.例如:实现一个通用的排序算法,此时关注的是算法本身,而非排序的对象的类型. 泛型方法 如下定义了一个泛型方法, 声明了一个类型变量,它可以应用于参数,返回值,和方法内的代码逻辑. class GenericMethod{ public <T> T[] sort(T[]

  • 详解Java中跳跃表的原理和实现

    目录 一.跳跃表的引入 二.算法分析 1.时间复杂度 2.空间复杂度 三.跳跃表介绍 四.跳跃表的实现 1.数据结构定义 2.查找 3.插入 4.删除 五.实战 1.代码 2.测试结果 一.跳跃表的引入 对有序顺序表可以采用二分查找,查找的时间复杂度为O (logn),插入.删除的时间复杂度为 O(n ).但是对有序链表不可以采用二分查找,查找.插入和删除的时间复杂度均为O (n). 有序链表如下图所示,若查找 8,则必须从第 1 个节点开始,依次比较 8 次才能查找成功. 如何利用链表的有序性

  • 详解Java中static关键字的使用和原理

    目录 概述 定义和使用格式 类变量 静态方法 调用格式 静态原理图解 静态代码块 概述 关于 static 关键字的使用,它可以用来修饰的成员变量和成员方法,被修饰的成员是属于类的,而不是单单是属 于某个对象的.也就是说,既然属于类,就可以不靠创建对象来调用了. 定义和使用格式 类变量 当 static 修饰成员变量时,该变量称为类变量.该类的每个对象都共享同一个类变量的值.任何对象都可以更改 该类变量的值,但也可以在不创建该类的对象的情况下对类变量进行操作. 类变量:使用 static关键字修

  • 详解Java中String类的各种用法

    目录 一.创建字符串 二.字符.字节与字符串的转换 1.字符与字符串的转换 2.字节与字符串的转换 三.字符串的比较 1.字符串常量池 2.字符串内容比较 四.字符串查找 五.字符串替换 六.字符串拆分 七.字符串截取 八.String类中其它的常用方法 九.StringBuffer 和 StringBuilder 1.StringBuilder与StringBuffer的区别 2.StringBuilder与StringBuffer常用的方法 十.对字符串引用的理解 一.创建字符串 创建字符串

  • 详解Java线程池和Executor原理的分析

    详解Java线程池和Executor原理的分析 线程池作用与基本知识 在开始之前,我们先来讨论下"线程池"这个概念."线程池",顾名思义就是一个线程缓存.它是一个或者多个线程的集合,用户可以把需要执行的任务简单地扔给线程池,而不用过多的纠结与执行的细节.那么线程池有哪些作用?或者说与直接用Thread相比,有什么优势?我简单总结了以下几点: 减小线程创建和销毁带来的消耗 对于Java Thread的实现,我在前面的一篇blog中进行了分析.Java Thread与内

  • 详解Java中数组判断元素存在几种方式比较

    1. 通过将数组转换成List,然后使用List中的contains进行判断其是否存在 public static boolean useList(String[] arr,String containValue){ return Arrays.asList(arr).contains(containValue); } 需要注意的是Arrays.asList这个方法中转换的List并不是java.util.ArrayList而是java.util.Arrays.ArrayList,其中java.

  • 详解Java中Math.round()的取整规则

    做Java的面试题时遇到了以下这题,百度了一下Math.round()的修约规则,有的说是四舍五入,有的说是四舍六入,发现和我学分析化学时用的数字修约规则(四舍六入五成双)很像,所以验证一下: 原题:Math.round(11.5) 等于多少?Math.round(-11.5)等于多少? 作者给的解题方法如下: 答:Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11.四舍五入的原理是在参数上加0.5然后进行下取整. 先说结论,题目作者给的解释是对的

  • 详解java中的阻塞队列

    阻塞队列简介 阻塞队列(BlockingQueue)首先是一个支持先进先出的队列,与普通的队列完全相同: 其次是一个支持阻塞操作的队列,即: 当队列满时,会阻塞执行插入操作的线程,直到队列不满. 当队列为空时,会阻塞执行获取操作的线程,直到队列不为空. 阻塞队列用在多线程的场景下,因此阻塞队列使用了锁机制来保证同步,这里使用的可重入锁: 而对于阻塞与唤醒机制则有与锁绑定的Condition实现 应用场景:生产者消费者模式 java中的阻塞队列 java中的阻塞队列根据容量可以分为有界队列和无界队

  • 详解JAVA中static的作用

    1.深度总结 引用一位网友的话,说的非常好,如果别人问你static的作用:如果你说静态修饰 类的属性 和 类的方法 别人认为你是合格的:如果是说 可以构成 静态代码块,那别人认为你还可以: 如果你说可以构成 静态内部类, 那别人认为你不错:如果你说了静态导包,那别人认为你很OK: 那我们就先在这几方面一一对static进行总结:然后说一些模糊的地方,以及一些面试中容易问道的地方: 1)static方法 static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方

随机推荐