双数组字典树能在O(1)(1是模式串长度)时间内高速完成单串匹配,并且内存消耗可控,然而软肋在于多模式匹配。如果要匹配多个模式串,必须先实现前缀查询,然后频繁截取文本后缀才可多匹配。比如 ushers、shers、hers…这样一份文本要回退扫描多遍,性能较低。既然 AC 自动机的goto表本身就是一棵字典树,能否利用双数组字典树来实现它呢?如果能用双数组字典树表达 AC自动机,就能集合两者的优点,得到一种近乎完美的数据结构。
ACDAT的基本原理是替换 AC自动机 的goto表,也可看作为一棵双数组字典树的每个状态(下标)附上额外的信息。上节提到, AC自动机 的goto表就是字典树,只不过 AC自动机 比字典树多了output 表和fail表。那么ACDAT的构建原理就是为每个状态(base[i]和check[i])构建output[i][]和fail[i]。具体说来,分为3步。
- 构建trie树,让终止节点记住对应模式串的字典序。
即将所有模式串构建为一颗字典树,同时将终止状态绑定外部value。在实现上可以先用TreeMap简单实现。
- 构建双数组字典树,在将每个状态映射到双数组时,让它记住自己在双数组中的下标。
与单独构建双数组Trie树不同,在为一个trie树State创建base[i]的时候,让该State记住自己的i,这样就建立State和下标的映射。
- 构建AC自动机,此时fail表中存储的就是状态的下标。
在构建AC自动机时,每构建一个节点State的fail表,就利用上述映射下标State.id将fail[id]设为failState.id。对于output表,也是同理。
返回所有匹配到的模式串
/** * 匹配母文本 * * @param text 一些文本 * @return 一个pair列表 */ public List<Hit<V>> parseText(String text)
其中Hit是一个表示命中结果的结构:
/** * 一个命中结果 * * @param <V> */ public class Hit<V> { /** * 模式串在母文本中的起始位置 */ public final int begin; /** * 模式串在母文本中的终止位置 */ public final int end; /** * 模式串对应的值 */ public final V value; }
即时处理
AhoCorasickDoubleArrayTrie提供即时处理的结构:
/** * 处理文本 * * @param text 文本 * @param processor 处理器 */ public void parseText(String text, IHit<V> processor)
其中IHit<V>
是一个轻便的接口:
/** * 命中一个模式串的处理方法 */ public interface IHit<V> { /** * 命中一个模式串 * * @param begin 模式串在母文本中的起始位置 * @param end 模式串在母文本中的终止位置 * @param value 模式串对应的值 */ void hit(int begin, int end, V value); }
调用方法
import com.hankcs.hanlp.collection.AhoCorasick.AhoCorasickDoubleArrayTrie; import com.hankcs.hanlp.dictionary.CoreDictionary; import com.hankcs.hanlp.utility.LexiconUtility; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.TreeMap; public static void main(String[] args) throws IOException { TreeMap<String, String> map = new TreeMap<>(); String[] keyArray = new String[] { "清华", "清华大学", "清新", "中华", "华人" }; for (String key : keyArray) { map.put(key, key); } AhoCorasickDoubleArrayTrie<String> act = new AhoCorasickDoubleArrayTrie<>(); act.build(map); act.parseText("清华大学生都是华人", new AhoCorasickDoubleArrayTrie.IHit<String>() { @Override public void hit(int begin, int end, String value) { System.out.printf("[%d:%d]=%s\n", begin, end, value); } }); }
输出:
[0:2]=清华 [0:4]=清华大学 [7:9]=华人
单独的AhoCorasickDoubleArrayTrie类库:https://github.com/hankcs/AhoCorasickDoubleArrayTrie