多模式匹配--AC自动机及JAVA实现

AC自动机是一种基于trie树的多模式匹配算法,下面我们将介绍AC自动机的构建,如何利用AC自动机进行模式匹配,以及一个基于java的demo。

AC自动机的构建

构建AC自动机分为:构建trie树 + 构建fail指针:

构建trie树

给定一些字符串,构建trie树的原则就是:遍历一个字符串,首字符从root开始,如果root的子节点里有这个字符,就走到这个子节点的位置,否则就在root下建一个相应的子节点,并移动到这个节点的位置。然后再看下一个字符,并重复上述步骤:如果存在这样的子节点,就走到这个子节点,否则就新建这样一个子节点并走到新建节点的位置。

例如,我们有she , he ,say, her, shr四个字符串,就可以建如下trie树:
在这里插入图片描述

构建fail指针

其实单独一个trie树,或称前缀树,也可以做模式匹配,方式跟构建trie树类似,但是当它在某一个节点处找不到能够继续匹配的子节点时,就表示以当前字符开头的所有字符串都匹配失败,需要将起始字符向后挪一位,并重新从root开始匹配。因此trie树一次只能匹配一个pattern,并不能像AC自动机一样一次匹配多个pattern。

从上面不难发现,由于trie树的特点是相同前缀的词共用前面的节点,因此它可以避免相同前缀pattern的重复匹配,但是对于相同后缀的pattern,它却无法识别,只能从头再开始匹配。例如对于上一节建好的trie树,如果要匹配字符串"sher"。那么要匹配到所有符合的pattern:[she,her,he],它至少需要从root开始匹配两次。一次走左边,匹配到"he,her"(因为有相同前缀,所以一轮就可以同时匹配到两个);另一次走右边,匹配到"she"。

而AC自动机里面构建fail指针的目的就是为了同时利用相同后缀的信息,减少匹配次数。

Fail Next的构造:

Next的定义: 当匹配下一个节点失败时, 模式串应该跳到哪个节点继续匹配.

初始值: Root的Next为空, 第一层的Next都为Root

计算某节点的Next: 取此节点的父节点的Next为Node,
1.若Node中编号index的子节点存在, 则此子节点就是Next
2.若不存在, 那么再将Node的Next设为Node, 继续刚才的逻辑
3.若Node的Next为空, 则以此Node为Next (此时这个Node应当为Root)

对整个Trie树的next赋值必须以广度遍历的方式进行, 因为每一个next的计算, 要基于上层已经设置的next.

这样的话,之前构建的trie树的fail next就应该如下图所示:
在这里插入图片描述

利用AC自动机做匹配

还是用上面那个例子,如果我们要查询"sher"这个字符串匹配了哪些pattern,首先从root右边开始往下,走过"s -> h -> e"之后,由于e是尾节点,因此匹配到pattern “she”;这时应该继续从e的子节点里面找"r"的,但是没有找到,因此这时我们启用e的fail指针,指向的是左边"h -> e -> r"里面的e,而这个e同时也是一个尾节点,因此匹配到了"he",这时再从这个e的子节点里面找r,就找到了!所以又匹配到了"her"。这样我们一轮匹配就找到了"sher"所有匹配的pattern。


AC自动机实现(JAVA)

AC自动机的实现与上述步骤基本相同,有一点细节上的不同是,每一个节点都会有一个String List,里面放的是这个节点处的pattern string。而pattern string由两部分构成,一部分是由于这个位置是尾节点带来的,例如上述trie树中,“s -> h -> e"里面,最后的e就是尾节点,那么它的String List里面就有一个"she”;另一部分是由fail指针带来的,节点会继承它的fail指针指向的节点上的所有pattern string,例如对"s -> h -> e"里的e,它的fail指针指向的e处有pattern string [“he”],因此"s -> h -> e"里的e的String List就是 [“she”, “he”]。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Collection;
import java.util.Set;
import java.util.Collections;
import java.util.Queue;
import java.util.LinkedList;

public class ACTrie {

    private Boolean failureStatesConstructed = false;   //是否建立了failure表
    private Node root;                                  //根结点


    public ACTrie() {
        this.root = new Node(true);
    }

    private static class Node{
        private Map<Character, Node> map;   //用于放这个Node的所有子节点,储存形式是:Map(char, Node)
        private List<String> PattenStrings; //该节点处包含的所有pattern string
        private Node failure;               //fail指针指向的node
        private Boolean isRoot = false;     //是否为根结点


        public Node(){
            map = new HashMap<>();
            PattenStrings = new ArrayList<>();
        }


        public Node(Boolean isRoot) {
            this();
            this.isRoot = isRoot;
        }


        //用于build trie,如果一个字符character存在于子节点中,不做任何操作,返回这个节点的node
        //否则,建一个node,并将map(char,node)添加到当前节点的子节点里,并返回这个node
        public Node insert(Character character) {
            Node node = this.map.get(character);
            if (node == null) {
                node = new Node();
                map.put(character, node);
            }
            return node;
        }


        public void addPattenString(String keyword) {
            PattenStrings.add(keyword);
        }


        public void addPattenString(Collection<String> keywords) {
            PattenStrings.addAll(keywords);
        }


        public Node find(Character character) {
            return map.get(character);
        }


        /**
         * 利用父节点fail node来寻找子节点的fail node
         * or 
         * parseText时找下一个匹配的node
         */
        private Node nextState(Character transition) {
            //用于构建fail node时,这里的this是父节点的fail node
            //首先从父节点的fail node的子节点里找有没有值和当前失败节点的char值相同的
            Node state = this.find(transition);

            //如果找到了这样的节点,那么该节点就是当前失败位置节点的fail node
            if (state != null) {
                return state;
            }

            //如果没有找到这样的节点,而父节点的fail node又是root,那么返回root作为当前失败位置节点的fail node
            if (this.isRoot) {
                return this;
            }

            //如果上述两种情况都不满足,那么就对父节点的fail node的fail node再重复上述过程,直到找到为止
            //这个地方借鉴了KMP算法里面求解next列表的思想
            return this.failure.nextState(transition);
        }


        public Collection<Node> children() {
            return this.map.values();
        }


        public void setFailure(Node node) {
            failure = node;
        }


        public Node getFailure() {
            return failure;
        }


        //返回一个Node的所有子节点的键值,也就是这个子节点上储存的char
        public Set<Character> getTransitions() {
            return map.keySet();
        }


        public Collection<String> PattenString() {
            return this.PattenStrings == null ? Collections.<String>emptyList() : this.PattenStrings;
        }
    }


    private static class Patten_String{
        private final String keyword;   //匹配到的模式串
        private final int start;        //起点
        private final int end;          //终点

        public Patten_String(final int start, final int end, final String keyword) {
            this.start = start;
            this.end = end;
            this.keyword = keyword;
        }

        public String getKeyword() {
            return this.keyword;
        }

        @Override
        public String toString() {
            return super.toString() + "=" + this.keyword;
        }
    }



    /**
     * 添加一个模式串(内部使用字典树构建)
     */
    public void addKeyword(String keyword) {
        if (keyword == null || keyword.length() == 0) {
            return;
        }
        Node currentState = this.root;
        for (Character character : keyword.toCharArray()) {
            //如果char已经在子节点里,返回这个节点的node;否则建一个node,并将map(char,node)加到子节点里去
            currentState = currentState.insert(character);
        }
        //在每一个尾节点处,将从root到尾节点的整个string添加到这个叶节点的PattenString里
        currentState.addPattenString(keyword);
    }



    /**
     * 用ac自动机做匹配,返回text里包含的pattern及其在text里的起始位置
     */
    public Collection<Patten_String> parseText(String text) {
        //首先构建 fail表,如已构建则跳过
        checkForConstructedFailureStates();     
                 
        Node currentState = this.root;
        List<Patten_String> collectedPattenStrings = new ArrayList<>();
        for (int position = 0; position < text.length(); position++) {
            Character character = text.charAt(position);
            //依次从子节点里找char,如果子节点没找到,就到子节点的fail node找,并返回最后找到的node;如果找不到就会返回root
            //这一步同时也在更新currentState,如果找到了就更新currentState为找到的node,没找到currentState就更新为root,相当于又从头开始找
            currentState = currentState.nextState(character);
            Collection<String> PattenStrings = currentState.PattenString();
            if (PattenStrings == null || PattenStrings.isEmpty()) {
                continue;
            }
            //如果找到的node的PattenString非空,说明有pattern被匹配到了
            for (String PattenString : PattenStrings) {
                collectedPattenStrings.add(new Patten_String(position - PattenString.length() + 1, position, PattenString));
            }
        }
        return collectedPattenStrings;//返回匹配到的所有pattern
    }



    /**
     * 建立Fail表(核心,BFS遍历)
     */
    private void constructFailureStates() {
        Queue<Node> queue = new LinkedList<>();

        //首先从把root的子节点的fail node全设为root
        //然后将root的所有子节点加到queue里面
        for (Node depthOneState : this.root.children()) {
            depthOneState.setFailure(this.root);
            queue.add(depthOneState);
        }
        this.failureStatesConstructed = true;

        while (!queue.isEmpty()) {
            Node parentNode = queue.poll();
            //下面给parentNode的所有子节点找fail node
            for (Character transition : parentNode.getTransitions()) {           //transition是父节点的子节点的char
                Node childNode = parentNode.find(transition);                    //childNode是子节点中对应上面char值的节点的Node值
                queue.add(childNode);                                            //将这个parentNode的所有子节点加入queue,在parentNode的所有兄弟节点都过了一遍之后,就会过这些再下一层的节点
                Node failNode = parentNode.getFailure().nextState(transition);   //利用父节点的fail node来构建子节点的fail node
                childNode.setFailure(failNode);

                //每个节点处的PattenString要加上它的fail node处的PattenString
                //因为能匹配到这个位置的话,那么fail node处的PattenString一定是匹配的pattern
                childNode.addPattenString(failNode.PattenString());
            }
        }
    }



    /**
     *  检查是否建立了Fail表(若没建立,则建立)
     */
    private void checkForConstructedFailureStates() {
        if (!this.failureStatesConstructed) {
            constructFailureStates();
        }
    }


    public static void main(String[] args) {
        ACTrie trie = new ACTrie();
        trie.addKeyword("导航");
        trie.addKeyword("酒店");
        trie.addKeyword("希尔顿酒店");

        //匹配text,并返回匹配到的pattern
        Collection<Patten_String> PattenStrings = trie.parseText("导航到附近的希尔顿酒店");
        for (Patten_String PattenString : PattenStrings) {
            System.out.println(PattenString.start + " " + PattenString.end + "\t" + PattenString.getKeyword());
        }
    }
}


Reference:
AC自动机算法详解 (转载)
AC自动机详解及实现

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值