返回顶部
首页 > 资讯 > 后端开发 > Python >详解Java中AC自动机的原理与实现
  • 575
分享到

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

2024-04-02 19:04:59 575人浏览 八月长安

Python 官方文档:入门教程 => 点击学习

摘要

目录简介工作过程数据结构初始化构建字典树构建失败指针匹配执行结果简介 AC自动机是一个多模式匹配算法,在模式匹配领域被广泛应用,举一个经典的例子,违禁词查找并替换为***。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节点开始进行匹配,如果字符与节点值匹配,则判断是否为尾字符,如果是匹配上一个违禁词,记录下来,如果不匹配则转移到失败指针继续进行匹配。


   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自动机内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: 详解Java中AC自动机的原理与实现

本文链接: https://lsjlt.com/news/148780.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 详解Java中AC自动机的原理与实现
    目录简介工作过程数据结构初始化构建字典树构建失败指针匹配执行结果简介 AC自动机是一个多模式匹配算法,在模式匹配领域被广泛应用,举一个经典的例子,违禁词查找并替换为***。AC自动机...
    99+
    2024-04-02
  • java编程之AC自动机工作原理的示例分析
    这篇文章将为大家详细讲解有关java编程之AC自动机工作原理的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。1.应用场景—多模字符串匹配我们现在考虑这样一个问题,在一个文本串text中,我们想找出...
    99+
    2023-05-30
    java
  • Java数据结构之AC自动机算法的实现
    目录1 概念和原理2 节点定义3 构建Trie前缀树4 构建fail失配指针5 匹配文本6 案例演示7 总结1 概念和原理 一般的字符串匹配算法都是匹配一个子串,例如KMP、Trie...
    99+
    2022-12-08
    Java AC自动机算法 Java AC自动机
  • JAVA Spring Boot 自动配置实现原理详解
    目录引言主启动类的注解@SpringBootApplication1、@SpringBootConfiguration2、@ComponentScan3、@EnableAutoCon...
    99+
    2024-04-02
  • Java数据结构之AC自动机算法如何实现
    本篇内容主要讲解“Java数据结构之AC自动机算法如何实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java数据结构之AC自动机算法如何实现”吧!1 概念和原理一般的字符串匹配算法都是匹配一...
    99+
    2023-07-04
  • Java中Prime算法的原理与实现详解
    目录Prim算法介绍1.点睛2.算法介绍3. 算法步骤4.图解Prime 算法实现1.构建后的图2.代码3.测试Prim算法介绍 1.点睛 在生成树的过程中,把已经在生成树中的节点看...
    99+
    2024-04-02
  • Java实现JDK动态代理的原理详解
    目录概念案例静态代理JDK动态代理模式原理分析真相大白概念 代理:为控制A对象,而创建出新B对象,由B对象代替执行A对象所有操作,称之为代理。一个代理体系建立涉及到3个参与角色:真实...
    99+
    2024-04-02
  • Java SpringBoot自动装配原理详解
    目录自动装配的含义springboot应用程序启动类总结自动装配的含义 在SpringBoot程序main方法中,添加@SpringBootApplication或者@EnableA...
    99+
    2024-04-02
  • 详解Java ReentrantReadWriteLock读写锁的原理与实现
    目录概述原理概述加锁原理图解过程源码解析解锁原理图解过程源码解析概述 ReentrantReadWriteLock读写锁是使用AQS的集大成者,用了独占模式和共享模式。本文和大家一起...
    99+
    2022-11-13
    Java ReentrantReadWriteLock读写锁 Java ReentrantReadWriteLock Java 读写锁
  • 详解Java Synchronized的实现原理
    目录SynchronizedSynchronized的使用方式Synchronized的底层实现1.Java对象头2.Monitor3.线程状态流转在Monitor上体现Synchr...
    99+
    2024-04-02
  • 详解RSA加密算法的原理与Java实现
    目录对称加密和非对称加密RSA加密是什么RSA的加密过程前几天阿粉刚刚说了这个 MD5 加密的前世今生,因为 MD5 也确实用的人不是很多了,阿粉就不再继续的一一赘述了,今天阿粉想给...
    99+
    2022-11-13
    Java RSA加密算法 Java RSA加密 Java RSA
  • 详解DES加密算法的原理与Java实现
    目录DES加密算法DES加密原理DES 加密算法Java实现前面阿粉说了关于 MD5 加密算法,还有 RSA 加密算法的实现,以及他们的前世今生,今天阿粉在来说一下这个关于 DES ...
    99+
    2022-11-13
    Java DES加密算法 Java DES加密 Java DES
  • SpringBoot原理之自动配置机制详解
    目录前言 Spring配置类 SpringBoot自动配置 自动配置的概念 自动配置的运行机制 加载方式 SpringFactoriesLoader机制 SpringFactorie...
    99+
    2024-04-02
  • Java注解机制之Spring自动装配实现原理的示例分析
    小编给大家分享一下Java注解机制之Spring自动装配实现原理的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧! Java中使用注解的情况主要在SpringMVC(Spring Boot等),注解实际上相...
    99+
    2023-05-31
    java spring
  • java的Builder原理和实现详解
    首先给一个简单的Builder设计模式的例子: 主实现类代码如下: public class CompanyClient { public String company...
    99+
    2024-04-02
  • android开机自启动原理与实现案例(附源码)
    原理: Android系统通过应用程序自行在系统中登记注册事件(即Intent)来响应系统产生的各类消息。 Android系统为应用程序管理功能提供了大量的API,通过配置In...
    99+
    2022-06-06
    启动 源码 Android
  • PHP核心的运行机制与实现原理详解
    PHP是一种流行的开源服务器端脚本语言,大量被用于Web开发。它能够处理动态数据以及控制HTML的输出,但是,如何实现这一切?那么,本文将会介绍PHP的核心运行机制和实现原理,并利用具体的代码示例,进一步说明其运行过程。PHP源码解读PHP...
    99+
    2023-11-08
    实现原理 运行机制 PHP核心
  • 详解Java单例模式的实现与原理剖析
    目录一、什么是单例模式二、哪些地方用到了单例模式三、单例模式的优缺点优点缺点四、手写单例模式饿汉式枚举饿汉式DCL懒汉式双检锁懒汉式内部类懒汉式小结一、什么是单例模式 单例模式(Si...
    99+
    2024-04-02
  • 详解Java中跳跃表的原理和实现
    目录一、跳跃表的引入二、算法分析1.时间复杂度2.空间复杂度三、跳跃表介绍四、跳跃表的实现1.数据结构定义2.查找3.插入4.删除五、实战1.代码2.测试结果一、跳跃表的引入 对有序...
    99+
    2022-12-27
    Java实现跳跃表 Java跳跃表原理 Java跳跃表
  • Java NIO Buffer实现原理详解
    目录1、Buffer的继承体系2、Buffer的操作API使用案例3、Buffer的基本原理4、allocate方法初始化一个指定容量大小的缓冲区5、slice方法缓冲区分片6、只读...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作