广告
返回顶部
首页 > 资讯 > 精选 >Java数据结构之AC自动机算法如何实现
  • 493
分享到

Java数据结构之AC自动机算法如何实现

2023-07-04 17:07:22 493人浏览 独家记忆
摘要

本篇内容主要讲解“Java数据结构之AC自动机算法如何实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java数据结构之AC自动机算法如何实现”吧!1 概念和原理一般的字符串匹配算法都是匹配一

本篇内容主要讲解“Java数据结构之AC自动机算法如何实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java数据结构之AC自动机算法如何实现”吧!

1 概念和原理

一般的字符串匹配算法都是匹配一个子串,例如KMP、Trie,那么如果同时匹配多个子串呢?此时就需要用到AC自动机了。

AC自动机算法是一个多模式字符串匹配算法,在模式匹配领域被广泛应用,例如违禁词查找替换、搜索关键词查找等等。

关于Trie树和KMP算法,我们此前已经讲解过了:

  • 前缀树Trie的实现原理以及Java代码的实现

  • KMP算法详解以及Java代码实现

AC自动机算法常被认为是Trie树+KMP算法的结合体,为什么呢?我们先看看它的构建步骤:

  • 对所有的关键词构建Trie前缀树。

  • 为Trie树上的所有节点构建fail失配指针。

第一步,对所有的关键词构建Trie前缀树。这一步利用Trie的特点构建快速前缀查找结构,trie树的特点是可以从字符串头部开始匹配,并且相同前缀的词共用前面的节点,因此它可以避免相同前缀pattern的重复匹配,但是对于相同的后缀无能为力。

第二步,为Trie树上的所有节点构建fail失配指针,即匹配失败后应该跳到哪个节点。所谓节点的失配指针,就是指向当前字符串最长真后缀位置的指针节点。这里需要理解KMP的next数组的概念,这一步就是利用KMP前后缀匹配的思想实现关键词前缀失配时利用相同的后缀信息快速跳转到另一个关键词继续前缀匹配。他们的区别是:

  • 在KMP算法中,是针对单个关键词匹配,求出的最长匹配长度的前缀和后缀都位于同一个关键词内。例如关键词abcdabc,最长匹配前后缀,为abc,他们属于该关键词。

  • 在AC自动机算法中,是针对多个关键词匹配,求出的最长匹配长度的前缀和后缀分别属于不同的关键词的前缀和后缀。

例如两个关键词dabab,ababd,那么关键词dabab中第二个b位置的失败指针应该指向关键词ababd中的第二个b。即dabab的后缀与ababd的前缀能够匹配的最长子串为abab。

2 节点定义

在这里,我们给出一个比较简单的节点的定义。

  • next,表示经过该节点的模式串的下层节点,这是Trie树结构的保证,存储着子节点的值到对应的节点的映射关系。

  • depth,表示以当前即诶单结尾的模式串的长度,也是节点的深度,默认为0。

  • failure,失配指针,其指向表示另一个关键词前缀的最长后缀节点,如果当前节点没有匹配到,则跳转到此节点继续匹配。如果当前节点匹配到了,那么可以通过此指针找到该节点的模式串包含的最长后缀模式串。

class Acnode {        Map<Character, AcNode> next = new HashMap<>();        int depth;        AcNode failure;    public boolean hashNext(char nexTKEy) {        return next.containsKey(nextKey);    }    public AcNode getNext(char nextKey) {        return next.get(nextKey);    }}

3 构建Trie前缀树

构建Ac自动机的Trie的方法和构建普通Trie的方法几乎一致。在添加每个模式串成功后,会为最后一个节点的depth赋值为当前模式串的长度。

private AcNode root;public void insert(String Word) {    AcNode cur = root;    for (char c : word.toCharArray()) {        if (!cur.next.containsKey(c)) {            cur.next.put(c, new AcNode());        }        cur = cur.next.get(c);    }    cur.depth = word.length();}

4 构建fail失配指针

构建fail失配指针的一种常见的方法如下,实际上是一个BFS层序遍历的算法:

Trie的root节点没有失配指针,或者说失配指针为null,其他节点都有失配指针,或者说不为null。

遍历root节点的所有下一层直接子节点,将它们的失配指针设置为root。因为这些节点代表着所有模式串的第一个字符,基于KMP的next数组定义,单个字符没有最长真后缀,此时直接指向root。

继续循环向下遍历每一层的子节点,由于bfs的遍历,那么上一层父节点的失配指针肯定都已经确定了。基于next数组的构建思想,子节点的失配指针可以通过父节点的是失配指针快速推导出来。设当前遍历的节点为c,它的父节点为p,父节点的失配指针为pf。

  • 如果pf节点的子节点对应的字符中,包含了当前节点的所表示的字符。那么基于求最长后缀的原理,此时c节点的失配指针可以直接指向pf节点下的相同字符对应的子节点。

  • 如果pf节点的子节点对应的字符中,没有包含了当前节点的所表示的字符。那么继续获取pf节点的失配指针节点,继续重复判断。直到满足第一种情况,或者pf指向了根节点,并且根节点的子节点也没有匹配,那么此时直接将c节点的失配指针指向根节点。

public void buildFailurePointer() {    ArrayDeque<AcNode> queue = new ArrayDeque<AcNode>();    //将所有root的直接子节点的failure设置为root,并且加入queue    for (AcNode acNode : root.next.values()) {        acNode.failure = root;        queue.addLast(acNode);    }    //bfs构建失配指针    while (!queue.isEmpty()) {        //父节点出队列        AcNode parent = queue.pollFirst();        //遍历父节点的下层子节点,基于父节点求子节点的失配指针        for (Map.Entry<Character, AcNode> characterAcNodeEntry : parent.next.entrySet()) {            //获取父节点的失配指针            AcNode pf = parent.failure;            //获取子节点            AcNode child = characterAcNodeEntry.getValue();            //获取子节点对应的字符            Character nextKey = characterAcNodeEntry.getKey();            //如果pf节点不为null,并且pf节点的子节点对应的字符中,没有包含了当前节点的所表示的字符            while (pf != null && !pf.hashNext(nextKey)) {                //继续获取pf节点的失配指针节点,继续重复判断                pf = pf.failure;            }            //pf为null,表示找到了根节点,并且根节点的子节点也没有匹配            if (pf == null) {                //此时直接将节点的失配指针指向根节点                child.failure = root;            }            //pf节点的子节点对应的字符中,包含了当前节点的所表示的字符            else {                //节点的失配指针可以直接指向pf节点下的相同字符对应的子节点                child.failure = pf.getNext(nextKey);            }            //最后不要忘了,将当前节点加入队列            queue.addLast(child);        }    }}

5 匹配文本

构建完AC自动机之后,下面我们需要进行文本的匹配,匹配的方式实际上比较简单。

遍历文本的每个字符,依次匹配,从Trie的根节点作为cur节点开始匹配:

将当前字符作为nextKey,如果cur节点不为null且节点的next映射中不包含nextKey,那么当前cur节点指向自己的failure失配指针。

如果cur节点为null,说明当前字符匹配到了root根节点且失败,那么cur设置为root继续从根节点开始进行下一轮匹配。

否则表示匹配成功的节点,cur指向匹配节点,获取该节点继续判断:

  • 如果该节点是某个关键词的结尾,那么取出来,也就是depth不为0,那么表示匹配到了一个关键词。

  • 继续判断该节点的失配指针节点表示的模式串。因为失配指针节点表示的模式串是当前匹配的模式串的在这些关键词中的最长后缀,且由于当前节点的路径包括了失配指针的全部路径,并且失配指针路径也是一个完整的关键词,需要找出来。

public List<ParseResult> parseText(String text) {    List<ParseResult> parseResults = new ArrayList<>();    char[] chars = text.toCharArray();    //从根节点开始匹配    AcNode cur = root;    //遍历字符串的每个字符    for (int i = 0; i < chars.length; i++) {        //当前字符        char nextKey = chars[i];        //如果cur不为null,并且当前节点的的子节点不包括当前字符,即不匹配        while (cur != null && !cur.hashNext(nextKey)) {            //那么通过失配指针转移到下一个节点继续匹配            cur = cur.failure;        }        //如果节点为null,说明当前字符匹配到了根节点且失败        //那么继续从根节点开始进行下一轮匹配        if (cur == null) {            cur = root;        } else {            //匹配成功的节点            cur = cur.getNext(nextKey);            //继续判断            AcNode temp = cur;            while (temp != null) {                //如果当前节点是某个关键词的结尾,那么取出来                if (temp.depth != 0) {                    int start = i - temp.depth + 1, end = i;                    parseResults.add(new ParseResult(start, end, new String(chars, start, temp.depth)));                    //System.out.println(start + " " + end + " " + new String(chars, start, temp.depth));                }                //继续判断该节点的失配指针节点                //因为失配指针节点表示的模式串是当前匹配的模式串的在这些关键词中的最长后缀,且由于当前节点的路径包括了失配指针的全部路径                //并且失配指针路径也是一个完整的关键词,需要找出来。                temp = temp.failure;            }        }    }    return parseResults;}class ParseResult {    int startIndex;    int endIndex;    String key;    public ParseResult(int startIndex, int endIndex, String key) {        this.startIndex = startIndex;        this.endIndex = endIndex;        this.key = key;    }    @Override    public String toString() {        return "{" +                "startIndex=" + startIndex +                ", endIndex=" + endIndex +                ", key='" + key + '\'' +                '}';    }}

6 案例演示

基于上面的方法,假如关键词为:he、shes、shers、hes、h、e,那么最终构建的AC自动机的结构如下(红色圈表示某个关键词的结束位置)。

Java数据结构之AC自动机算法如何实现

假如我们的文本为sheshe,那么我们来看看AC自动机匹配的过程:

遍历文本,cur首先指向root。

nextKey=s,cur.next包含s,表示这是一个匹配成功的节点,那么获取到该节点s,cur=s,s不是某个关键词的结尾,failure节点也不包含模式串,那么查找完毕进行下一轮。

nextKey=h,cur=s,cur.next包含h,表示这是一个匹配成功的节点,那么获取到该节点h,cur=h,h节点不是某个关键词的结尾,但是它的failure节点包含模式串h,因此找到了第1个匹配的关键词h,查找完毕后进行下一轮。

Java数据结构之AC自动机算法如何实现

nextKey=e,cur=h,cur.next包含e,表示这是一个匹配成功的节点,那么获取到该节点e,cur=e,e节点不是某个关键词的结尾,但是它的failure节点包含模式串he,因此找到了第2个匹配的关键词he。

而fuilure节点e又包含另一个模式串e,因此找到了第3个匹配的关键词e,查找完毕后进行下一轮。

Java数据结构之AC自动机算法如何实现

nextKey=s,cur=e,cur.next包含s,表示这是一个匹配成功的节点,那么获取到该节点e,cur=e,s节点是关键词shes的结尾,因此找到了第4个匹配的关键词shes。

继续判断s的failure,它的failure节点包含模式串hes,因此找到了第5个匹配的关键词hes,查找完毕后进行下一轮。

Java数据结构之AC自动机算法如何实现

nextKey=h,cur=s,cur.next不包含h,表示这是一个匹配失败的节点,那么继续匹配它的failure节点,经过s-s-s的匹配,最终匹配到子节点h。

Java数据结构之AC自动机算法如何实现

该节点h并不是关键词的结尾,但是h的failure,它的failure节点包含模式串h,因此找到了第6个匹配的关键词h,查找完毕后进行下一轮。

Java数据结构之AC自动机算法如何实现

nextKey=e,cur=h,cur.next包含e,表示这是一个匹配成功的节点,那么获取到该节点e,cur=e,e节点不是某个关键词的结尾,但是它的failure节点包含模式串he,因此找到了第7个匹配的关键词he。

而fuilure节点e又包含另一个模式串e,因此找到了第8个匹配的关键词e。

Java数据结构之AC自动机算法如何实现

到此字符串遍历完毕,查找完毕!

最终文本sheshe,匹配到关键词如下:

[{startIndex=1, endIndex=1, key='h'}, {startIndex=1, endIndex=2, key='he'}, 
{startIndex=2, endIndex=2, key='e'}, {startIndex=0, endIndex=3, key='shes'}, 
{startIndex=1, endIndex=3, key='hes'}, {startIndex=4, endIndex=4, key='h'}, 
{startIndex=4, endIndex=5, key='he'}, {startIndex=5, endIndex=5, key='e'}]

到此,相信大家对“Java数据结构之AC自动机算法如何实现”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

--结束END--

本文标题: Java数据结构之AC自动机算法如何实现

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

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

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • Java数据结构之AC自动机算法如何实现
    本篇内容主要讲解“Java数据结构之AC自动机算法如何实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java数据结构之AC自动机算法如何实现”吧!1 概念和原理一般的字符串匹配算法都是匹配一...
    99+
    2023-07-04
  • Java数据结构之AC自动机算法的实现
    目录1 概念和原理2 节点定义3 构建Trie前缀树4 构建fail失配指针5 匹配文本6 案例演示7 总结1 概念和原理 一般的字符串匹配算法都是匹配一个子串,例如KMP、Trie...
    99+
    2022-12-08
    Java AC自动机算法 Java AC自动机
  • Java数据结构之KMP算法的实现
    目录问题介绍暴力求解知识补充Next示例Next代码匹配示例匹配代码完整代码本次我们介绍数据结构中的KMP算法,我们会从下面几个角度来介绍: 问题介绍 首先我们先介绍适用于KMP算法...
    99+
    2022-11-21
    Java KMP算法 Java KMP
  • Java数据结构之KMP算法怎么实现
    这篇文章主要讲解了“Java数据结构之KMP算法怎么实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java数据结构之KMP算法怎么实现”吧!暴力匹配算法(Brute-Force,BF)这...
    99+
    2023-07-04
  • Java数据结构与算法系列精讲之哈希算法实现
    概述 从今天开始, 小白我将带大家开启 Java 数据结构 & 算法的新篇章. 获取哈希值 hashCode()方法可以返回一个对象的哈希值. 需要注意的是, 我们需要对值...
    99+
    2022-11-13
  • Java数据结构之常见排序算法怎么实现
    这篇文章主要介绍“Java数据结构之常见排序算法怎么实现”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Java数据结构之常见排序算法怎么实现”文章能帮助大家解决问题。注:后续所说的复杂度 log,都...
    99+
    2023-07-04
  • Java数据结构与算法之循环队列的实现
    目录概述循环队列循环队列实现改变队列大小enqueue 方法dequeue 方法main完整代码 概述 从今天开始, 小白我将带大家开启 Jave 数据结构 & 算法的新篇章...
    99+
    2022-11-12
  • java数据结构与算法之桶排序实现方法详解
    本文实例讲述了java数据结构与算法之桶排序实现方法。分享给大家供大家参考,具体如下:基本思想:假定输入是由一个随机过程产生的[0, M)区间上均匀分布的实数。将区间[0, M)划分为n个大小相等的子区间(桶),将n个输入元素分配到这些桶中...
    99+
    2023-05-31
    java 数据结构 算法
  • Java数据结构之KMP算法详解以及代码实现
    目录暴力匹配算法(Brute-Force,BF)概念和原理next数组KMP匹配KMP全匹配总结我们此前学了前缀树Trie的实现原理以及Java代码的实现。Trie树很好,但是它只能...
    99+
    2022-12-08
    Java 数据结构 KMP算法 Java KMP算法 Java KMP
  • Java编程算法:如何实现复杂数据结构?
    Java是一种高级编程语言,它有着强大的编程能力和广泛的应用范围。在Java中,数据结构是编程中最重要的概念之一,因为它能够帮助我们处理和组织数据,从而实现更高效的算法和程序。而对于复杂数据结构的实现,更是Java编程中的重要话题。本文将...
    99+
    2023-07-29
    编程算法 异步编程 path
  • Java数据结构之选择排序算法的实现与优化
    目录初识选择排序算法实现优化后的算法实现选择排序 VS 冒泡排序初识选择排序 算法思想[以升序为例]: 第一趟选择排序时,从第一个记录开始,通过n-1次关键字的比较,从第n个记录中选...
    99+
    2023-01-28
    Java实现选择排序算法 Java选择排序算法 Java选择排序
  • Java数据结构与算法之栈(动力节点Java学院整理)
    stack,中文翻译为堆栈,其实指的是栈,heap,堆。这里讲的是数据结构的栈,不是内存分配里面的堆和栈。栈是先进后出的数据的结构,好比你碟子一个一个堆起来,最后放的那个是堆在最上面的。队列就是排队买苹果,先去的那个可以先买。栈public...
    99+
    2023-05-31
    java 数据结构 算法
  • Java数据结构之栈与综合计算器的实现
    目录1.栈1.1 栈的简介1.2 使用数组模拟栈1.3 栈的测试2.综合计算器的实现2.1 需求简介2.2 详细思路及分步图解2.3 完整代码及测试1.栈 1.1 栈的简介 栈(st...
    99+
    2022-11-13
    Java 栈 综合计算器 Java 栈 Java 综合计算器
  • 如何理解Java数据结构与算法
    本篇内容介绍了“如何理解Java数据结构与算法”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!基本介绍鸿蒙官方战略合作共建——HarmonyO...
    99+
    2023-06-15
  • Java数据结构之双向链表如何实现
    这篇文章主要讲解了“Java数据结构之双向链表如何实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java数据结构之双向链表如何实现”吧!双向链表(Doubly linked list)什...
    99+
    2023-06-30
  • Java数据结构与算法实现递归与回溯
    目录1.什么是递归?2.代码案例一——迷宫问题3.代码案例二——八皇后问题1.什么是递归? 简单的说: 递归就是方法自己调用自己,每次...
    99+
    2022-11-13
  • Java数据结构与算法之选择排序(动力节点java学院整理)
    每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。代码public class ChoseSort { //constructor without parameters ...
    99+
    2023-05-31
    java 选择排序 数据结构
  • 【一起学数据结构与算法】Java实现双链表
    目录 一、双链表的概念二、双链表一些方法的实现2.1 双链表的属性2.2 打印双链表2.3 得到双链表的长度2.4 查找是否包含关键字key是否在双链表中2.5 头插法2.6 尾插法2.7 任...
    99+
    2023-09-04
    java 链表 数据结构
  • C++数据结构之AVL树如何实现
    这篇文章主要讲解了“C++数据结构之AVL树如何实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C++数据结构之AVL树如何实现”吧!1.概念(1)二叉搜索树的缺点要手撕AVL树,我们首先...
    99+
    2023-07-02
  • C语言数据结构与算法之队列的实现详解
    目录队列的概念及结构队列的实现Queue.hQueue.cTest.c队列的概念及结构 队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FI...
    99+
    2022-11-13
    C语言数据结构 队列 C语言 队列实现 C语言 队列
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作