雙陣列字典樹能在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