简介

Aho-Corasick算法简称AC算法,通过将模式串预处理为确定有限状态自动机,扫描文本一遍就能结束。其复杂度为O(n),即与模式串的数量和长度无关。

思想

自动机按照文本字符顺序,接受字符,并发生状态转移。这些状态缓存了“按照字符转移成功(但不是模式串的结尾)”、“按照字符转移成功(是模式串的结尾)”、“按照字符转移失败”三种情况下的跳转与输出情况,因而降低了复杂度。

基本构造

AC算法中有三个核心函数,分别是:

success; 成功转移到另一个状态(也称goto表或success表)

failure; 不可顺着字符串跳转的话,则跳转到一个特定的节点(也称failure表),从根节点到这个特定的节点的路径恰好是失败前的文本的一部分。

emits; 命中一个模式串(也称output表)

举例

以经典的ushers为例,模式串是he/ she/ his /hers,文本为“ushers”。构建的自动机如图:

其实上图省略了到根节点的fail边,完整的自动机如下图:

匹配过程

自动机从根节点0出发

首先尝试按success表转移(图中实线)。按照文本的指示转移,也就是接收一个u。此时success表中并没有相应路线,转移失败。

失败了则按照failure表回去(图中虚线)。按照文本指示,这次接收一个s,转移到状态3。

成功了继续按success表转移,直到失败跳转步骤2,或者遇到output表中标明的“可输出状态”(图中红色状态)。此时输出匹配到的模式串,然后将此状态视作普通的状态继续转移。

算法高效之处在于,当自动机接受了“ushe”之后,再接受一个r会导致无法按照success表转移,此时自动机会聪明地按照failure表转移到2号状态,并经过几次转移后输出“hers”。来到2号状态的路不止一条,从根节点一路往下,“h→e”也可以到达。而这个“he”恰好是“ushe”的结尾,状态机就仿佛是压根就没失败过(没有接受r),也没有接受过中间的字符“us”,直接就从初始状态按照“he”的路径走过来一样(到达同一节点,状态完全相同)。

构造过程

看来这三个表很厉害,不过,它们是怎么计算出来的呢?

goto表

很简单,了解一点trie树知识的话就能一眼看穿,goto表就是一棵trie树。把上图的虚线去掉,实线部分就是一棵trie树了。

output表

output表也很简单,与trie树里面代表这个节点是否是单词结尾的结构很像。不过trie树只有叶节点才有“output”,并且一个叶节点只有一个output。下图却违背了这两点,这是为什么呢?其实下图的output会在建立failure表的时候进行一次拓充。

failure表

这个表是trie树没有的,加了这个表,AC自动机就看起来不像一棵树,而像一个图了。failure表是状态与状态的一对一关系,别看图中虚线乱糟糟的,不过你仔细看看,就会发现节点只会发出一条虚线,它们严格一对一。

这个表的构造方法是:

首先规定与状态0距离为1(即深度为1)的所有状态的fail值都为0。

然后设当前状态是S1,求fail(S1)。我们知道,S1的前一状态必定是唯一的(刚才说的一对一),设S1的前一状态是S2,S2转换到S1的条件为接受字符C,测试S3 = goto(fail(S2), C)。

如果成功,则fail(S1) = goto(fail(S2), C) = S3。

如果不成功,继续测试S4 = goto(fail(S3), C)是否成功,如此重复,直到转换到某个有效的状态Sn,令fail(S1) = Sn。

Java实现

原理谁都可以说几句的,可是优雅健壮的代码却不是那么容易写的。我考察了Git上几个AC算法的实现,发现robert-bor的实现非常好。一趟代码看下来,学到了不少设计上的知识。我fork了下来,针对Ascii做了优化,添加了中文注释。

开源项目

调用方法

Trie trie = new Trie();

trie.addKeyword("hers");

trie.addKeyword("his");

trie.addKeyword("she");

trie.addKeyword("he");

Collection emits = trie.parseText("ushers");

System.out.println(emits);

输出:

[2:3=he, 1:3=she, 2:5=hers]

此外,还有一些配置选项:

/**

* 大小写敏感

*/

public Trie caseInsensitive()

{

this.trieConfig.setCaseInsensitive(true);

return this;

}

/**

* 不允许模式串在位置上前后重叠

*/

public Trie removeOverlaps()

{

this.trieConfig.setAllowOverlaps(false);

return this;

}

/**

* 只匹配完整单词

*/

public Trie onlyWholeWords()

{

this.trieConfig.setOnlyWholeWords(true);

return this;

}

org.ahocorasick.trie包

这里封装了Trie树,其中比较重要的类是Trie树的节点State:

我重构了State,将其异化为UnicodeState和AsciiState类。其中UnicodeState类使用 Map 来储存goto表,而AsciiState类使用数组 State[] success = new State[256]来储存,这样在Ascii表上面,AsciiState的匹配要稍微快一些,相应的在构建时会慢一些,内存占用也会多一些。

从对万字的英语词典的测试结果来看,AsciiState的确有那么一点优势:

asciiTrie adding time:1013ms

unicodeTrie adding time:96ms

asciiTrie building time:903ms

unicodeTrie building time:312ms

asciiTrie parsing time:355ms

unicodeTrie parsing time:463ms

org.ahocorasick.interval包

这里封装了一棵线段树,关于线段树的介绍请查看:线段树。

线段树用于修饰最后的匹配结果,匹配结果中有一些可能会重叠,比如she和he,这棵线段树对匹配结果(一系列区间)进行索引,能够在log(n)时间内判断一个区间与另一个是否重叠。详细的实现请看代码,都有中文注释,应该很好懂。

基于双数组Trie树的Aho Corasick自动机

AC自动机能高速完成多模式匹配,然而具体实现聪明与否决定最终性能高低。大部分实现都是一个Map了事,无论是TreeMap的对数复杂度,还是HashMap的巨额空间复杂度与哈希函数的性能消耗,都会降低整体性能。

双数组Trie树能高速O(n)完成单串匹配,并且内存消耗可控,然而软肋在于多模式匹配,如果要匹配多个模式串,必须先实现前缀查询,然后频繁截取文本后缀才可多匹配,这样一份文本要回退扫描多遍,性能极低。

如果能用双数组Trie树表达AC自动机,就能集合两者的优点,得到一种近乎完美的数据结构。具体实现请参考《Aho Corasick自动机结合DoubleArrayTrie极速多模式匹配》。

Reference

部分图片和介绍来自:

http://www.cnblogs.com/zzqcn/p/3525636.html

http://blog.csdn.net/sealyao/article/details/4560427

ac算法 java_Aho-Corasick算法的Java实现与分析相关推荐

  1. TypeScript:Aho–Corasick算法实现敏感词过滤

    敏感词过滤应该是许多后端同事经常会遇到的需求,无论是评论.弹幕.文章,都需要做敏感词过滤处理来规避风险.在前端开发中,使用replace函数来替换字符串是我们的常规操作,在这之前我思考过如果用Java ...

  2. java python算法_用Python,Java和C ++示例解释的排序算法

    java python算法 什么是排序算法? (What is a Sorting Algorithm?) Sorting algorithms are a set of instructions t ...

  3. 集合70多种推荐算法,东北大学老师用Java写了一个开源库,在GitHub上收获近1500个Star...

     [AI科技大本营导读]在经过一年多的开发工作之后,LibRec 3.0 版本终于发布了.LibRec 是一个基于 Java 的开源算法工具库,覆盖了 70 余个各类型推荐算法,可以有效解决评分预测 ...

  4. A*算法解决八数码问题 Java语言实现

    A*算法解决八数码问题 Java语言实现 参考文章: (1)A*算法解决八数码问题 Java语言实现 (2)https://www.cnblogs.com/beilin/p/5981483.html ...

  5. 详解KMP算法原理,以及完整java与C++实现

    点击此处学习更多算法与通信知识 作者 | labuladong 来源 | labuladong KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法,效率很高,但是确实 ...

  6. 1.1.10 从二分查找BinarySearch开启算法学习之路---《java算法第四版》

    文章目录 0.前言 1.功能 2.示例 有两个名单tinyW.txt和tinyT.txt,将tinyT.txt名单中不在tinyW.txt的数据打印出来 ① 实现原理 ② 实现代码 ③ 性能分析 0. ...

  7. 67.Java垃圾收集机制\对象引用\垃圾对象的判定\垃圾收集算法\标记—清除算法\标记—整理算法\分代收集\垃圾收集器\性能调优

    67.Java垃圾收集机制 67.1.对象引用 67.2.垃圾对象的判定 67.3.垃圾收集算法 67.3.1.标记-清除算法 67.3.2.标记-整理算法 67.3.3.分代收集 67.4.垃圾收集 ...

  8. 终于,把十大经典排序算法汇总了!(Java实现版)

    转载自  终于,把十大经典排序算法汇总了!(Java实现版) 最近几天在研究排序算法,看了很多博客,发现网上有的文章中对排序算法解释的并不是很透彻,而且有很多代码都是错误的,例如有的文章中在" ...

  9. 分治法在排序算法中的应用(JAVA)--快速排序(Lomuto划分、Hoare划分、随机化快排)

    分治法在排序算法中的应用--快速排序 时间复杂度:平均O(nlogn),最坏O(n^2) 如果说归并排序是按照元素在数组中的位置划分的话,那么快速排序就是按照元素的值进行划分.划分方法由两种,本节将主 ...

  10. 算法训练 纪念品分组(java)

    算法训练 纪念品分组(java) 描述 元旦快到了,校学生会让乐乐负责新年晚会的纪念品发放工作.为使得参加晚会的同学所获得的纪念品价值 相对均衡,他要把购来的纪念品根据价格进行分组,但每组最多只能包括 ...

最新文章

  1. Python机器学习——DBSCAN聚类
  2. linux系统定时检查网络状态python脚本
  3. 如何形象地描述 RxJava 中的背压和流控机制?
  4. 高速串行总线的设计基础(一)同步时序模型介绍
  5. 成功解决Python中导出图片出现错误SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position
  6. 数组和矩阵问题:奇数下标都是奇数或者偶数下标都是偶数
  7. 中文正则表达式初步使用
  8. .NET Core快速入门教程 4、使用VS Code开发.NET Core控制台应用程序
  9. leetcood学习笔记-58-最后一个单词的长度
  10. 无代码来了,还要程序员吗?
  11. java修改.class重新打包jar
  12. 安装天文基本包:kapteyn和pyslalib
  13. html5 粽子飘落,飘落的丁香花阅读*
  14. 新手,如何快速建立一个网站?
  15. 微服务生态组件之Spring Cloud LoadBalancer详解和源码分析
  16. python绘制函数曲线
  17. 微信小程序有哪些?401~500
  18. 纯CSS3实现旋转风车
  19. OPUS进行音频编码与解码
  20. knif4j 在线APl 文档测试

热门文章

  1. LED背光源的技术术语
  2. 【BZOJ3097】 Hash Killer I
  3. 今生梦一场,思念你的殇
  4. 【BZOJ】5294: [Bjoi2018]二进制-动态DP线段树
  5. 阿里爸爸YYDS!阿里内网首发的Java成长开发手册,不惜重金开源
  6. java linux路径带括号,java执行linux命令 括号
  7. html密码浏览器不自动登录,怎样取消浏览器密码的自动登录
  8. MIUI12系统如何刷入开发版获得ROOT权限
  9. 高云FPGA系列教程(2):FPGA点灯工程创建、程序下载和固化
  10. PS带框的对号怎么打