背景

在做实际工作中,最简单也最常用的一种自然语言处理方法就是关键词匹配,例如我们要对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 parseText(String text) {

checkForConstructedFailureStates();

Node currentState = this.root;

List collectedEmits = new ArrayList<>();

for (int position = 0; position < text.length(); position++) {

Character character = text.charAt(position);

currentState = currentState.nextState(character);

Collection 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 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 map;

private List 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 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 children() {

return this.map.values();

}

public void setFailure(Node node) {

failure = node;

}

public Node getFailure() {

return failure;

}

public Set getTransitions() {

return map.keySet();

}

public Collection emit() {

return this.emits == null ? Collections.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 emits = trie.parseText("ushers");

for (Emit emit : emits) {

System.out.println(emit.start + " " + emit.end + "\t" + emit.getKeyword());

}

}

}

java实现多模匹配算法_多模字符串匹配算法-Aho–Corasick相关推荐

  1. Java实现算法导论中朴素字符串匹配算法

    朴素字符串匹配算法沿着主串滑动子串来循环匹配,算法时间性能是O((n-m+1)m),n是主串长度,m是字串长度,结合算法导论中来理解,具体代码参考: package cn.ansj;public cl ...

  2. java中KMP模式_朴素模式匹配算法、kmp模式匹配算法、kmp模式匹配算法改进。java代码...

    ** 朴素模式匹配算法.kmp模式匹配算法.kmp模式匹配算法改进.java代码** 思路过段时间整理~ 可以先看看阮一峰的这篇博客,字符串匹配的KMP算法 package edu.hubu.base ...

  3. java 以什么开头_判断字符串以什么开头

    Java 判断字符串是否以什么开头 可以使用 startsWith() 判断,例如: String str = "Hello World.";// 判断是否以 Hello 开始Sy ...

  4. java正则表达式去除空格_去掉字符串前后所有空格的正则表达式

    正则表达式,又称正规表示法.常规表示法(英语:Regular Expression,在代码中常简写为regex.regexp或RE),计算机科学的一个概念.正则表达式使用单个字符串来描述.匹配一系列符 ...

  5. Java实现算法导论中KMP字符串匹配算法

    "前缀"和"后缀". "前缀"指除了最后一个字符以外,一个字符串的全部头部组合:"后缀"指除了第一个字符以外,一个字符串 ...

  6. Java实现算法导论中有限自动机字符串匹配算法

    这里实现了基于有限自动机(Finite Automaton,FA)的模式匹配算法,算法的重点在于利用字符串的前后缀构造模式P的自动机,具体结合导论中的说明来理解,可参考http://www.geeks ...

  7. Java实现算法导论中Rabin-Karp字符串匹配算法

    Rabin-Karp算法的思想: 假设子串的长度为M,目标字符串的长度为N 计算子串的hash值 计算目标字符串中每个长度为M的子串的hash值(共需要计算N-M+1次) 比较hash值 如果hash ...

  8. java识别汉字个数_统计字符串中汉字的个数

    字符串可以包括数字.字母.汉字或者其他字符.使用Charater类的isDigit()方法可以判断字符串中的某个字符是否为数字, 使用Character类的isLetter()方法可以判断字符串中的某 ...

  9. Java 计算字母个数_统计字符串中每个字母的个数

    最近整理之前自己学习Java时的一些代码笔记,可能都是一些比较基础的Java知识,在这里只是给需要的人参考一下. 统计一个字符串中的每个字母的个数 现有字符串:dludsstytrwtrjahtksd ...

  10. es6与java的相似度_计算字符串的相似度-两种解法

    一直不理解,为什么要计算两个字符串的相似度呢.什么叫做两个字符串的相似度.经常看别人的博客,碰到比较牛的人,然后就翻了翻,终于找到了比较全面的答案和为什么要计算字符串相似度的解释.因为搜索引擎要把通过 ...

最新文章

  1. 合并排序(C语言实现)
  2. 赔腾讯70万,QQ“自动抢红包”软件被判不正当竞争
  3. package.json
  4. 算法学习-莫比乌斯反演
  5. [BuildRelease]跨平台构建工具Cmake
  6. mysql中预定义常量_PHP预定义常量
  7. vue 添加子路由,并对路由重定向
  8. android窗口泄漏
  9. 「大学生学编程系列」第六篇:如何学习C语言?
  10. jQuery 生成随机字符
  11. 图像分割并存储 matlab,matlab图像分割算法源码.pdf
  12. 墙裂推荐9个在线图片压缩网站
  13. 自定义流程比较全的表结构
  14. 嵌入式linux IIO驱动
  15. Vue项目上线后刷新报错404问题(apache,nginx,tomcat)
  16. [NOIP 2018 T3]摆渡车
  17. Redis 配置不当致使 root 被提权漏洞
  18. 程序员如何优雅地使用 macOS?
  19. 直接从数据库中查询数据生成email附件(excel)
  20. 用Python代码实现视频转gif动图

热门文章

  1. php加cnzz,vue项目中添加cnzz统计
  2. 13.PIO源码详解3-PIO模块(PIO.v)剖析
  3. 团体力学理论(1944)--轉
  4. C1083: 无法打开包括文件:“corecrt.h”
  5. 2021.09青少年软件编程(Python)等级考试试卷(五级)
  6. 数学之美 第六章——信息的度量和作用
  7. Day3:MVP+ButterKnife+Dagger2的使用
  8. uc收藏导入html,uc浏览器的收藏网址如何导出方法
  9. 湘潭大学计算机分数线,湘潭大学录取分数线2021是多少分(附历年录取分数线)
  10. 浏览器提示网站链接不安全,证书过期了