之前的Trie树
,DBTrie
都属于前缀树,虽然DAT每次状态转移的时间复杂度都是常数,但全切分长度为n的文本时,时间复杂度为O(n2)。这是因为扫描过程中需要不断的挪动起点,发起新的查询。所以说,DAT的全切分复杂度为O(n2)。
显然,前缀树的短板是扫描,查询一个句子时,前缀树需要不断的挪动起点,发起新查询,这个过程浪费了大量时间。
举个栗子,扫描"清华大学"这个短语,算法以"清"为起点扫描"清",“清华”,“清华大”,“清华大学”,之后再回退到"华",继续扫描"华",“华大”…
如果能够在扫描到"清华大学"的同时想办法知道,“华大学”,“大学”,"学"在不在字典树中,那么就可以省略掉这三次查询,观察一下这三个字符串,它们共享递进式的后缀,首尾对调后(“学”,“学大”,“学大华”)恰好可以用另一颗前缀树索引,称它为后缀树。
AC(Aho-Corasick)自动机
的原理就是在前缀树的基础上,为前缀树上的每个节点建立一颗后缀树,从而节省了大量查询。这使得每次扫描由原来的O(n2)降到了O(n),AC自动机
现在被广泛的用于多字符串匹配
。
整体流程如下:
sucess表
的本质是前缀树,所以构建不再赘述,唯一不同的是,根节点不光可以按第一层的节点转移(比如像图中的h和s),还可以接受其它字符,转移终点都是自己。下面是构建代码:
/**
* @author: Ragty
* @Date: 2020/4/1 14:22
* @Description: success跳转
*/
public Node find(Character character) {
return map.get(character);
}
/**
* @author: Ragty
* @Date: 2020/4/1 14:23
* @Description: 状态转移(此处的transition为转移的状态,可理解为接收的一个词)
*/
private Node nextState(Character transition) {
Node state = this.find(transition); //先按success跳转
if (state != null) {
return state;
}
if (this.isRoot) { //如果跳转到根结点还是失败,则返回根结点
return this;
}
return this.failure.nextState(transition); // 跳转失败,按failure跳转
}
Fail表
保存的是状态(节点)间的一对一的关系,存储状态转移失败后应当回退的最佳状态(敲黑板,看下面的实例讲解!!!)。
以图为例,匹配she之后到达状态5,再来一个字符,状态转移失败,此时,最长后缀为he,对应路径为0-1-2。因此,状态2是状态5 fail的最佳选择,fail到状态2之后,做好了接受r的准备。
再比如,匹配his后到达状态7,此时his的最长后缀为is,但是途中没有找到is的路径,于是找次长后缀s,对应路径为0-3,因此状态7的最佳fail为3。
下面是构建方法:
下面是具体的构建代码:
/**
* @author: Ragty
* @Date: 2020/4/1 16:04
* @Description: 建立Fail表(核心,BFS遍历)
*/
private void constructFailureStates() {
Queue<Node> queue = new LinkedList<>();
for (Node depthOneState : this.root.children()) {
depthOneState.setFailure(this.root);
queue.add(depthOneState);
}
this.failureStatesConstructed = true;
while (!queue.isEmpty()) {
Node parentNode = queue.poll();
for (Character transition : parentNode.getTransitions()) {
Node childNode = parentNode.find(transition);
queue.add(childNode);
Node failNode = parentNode.getFailure().nextState(transition); //在这里构建failNode
childNode.setFailure(failNode);
childNode.addEmit(failNode.emit()); //用路径后缀构建output表
}
}
}
output表
用来记录命中的模式串,output表中的元素有两种:
所以output表的构造也分为两步:
下面是构建代码:
/**
* @author: Ragty
* @Date: 2020/4/1 15:10
* @Description: 添加一个模式串(内部使用字典树构建)
*/
public void addKeyword(String keyword) {
if (keyword == null || keyword.length() == 0) {
return;
}
Node currentState = this.root;
for (Character character : keyword.toCharArray()) {
currentState = currentState.insert(character);
}
currentState.addEmit(keyword);
}
模式匹配实现的功能是,输入一段文本,输出AC自动机中所有匹配的词,下面是实现代码:
/**
* @author: Ragty
* @Date: 2020/4/1 17:43
* @Description: 模式匹配
*/
public Collection<Emit> parseText(String text) {
checkForConstructedFailureStates();
Node currentState = this.root;
List<Emit> collectedEmits = new ArrayList<>();
for (int position = 0; position < text.length(); position++) {
Character character = text.charAt(position);
currentState = currentState.nextState(character);
Collection<String> emits = currentState.emit();
if (emits == null || emits.isEmpty()) {
continue;
}
for (String emit : emits) {
collectedEmits.add(new Emit(position - emit.length() + 1, position, emit));
}
}
return collectedEmits;
}
public static void main(String[] args) {
AhoCorasickTrie trie = new AhoCorasickTrie();
trie.addKeyword("hers");
trie.addKeyword("his");
trie.addKeyword("she");
trie.addKeyword("he");
Collection<Emit> emits = trie.parseText("ushers");
for (Emit emit : emits) {
System.out.println(emit.start + " " + emit.end + "\t" + emit.getKeyword());
}
}
输入文本为"ushers"时,输出结果为:
1 3 she
2 3 he
2 5 hers
基于双数组字典树的AC自动机会进一步优化,结构上只需将原来的success表的构建由Trie树替换为DATrie,效果上与双数组字典树不相上下,原因为:
总结一下,当含有短模式串时,优先用双数组字典树,否则优先使用基于双数组字典树的AC自动机
public class AhoCorasickTrie {
private Boolean failureStatesConstructed = false; //是否建立了failure表
private Node root; //根结点
/**
* @author: Ragty
* @Date: 2020/4/1 13:49
* @Description: ACTire初始化
*/
public AhoCorasickTrie() {
this.root = new Node(true);
}
/**
* @author: Ragty
* @Date: 2020/4/1 13:54
* @Description: ACTrie节点(内部用字典树构建)
*
*/
private static class Node{
private Map<Character, Node> map;
private List<String> emits; //输出
private Node failure; //失败中转
private Boolean isRoot = false; //是否为根结点
public Node(){
map = new HashMap<>();
emits = new ArrayList<>();
}
public Node(Boolean isRoot) {
this();
this.isRoot = isRoot;
}
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 addEmit(String keyword) {
emits.add(keyword);
}
public void addEmit(Collection<String> keywords) {
emits.addAll(keywords);
}
/**
* @author: Ragty
* @Date: 2020/4/1 14:22
* @Description: success跳转
*/
public Node find(Character character) {
return map.get(character);
}
/**
* @author: Ragty
* @Date: 2020/4/1 14:23
* @Description: 状态转移(此处的transition为转移的状态,可理解为接收的一个词)
*/
private Node nextState(Character transition) {
Node state = this.find(transition); //先按success跳转
if (state != null) {
return state;
}
if (this.isRoot) { //如果跳转到根结点还是失败,则返回根结点
return this;
}
return this.failure.nextState(transition); // 跳转失败,按failure跳转
}
public Collection<Node> children() {
return this.map.values();
}
public void setFailure(Node node) {
failure = node;
}
public Node getFailure() {
return failure;
}
public Set<Character> getTransitions() {
return map.keySet();
}
public Collection<String> emit() {
return this.emits == null ? Collections.<String>emptyList() : this.emits;
}
}
/**
* @author: Ragty
* @Date: 2020/4/1 15:01
* @Description: 模式串(用于模式串匹配)
*/
private static class Emit{
private final String keyword; //匹配到的模式串
private final int start; //起点
private final int end; //终点
public Emit(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;
}
}
/**
* @author: Ragty
* @Date: 2020/4/1 15:10
* @Description: 添加一个模式串(内部使用字典树构建)
*/
public void addKeyword(String keyword) {
if (keyword == null || keyword.length() == 0) {
return;
}
Node currentState = this.root;
for (Character character : keyword.toCharArray()) {
currentState = currentState.insert(character);
}
currentState.addEmit(keyword); //记录完整路径的output表(第一步)
}
/**
* @author: Ragty
* @Date: 2020/4/1 17:43
* @Description: 模式匹配
*/
public Collection<Emit> parseText(String text) {
checkForConstructedFailureStates();
Node currentState = this.root;
List<Emit> collectedEmits = new ArrayList<>();
for (int position = 0; position < text.length(); position++) {
Character character = text.charAt(position);
currentState = currentState.nextState(character);
Collection<String> emits = currentState.emit();
if (emits == null || emits.isEmpty()) {
continue;
}
for (String emit : emits) {
collectedEmits.add(new Emit(position - emit.length() + 1, position, emit));
}
}
return collectedEmits;
}
/**
* @author: Ragty
* @Date: 2020/4/1 16:04
* @Description: 建立Fail表(核心,BFS遍历)
*/
private void constructFailureStates() {
Queue<Node> queue = new LinkedList<>();
for (Node depthOneState : this.root.children()) {
depthOneState.setFailure(this.root);
queue.add(depthOneState);
}
this.failureStatesConstructed = true;
while (!queue.isEmpty()) {
Node parentNode = queue.poll();
for (Character transition : parentNode.getTransitions()) {
Node childNode = parentNode.find(transition);
queue.add(childNode);
Node failNode = parentNode.getFailure().nextState(transition); //在这里构建failNode
childNode.setFailure(failNode);
childNode.addEmit(failNode.emit()); //用路径后缀构建output表(第二步)
}
}
}
/**
* @author: Ragty
* @Date: 2020/4/1 15:28
* @Description: 检查是否建立了Fail表(若没建立,则建立)
*/
private void checkForConstructedFailureStates() {
if (!this.failureStatesConstructed) {
constructFailureStates();
}
}
public static void main(String[] args) {
AhoCorasickTrie trie = new AhoCorasickTrie();
trie.addKeyword("hers");
trie.addKeyword("his");
trie.addKeyword("she");
trie.addKeyword("he");
Collection<Emit> emits = trie.parseText("ushers");
for (Emit emit : emits) {
System.out.println(emit.start + " " + emit.end + "\t" + emit.getKeyword());
}
}
}