AC自动机-Java

简介

AC自动机算法主要依靠构造一个有限状态机(类似于在一个trie树中添加失配指针)来实现。这些额外的失配指针允许在查找字符串失败时进行回退(例如设Trie树的单词cat匹配失败,但是在Trie树中存在另一个单词cart,失配指针就会指向前缀ca),转向某前缀的其他分支,免于重复匹配前缀,提高算法效率。

构成

AC自动机是以Trie的结构为基础,结合KMP的思想建立的,其由Trie树+失配信息构成。
简单的来说建立AC自动机有两个步骤:

  • 基础的Trie结构:将所有的模式串构成一个Trie树。
  • KMP思想:对Trie树上所有的节点构造失配指针。

以{she,he,hers,his}为模式串。构建自动机。

Trie树的构建

在这里插入图片描述

失配指针fail

在这里插入图片描述

匹配过程

在这里插入图片描述以模式串{she,he,hers,his},文本“ushers”来简单的对匹配过程借助上图进行说明

  1. 输入字符何u不等于{s,h},则root结点不进行状态转移
  2. 输入字符s,从root结点转移到状态1结点
  3. 输入字符h,从状态1转移到状态2
  4. 输入字符e,从状态2转移到状态3,3号结点是关键字结束结点,则匹配到模式串she
  5. 输入字符r,状态3无法正常穿衣,则根据fail指针转移到状态5,5号结点是关键字结束结点,则匹配到模式串he
  6. 5号结点处理输入的r之后,转移到状态6结点
  7. 输入字符s,从状态6转移到状态7,7号节点是关键字结束结点,则匹配到模式串hers
  8. 文本输入结束,最终匹配到she、he、hers

Java代码

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("he");
        trie.addKeyword("she");
        trie.addKeyword("his");
        trie.addKeyword("hers");

        //匹配text,并返回匹配到的pattern
        Collection<Patten_String> pattenStrings = trie.parseText("ushers");
        for (Patten_String pattenString : pattenStrings) {
            System.out.println(pattenString.start + " " + pattenString.end + "\t" + pattenString.getKeyword());
        }
    }
}
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值