多模字符串匹配算法-Aho–Corasick
背景
在做实际工作中,最简单也最常用的一种自然语言处理方法就是关键词匹配,例如我们要对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自动机的思想是,在开始时先通过词表,对以下三种情况进行缓存:
- 按照字符转移成功进行跳转(success表)
- 按照字符转移失败进行跳转(fail表)
- 匹配成功输出表(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的更多相关文章
- 多模字符串匹配算法之AC自动机—原理与实现
简介: 本文是博主自身对AC自动机的原理的一些理解和看法,主要以举例的方式讲解,同时又配以相应的图片.代码实现部分也予以明确的注释,希望给大家不一样的感受.AC自动机主要用于多模式字符串的匹配,本质上 ...
- Aho - Corasick string matching algorithm
Aho - Corasick string matching algorithm 俗称:多模式匹配算法,它是对 Knuth - Morris - pratt algorithm (单模式匹配算法) 形 ...
- 字符串匹配算法 - KMP
前几日在微博上看到一则微博是说面试的时候让面试者写一个很简单的字符串匹配都写不出来,于是我就自己去试了一把.结果写出来的是一个最简单粗暴的算法.这里重新学习了一下几个经典的字符串匹配算法,写篇文章以巩 ...
- Boyer-Moore 字符串匹配算法
字符串匹配问题的形式定义: 文本(Text)是一个长度为 n 的数组 T[1..n]: 模式(Pattern)是一个长度为 m 且 m≤n 的数组 P[1..m]: T 和 P 中的元素都属于有限的字 ...
- KMP单模快速字符串匹配算法
KMP算法是由Knuth,Morris,Pratt共同提出的算法,专门用来解决模式串的匹配,无论目标序列和模式串是什么样子的,都可以在线性时间内完成,而且也不会发生退化,是一个非常优秀的算法,时间复杂 ...
- 字符串匹配算法之BF(Brute-Force)算法
BF(Brute-Force)算法 蛮力搜索,比较简单的一种字符串匹配算法,在处理简单的数据时候就可以用这种算法,完全匹配,就是速度慢啊. 基本思想 从目标串s 的第一个字符起和模式串t的第一个字符进 ...
- 【原创】通俗易懂的讲解KMP算法(字符串匹配算法)及代码实现
一.本文简介 本文的目的是简单明了的讲解KMP算法的思想及实现过程. 网上的文章的确有些杂乱,有的过浅,有的太深,希望本文对初学者是非常友好的. 其实KMP算法有一些改良版,这些是在理解KMP核心思想 ...
- 字符串匹配算法——KMP算法学习
KMP算法是用来解决字符串的匹配问题的,即在字符串S中寻找字符串P.形式定义:假设存在长度为n的字符数组S[0...n-1],长度为m的字符数组P[0...m-1],是否存在i,使得SiSi+1... ...
- 4种字符串匹配算法:KMP(下)
回顾:4种字符串匹配算法:BS朴素 Rabin-karp(上) 4种字符串匹配算法:有限自动机(中) 1.图解 KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R ...
随机推荐
- 阶段3 1.Mybatis_11.Mybatis的缓存_7 触发清空一级缓存的情况
如果数据库的数据和一级缓存的数据不一致了,怎么做到同步的呢? 增加一个更新 用户信息的方法 增加更新的节点配置 测试类增加测试方法.先查询id为41的 然后更新了41的数据.再次查询41的数据 先把更 ...
- IDEA maven 阿里云镜像 下载jar失败
我在使用IDEA搭建SpringCloud项目的时候,发现很多spring cloud 包总是下载不了.maven配置了阿里云镜像.在换了一下新版的镜像地址以后就可以下包了. 官方答疑:新版maven ...
- 【Spring】的【Bean】管理(注解)【四个相同功能的注解】
[Spring]的[Bean]管理(注解)[四个相同功能的注解] 注解:代码里面特殊的标记,使用注解也可以完成一些相关的功能. 注解写法:@注解名称(属性名称=属性值) 注解使用在类.方法.属性上面 ...
- 获取当前操作的IFrame 对象的方法
分两种情况:第一种:获取iframe对象的JS函数在父页面上,如下 function getIframeByElement(element){ var iframe; $("iframe&q ...
- ElasticSearch Machine Learning
https://www.youtube.com/watch?v=DBRISS0UKcA, 2017/04 Single Metric job: 我想按照 一定的time interval 去 aggr ...
- Linux-SSH远程管理服务实战
figure:first-child { margin-top: -20px; } #write ol, #write ul { position: relative; } img { max-wid ...
- laravel框架源码分析(一)自动加载
一.前言 使用php已有好几年,laravel的使用也是有好长时间,但是一直对于框架源码的理解不深,原因很多,归根到底还是php基础不扎实,所以源码看起来也比较吃力.最近有时间,所以开启第5.6遍的框 ...
- CentOS7中MySQL跨机器数据迁移
1.概况 在CentOS7环境下,使用命令方式将MySQL数据从源端主机迁移到目标端主机上. 2.迁移全部数据库 1)源端备份: [root@hadoop102 /]# mysqldump -u ro ...
- vue element 导出 分页数据的excel表格
1.安装相关依赖 npm install --save xlsx file-saver 2.导入相关插件 在组建头部导入相关插件 const FileSaver = require("fil ...
- 【7.10校内test】T3经营与开发
[题目链接luogu] 它……又是个DP 我……我讨厌DP!? 然后又是读入,显然用快读啊:(数据范围还是很大的)(习惯) 然后我们发现,不论是损耗值维修值,还是采矿所得,维修花费,都带着个p,因此我 ...