文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2023-07-04 17:11

关注

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

1 概念和原理

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

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

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

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

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

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

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

2 节点定义

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

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。

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指向匹配节点,获取该节点继续判断:

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自动机算法如何实现”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯