背景

在做实际工作中,最简单也最常用的一种自然语言处理方法就是关键词匹配,例如我们要对n条文本进行过滤,那本身是一个过滤词表的,通常进行过滤的代码如下

for (String document : documents) {
for (String filterWord : filterWords) {
if (document.contains(filterWord)) {
//process ...
}
}
}

如果文本的数量是n,过滤词的数量是k,那么复杂度为O(nk);如果关键词的数量较多,那么支行效率是非常低的。

计算机科学中,Aho–Corasick算法是由Alfred V. Aho和Margaret J.Corasick 发明的字符串搜索算法,用于在输入的一串字符串中匹配有限组“字典”中的子串。它与普通字符串匹配的不同点在于同时与所有字典串进行匹配。算法均摊情况下具有近似于线性的时间复杂度,约为字符串的长度加所有匹配的数量。然而由于需要找到所有匹配数,如果每个子串互相匹配(如字典为a,aa,aaa,aaaa,输入的字符串为aaaa),算法的时间复杂度会近似于匹配的二次函数。

原理

在一般的情况下,针对一个文本进行关键词匹配,在匹配的过程中要与每个关键词一一进行计算。也就是说,每与一个关键词进行匹配,都要重新从文档的开始到结束进行扫描。AC自动机的思想是,在开始时先通过词表,对以下三种情况进行缓存:

  1. 按照字符转移成功进行跳转(success表)
  2. 按照字符转移失败进行跳转(fail表)
  3. 匹配成功输出表(output表)

因此在匹配的过程中,无需从新从文档的开始进行匹配,而是通过缓存直接进行跳转,从而实现近似于线性的时间复杂度。

构建

构建的过程分三个步骤,分别对success表,fail表,output表进行构建。其中output表在构建sucess和fail表进行都进行了补充。fail表是一对一的,output表是一对多的。

按照字符转移成功进行跳转(success表)

sucess表实际就是一棵trie树,构建的方式和trie树是一样的,这里就不赘述。

按照字符转移失败进行跳转(fail表)

设这个节点上的字母为C,沿着他父亲的失败指针走,直到走到一个节点,他的儿子中也有字母为C的节点。然后把当前节点的失败指针指向那个字母也为C的儿子。如果一直走到了root都没找到,那就把失败指针指向root。 使用广度优先搜索BFS,层次遍历节点来处理,每一个节点的失败路径。

匹配成功输出表(output表)

匹配

举例说明,按顺序先后添加关键词he,she,,his,hers。在匹配ushers过程中。先构建三个表,如下图,实线是sucess表,虚线是fail表,结点后的单词是ourput表。

代码

import java.util.*;

/**
*/
public class ACTrie {
private boolean failureStatesConstructed = false; //是否建立了failure表
private Node root; //根结点 public ACTrie() {
this.root = new Node(true);
} /**
* 添加一个模式串
* @param keyword
*/
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);
} /**
* 模式匹配
*
* @param text 待匹配的文本
* @return 匹配到的模式串
*/
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;
} /**
* 检查是否建立了failure表
*/
private void checkForConstructedFailureStates() {
if (!this.failureStatesConstructed) {
constructFailureStates();
}
} /**
* 建立failure表
*/
private void constructFailureStates() {
Queue<Node> queue = new LinkedList<>(); // 第一步,将深度为1的节点的failure设为根节点
//特殊处理:第二层要特殊处理,将这层中的节点的失败路径直接指向父节点(也就是根节点)。
for (Node depthOneState : this.root.children()) {
depthOneState.setFailure(this.root);
queue.add(depthOneState);
}
this.failureStatesConstructed = true; // 第二步,为深度 > 1 的节点建立failure表,这是一个bfs 广度优先遍历
/**
* 构造失败指针的过程概括起来就一句话:设这个节点上的字母为C,沿着他父亲的失败指针走,直到走到一个节点,他的儿子中也有字母为C的节点。
* 然后把当前节点的失败指针指向那个字母也为C的儿子。如果一直走到了root都没找到,那就把失败指针指向root。
* 使用广度优先搜索BFS,层次遍历节点来处理,每一个节点的失败路径。  
*/
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); childNode.setFailure(failNode);
childNode.addEmit(failNode.emit());
}
}
} 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);
} /**
* success跳转
* @param character
* @return
*/
public Node find(Character character) {
return map.get(character);
} /**
* 跳转到下一个状态
* @param transition 接受字符
* @return 跳转结果
*/
private Node nextState(Character transition) {
Node state = this.find(transition); // 先按success跳转
if (state != null) {
return state;
}
//如果跳转到根结点还是失败,则返回根结点
if (this.isRoot) {
return this;
}
// 跳转失败的话,按failure跳转
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;
} public Set<Character> getTransitions() {
return map.keySet();
} public Collection<String> emit() {
return this.emits == null ? Collections.<String>emptyList() : this.emits;
} } private static class Emit{ private final String keyword;//匹配到的模式串
private final int start;
private final int end; /**
* 构造一个模式串匹配结果
* @param start 起点
* @param end 重点
* @param keyword 模式串
*/
public Emit(final int start, final int end, final String keyword) {
this.start = start;
this.end = end;
this.keyword = keyword;
} /**
* 获取对应的模式串
* @return 模式串
*/
public String getKeyword() {
return this.keyword;
} @Override
public String toString() {
return super.toString() + "=" + this.keyword;
}
} public static void main(String[] args) {
ACTrie trie = new ACTrie();
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());
}
}
}

原始地址:http://www.thinkinglite.cn/

多模字符串匹配算法-Aho–Corasick的更多相关文章

  1. 多模字符串匹配算法之AC自动机—原理与实现

    简介: 本文是博主自身对AC自动机的原理的一些理解和看法,主要以举例的方式讲解,同时又配以相应的图片.代码实现部分也予以明确的注释,希望给大家不一样的感受.AC自动机主要用于多模式字符串的匹配,本质上 ...

  2. Aho - Corasick string matching algorithm

    Aho - Corasick string matching algorithm 俗称:多模式匹配算法,它是对 Knuth - Morris - pratt algorithm (单模式匹配算法) 形 ...

  3. 字符串匹配算法 - KMP

    前几日在微博上看到一则微博是说面试的时候让面试者写一个很简单的字符串匹配都写不出来,于是我就自己去试了一把.结果写出来的是一个最简单粗暴的算法.这里重新学习了一下几个经典的字符串匹配算法,写篇文章以巩 ...

  4. Boyer-Moore 字符串匹配算法

    字符串匹配问题的形式定义: 文本(Text)是一个长度为 n 的数组 T[1..n]: 模式(Pattern)是一个长度为 m 且 m≤n 的数组 P[1..m]: T 和 P 中的元素都属于有限的字 ...

  5. KMP单模快速字符串匹配算法

    KMP算法是由Knuth,Morris,Pratt共同提出的算法,专门用来解决模式串的匹配,无论目标序列和模式串是什么样子的,都可以在线性时间内完成,而且也不会发生退化,是一个非常优秀的算法,时间复杂 ...

  6. 字符串匹配算法之BF(Brute-Force)算法

    BF(Brute-Force)算法 蛮力搜索,比较简单的一种字符串匹配算法,在处理简单的数据时候就可以用这种算法,完全匹配,就是速度慢啊. 基本思想 从目标串s 的第一个字符起和模式串t的第一个字符进 ...

  7. 【原创】通俗易懂的讲解KMP算法(字符串匹配算法)及代码实现

    一.本文简介 本文的目的是简单明了的讲解KMP算法的思想及实现过程. 网上的文章的确有些杂乱,有的过浅,有的太深,希望本文对初学者是非常友好的. 其实KMP算法有一些改良版,这些是在理解KMP核心思想 ...

  8. 字符串匹配算法——KMP算法学习

    KMP算法是用来解决字符串的匹配问题的,即在字符串S中寻找字符串P.形式定义:假设存在长度为n的字符数组S[0...n-1],长度为m的字符数组P[0...m-1],是否存在i,使得SiSi+1... ...

  9. 4种字符串匹配算法:KMP(下)

    回顾:4种字符串匹配算法:BS朴素 Rabin-karp(上) 4种字符串匹配算法:有限自动机(中) 1.图解 KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R ...

随机推荐

  1. 自建 CA 中心并签发 CA 证书

    目录 文章目录 目录 CA 认证原理浅析 基本概念 PKI CA 认证中心(证书签发) X.509 标准 证书 证书的签发过程 自建 CA 签发证书并认证 HTTPS 网站的过程 使用 OpenSSL ...

  2. 【ABAP系列】SAP ABAP MRKO增强

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[ABAP系列]SAP ABAP MRKO增强 ...

  3. 【MM系列】SAP MM物料账在制品承担差异功能及配置

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[MM系列]SAP MM物料账在制品承担差异功能 ...

  4. 修改了Ubuntu下的/usr目录权限,导致不能使用sudo命令的修复-----转载

    刚开始运行sudo时,报了下面这个错误 sudo: must be setuid root,于是上网找解决方法,搜索出来的都是这样解决的 ls -l  /usr/bin/sudochown root: ...

  5. seaborn用heatmap画热度图

    原文链接 https://blog.csdn.net/m0_38103546/article/details/79935671

  6. 国家授时中心的NTP服务器地址 210.72.145.44

    国家授时中心的NTP服务器地址 210.72.145.44

  7. DOM练习(邓邓版)

    先来图片: 今天直接粘代码: 下面是html: <h4>01.图片切换</h4> <img width = "100" src = "../ ...

  8. input输入框的的input事件和change事件以及change和blur事件的区别

    input输入框的 oninput事件 ,在用户输入的时候触发,只要元素值发生变化就会触发 input输入框的 onchange事件 ,要在输入框失去焦点的时候触发事件,当鼠标在其他地方点击一下才会触 ...

  9. docker之配置TensorFlow的运行环境

    Docker是一种 操作系统层面的虚拟化技术,类似于传统的虚拟机.传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程:而容器内的应用进程直接运行于宿主的内核,容 ...

  10. PhoneGap学习网址

    官网:http://app-framework-software.intel.com/ 下载地址:http://download.csdn.net/download/haozq2012/7635951