详细介绍了AC自动机算法详解以及Java代码实现。

文章目录

  • 1 概念和原理
  • 2 节点定义
  • 3 构建Trie前缀树
    • 3.1 案例演示
  • 4 构建fail失配指针
    • 4.1 案例演示
  • 5 匹配文本
    • 5.1 案例演示
  • 6 完整实现
  • 7 总结
  • 8 其他参考

1 概念和原理

AC自动机(Aho-Corasick automaton)算法于1975年产生于贝尔实验室,是一个多模式字符串匹配算法,在多模式匹配领域被广泛应用,例如违禁词查找替换、搜索关键词查找等等。

关于Trie树和KMP算法,我们此前已经讲解过了:

  1. 前缀树Trie的实现原理以及Java代码的实现
  2. KMP算法详解以及Java代码实现

AC自动机算法常被认为是Trie树+KMP算法的结合体,为什么呢?我们先看看它的构建步骤:

  1. 对所有的关键词构建Trie前缀树。
  2. 为Trie树上的所有节点构建fail失配指针。

第一步,对所有的关键词构建Trie前缀树。这一步利用Trie的特点构建快速前缀查找结构,trie树的特点是可以从字符串头部开始匹配,并且相同前缀的词共用前面的节点,因此它可以避免相同前缀pattern的重复匹配,但是对于相同的后缀无能为力。

第二步,为Trie树上的所有节点构建fail失配指针节点,某个节点的失配指针节点即表示当前节点匹配失败后应该跳往的继续匹配的节点。fail失配指针是AC自动机能够匹配多个关键词的关键。

所谓节点的失配指针节点,就是当前节点表示的路径字符串中的最长真后缀位置的指针节点。这里需要理解KMP的next数组以及最长匹配长度的前缀和后缀概念,这一步就是利用KMP前后缀匹配的思想,实现利用相同的后缀信息快速跳转到另一个关键词继续前缀匹配,不会出现关键词遗漏的现象。

  1. 如果当前节点没有匹配到,则跳转到此节点继续匹配。
  2. 如果当前节点匹配到了,那么可以通过此指针找到该节点的模式串路径中包含的最长后缀模式串。

这里的失配指针所谓最长匹配长度的前缀和后缀的和KMP的next数组中的概念区别是:

  1. 在KMP算法中,是针对单个关键词匹配,求出的最长匹配长度的前缀和后缀都位于同一个关键词内。例如关键词abcdabc,最长匹配前后缀为abc,他们都属于该关键词。
  2. 在AC自动机算法中,是针对多个关键词匹配,对于某个关键词路径求出的“最长匹配长度的前缀”是该关键路径的后缀串,它对应的“最长匹配长度的后缀”是另一个关键路径的前缀串。
  3. 另外,一个节点的失配指针节点,只能是它的某个上层节点,不可是它的下层节点。因为某个节点匹配失配之后只能够向上跳转,无法向下跳转(因为向下跳转就说明失配指针节点路径长度超过当前匹配的路径长度了)

例如3个关键词“普工电焊工”,“电焊工人”,“焊工”。

  1. 第一个关键路径“普工电焊工”中第2个“工”的节点的失配指针应该指向第二个关键路径 “电焊工人”中的“工”节点。因为关键路径字符串“普工电焊工”的后缀子串,与 另一个关键路径“电焊工人”的前缀子串“电焊工”相匹配,并且能够得到最长匹配路径。

    1. 注意,“普工电焊工”虽然有后缀“焊工”能与第三个关键路径“焊工”进行前缀匹配,但由于它不是最长匹配路径,因此不能指向“焊工”这一条路径中的“工”节点。
  2. 而关键路径“电焊工人”中的“工”节点的失配指针才应该指向第三个关键路径“焊工”中的“工”节点。如下:


假设基于三个关键词要匹配“普工电焊工”文本,那么当我们匹配到最后一个字符“工”节点的时候,首先匹配到了一个关键词“普工电焊工”,然后我们可以直接根据“工”节点的失配指针,找到“电焊工人”的“工”节点,然后又根据这个“工”节点的失配指针,找到“焊工”这个关键词。这样所有的关键词都找到了,不会出现遗漏“焊工”关键词的情况。

我们需要为所有节点构建失配指针节点,具体的构建策略下面会讲到。

2 节点定义

在这里,我们给出一个比较简单的节点的定义。

  1. next,表示经过该节点的模式串的下层节点,这是Trie树结构的保证,存储着子节点的值到对应的节点的映射关系。
  2. depth,表示以当前节点结尾的模式串的长度,也是节点的深度,默认为0。
  3. failure,失配指针节点,其指向表示另一个关键词前缀的最长后缀节点。用于实现利用相同的后缀信息快速跳转到另一个关键词继续前缀匹配,不会出现关键词遗漏的现象。
    1. 如果当前节点没有匹配到,则跳转到此节点继续匹配。
    2. 如果当前节点匹配到了,那么可以通过此指针找到该节点的模式串包含的最长后缀模式串继续匹配。
class AcNode {/*** 经过该节点的模式串的下层节点*/Map<Character, AcNode> next = new HashMap<>();/*** 模式串的长度,也是节点的深度*/int depth;/*** 失配指针,如果没有匹配到,则跳转到此状态。*/AcNode failure;public boolean hashNext(char nextKey) {return next.containsKey(nextKey);}public AcNode getNext(char nextKey) {return next.get(nextKey);}
}

3 构建Trie前缀树

构建AC自动机的Trie的方法和构建普通Trie的方法几乎一致。

在添加每个模式串成功后,会为最后一个节点的depth赋值为当前模式串的长度,也就是说depth值不为0,则表示当前节点是一个关键词的结尾。

/*** trie根节点*/
private AcNode root;
/*** 加入模式串,构建Trie** @param word 模式串,非空*/
public void insert(String word) {AcNode cur = root;for (char c : word.toCharArray()) {if (!cur.next.containsKey(c)) {cur.next.put(c, new AcNode());}cur = cur.next.get(c);}cur.depth = word.length();
}

3.1 案例演示

假设我们有如下关键词:电焊、电焊工、电焊工人、电焊学员、电焊学徒、电焊学徒工、普工电焊工、普工电商、普工

那么我们构建的前缀树结构如下,红色圈表示该节点是某个关键词的结束位置:

4 构建fail失配指针

构建fail失配指针的一种常见的方法如下,实际上是一个BFS层序遍历的算法

  1. Trie的root节点没有失配指针,或者说失配指针为null,其他节点都有失配指针,或者说不为null。
  2. 遍历root节点的所有下一层直接子节点,将它们的失配指针设置为root。因为这些节点代表着所有模式串的第一个字符,基于KMP的next数组定义,单个字符没有最长真后缀,此时直接指向root
  3. 继续循环向下遍历每一层的子节点,由于bfs的遍历,那么上一层父节点的失配指针肯定都已经确定了。基于next数组的构建思想,子节点的失配指针可以通过父节点的是失配指针快速推导出来。设当前遍历的节点为c,它的父节点为p,父节点的失配指针为pf。
    1. 如果pf节点的子节点对应的字符中,包含了当前节点的所表示的字符。那么基于求最长后缀的原理,此时c节点的失配指针可以直接指向pf节点下的相同字符对应的子节点。
    2. 如果pf节点的子节点对应的字符中,没有包含了当前节点的所表示的字符。那么继续获取pf节点的失配指针节点,继续重复判断。直到满足第一种情况,或者pf指向了根节点,并且根节点的子节点也没有匹配,那么此时直接将c节点的失配指针指向根节点。
/*** 为所有节点构建失配指针,一个bfs层序遍历*/
public void buildFailurePointer() {ArrayDeque<AcNode> queue = new ArrayDeque<AcNode>();//将所有root的直接子节点的failure设置为root,并且加入queuefor (AcNode acNode : root.next.values()) {acNode.failure = root;queue.addLast(acNode);}//bfs构建失配指针while (!queue.isEmpty()) {//父节点出队列AcNode parent = queue.pollFirst();//遍历父节点的下层子节点,基于父节点求子节点的失配指针for (Map.Entry<Character, AcNode> characterAcNodeEntry : parent.next.entrySet()) {//获取父节点的失配指针AcNode pf = parent.failure;//获取子节点AcNode child = characterAcNodeEntry.getValue();//获取子节点对应的字符Character nextKey = characterAcNodeEntry.getKey();//如果pf节点不为null,并且pf节点的子节点对应的字符中,没有包含了当前节点的所表示的字符while (pf != null && !pf.hashNext(nextKey)) {//继续获取pf节点的失配指针节点,继续重复判断pf = pf.failure;}//pf为null,表示找到了根节点,并且根节点的子节点也没有匹配if (pf == null) {//此时直接将节点的失配指针指向根节点child.failure = root;}//pf节点的子节点对应的字符中,包含了当前节点的所表示的字符else {//节点的失配指针可以直接指向pf节点下的相同字符对应的子节点child.failure = pf.getNext(nextKey);}//最后不要忘了,将当前节点加入队列queue.addLast(child);}}
}

4.1 案例演示

首先,根据我们的构建BFS方法,从上层向下层依次构建。

首先是第1层root节点,root节点没有fail指针节点,它的failure属性为null。

然后是root节点的所有下一层直接子节点,即第2层节点,将它们的失配指针设置为root。因为这些节点代表着所有模式串的第一个字符,基于KMP的next数组定义,单个字符没有最长真后缀,此时直接指向root。

然后遍历下一层子节点,此时需要使用到规律。设当前遍历的节点为c,它的父节点为p,父节点的失配指针为pf,当前节点的失配指针为cf。

  1. 如果pf节点的子节点对应的字符中,包含了当前节点的所表示的字符。那么基于求最长后缀的原理,此时c节点的失配指针可以直接指向pf节点下的相同字符对应的子节点。
  2. 如果pf节点的子节点对应的字符中,没有包含了当前节点的所表示的字符。那么继续获取pf节点的失配指针节点,继续重复判断。直到满足第一种情况,或者pf指向了根节点,并且根节点的子节点也没有匹配,那么此时直接将c节点的失配指针指向根节点。

利用上面的规律,第3层节点的失配指针构建如下(从左到右):

首先找到第3层“焊”节点,它的父节点是“电”,“电”的pf为root,而root节点的子节点对应的字符中,不包含了当前节点的所表示的字符,所以此时“焊”节点的cf指向root节点。

同理,第3层“工”节点的cf指向root节点。

利用上面的规律,第4层节点的失配指针构建如下(从左到右):

首先找到“学”节点,它的父节点是“焊”,“焊”的pf为root,而root节点的子节点对应的字符中,不包含了当前节点的所表示的字符,所以此时“学”节点的cf指向root节点。

同理,“工”节点的cf指向root节点。

最后是“电”节点,,它的父节点是“工”,“工”的pf为root,而root节点的子节点对应的字符中,包含了当前节点的所表示的字符“电”,所以此时“电”节点的cf指向root节点的“电”子节点。

利用上面的规律,第5层节点的失配指针构建如下(从左到右):

首先找到“员”节点,它的父节点是“学”,“学”的pf为root,而root节点的子节点对应的字符中,不包含了当前节点的所表示的字符,所以此时“员”节点的cf指向root节点。

同理,“徒”节点的cf指向root节点,“人”节点的cf指向root节点。

随后是,“焊”节点,它的父节点是“电”,“电”的pf为第2层的“电”节点,而该pf节点的子节点对应的字符中,包含了当前节点的所表示的字符“焊”,所以此时“焊”节点的cf指向第3层的“焊”节点。

最后是“商”节点,它的父节点是“电”,“电”的pf为第2层的电节点,而该pf节点的子节点对应的字符中,不包含了当前节点的所表示的字符“商”,此时,继续找pf节点的pf节点,最后找到root节点,并且root节点的子节点不包含当前节点的字符,所以此时“商”节点的cf指向root节点。

利用上面的规律,最后一层第6层节点的失配指针构建如下(从左到右):

首先找到“工”节点,它的父节点是“徒”,“徒”的pf为root,而root节点的子节点对应的字符中,不包含了当前节点的所表示的字符,所以此时“工”节点的cf指向root节点。

随后是,第二个“工”节点,它的父节点是“焊”,“焊”的pf为第3层的“焊”节点,而该pf节点的子节点对应的字符中,包含了当前节点的所表示的字符“工”,所以此时“工”节点的cf指向第4层的“工”节点。

最终的数据结构如下。整个Trie树的失配指针构建完毕,AC自动机构建完毕。

5 匹配文本

构建完AC自动机之后,下面我们需要进行文本的匹配,匹配的方式实际上比较简单。

  1. 遍历文本的每个字符,依次匹配,从Trie的根节点作为cur节点开始匹配:
  2. 将当前字符作为nextKey,如果cur节点不为null且节点的next映射中不包含nextKey,那么当前cur节点指向自己的failure失配指针。
  3. 如果cur节点为null,说明当前字符匹配到了root根节点且失败,那么cur设置为root继续从根节点开始进行下一轮匹配。
  4. 否则表示匹配成功的节点,cur指向匹配节点,获取该节点继续判断:
    1. 如果该节点是某个关键词的结尾,那么取出来,也就是depth不为0,那么表示匹配到了一个关键词。
    2. 继续判断该节点的失配指针节点表示的模式串。因为失配指针节点表示的是当前匹配的模式串的在这些关键词中的最长后缀,且由于当前节点的路径包括了失配指针的全部路径,并且失配指针路径也是一个完整的关键词,需要找出来。
/*** 匹配文本** @param text 文本字符串*/
public List<ParseResult> parseText(String text) {List<ParseResult> parseResults = new ArrayList<>();char[] chars = text.toCharArray();//从根节点开始匹配AcNode cur = root;//遍历字符串的每个字符for (int i = 0; i < chars.length; i++) {//当前字符char nextKey = chars[i];//如果cur不为null,并且当前节点的的子节点不包括当前字符,即不匹配while (cur != null && !cur.hashNext(nextKey)) {//那么通过失配指针转移到下一个节点继续匹配cur = cur.failure;}//如果节点为null,说明当前字符匹配到了根节点且失败//那么继续从根节点开始进行下一轮匹配if (cur == null) {cur = root;} else {//匹配成功的节点cur = cur.getNext(nextKey);//继续判断AcNode temp = cur;while (temp != null) {//如果当前节点是某个关键词的结尾,那么取出来if (temp.depth != 0) {int start = i - temp.depth + 1, end = i;parseResults.add(new ParseResult(start, end, new String(chars, start, temp.depth)));//System.out.println(start + " " + end + " " + new String(chars, start, temp.depth));}//继续判断该节点的失配指针节点//因为失配指针节点表示的模式串是当前匹配的模式串的在这些关键词中的最长后缀,且由于当前节点的路径包括了失配指针的全部路径//并且失配指针路径也是一个完整的关键词,需要找出来。temp = temp.failure;}}}return parseResults;
}class ParseResult {int startIndex;int endIndex;String key;public ParseResult(int startIndex, int endIndex, String key) {this.startIndex = startIndex;this.endIndex = endIndex;this.key = key;}@Overridepublic String toString() {return "{" +"startIndex=" + startIndex +", endIndex=" + endIndex +", key='" + key + '\'' +'}';}
}

5.1 案例演示

基于我们上面构建的AC自动机。假如,此时文本为:“你好我想找一个普工电焊工相关的工作”,下面我们来看看AC自动机匹配的过程:

cur=root,遍历文本的每一个字符进行匹配:

当前字符nextKey=“你”,cur.next不包含“你”,且cur.failure=null,此时进入下一轮。cur=root。

当前字符nextKey=“好”,cur.next不包含“好”,且cur.failure=null,此时进入下一轮。cur=root。

后续的字符“我想找一个”,都是上面的判断逻辑,此时还没有找到任何关键词。cur=root。

当前字符nextKey=“普”,cur.next包含“普”节点,表示这是一个匹配成功的节点,那么cur指向该节点“普”,temp=cur,继续循环判断(temp!=null):

  1. temp不是某个关键词的结尾,temp=temp.failure=root。
  2. 最终结束本次查找,没找到任何关键词,cur=“普”。


当前字符nextKey=“工”,cur.next包含“工”节点,表示这是一个匹配成功的节点,那么cur指向该节点“工”,temp=cur,继续循环判断(temp!=null):
3. temp是某个关键词的结尾,此时找到了第1个匹配的关键词“普工”。temp=temp.failure=root。
4. 最终结束本次查找。cur=“工”。


当前字符nextKey=“电”,cur.next包含“电”节点,表示这是一个匹配成功的节点,那么cur指向该节点“电”,temp=cur,继续循环判断(temp!=null):

  1. temp不是某个关键词的结尾,temp=temp.failure,即temp指向第2层的电节点。
  2. temp不是某个关键词的结尾,temp=temp.failure,即temp=root。
  3. 最终结束本次查找,没找到任何关键词,cur=“电”。


当前字符nextKey=“焊”,cur.next包含“焊”节点,表示这是一个匹配成功的节点,那么cur指向该节点“焊”,temp=cur,继续循环判断(temp!=null):

  1. temp不是某个关键词的结尾,temp=temp.failure,即temp指向第3层的“焊”节点。
  2. temp是某个关键词的结尾,此时找到了第2个匹配的关键词“电焊”。这里就能看出失配指针的重要作用,它可以在不同的关键词之间跳转,避免了关键词匹配的遗漏。temp=temp.failure=root。
  3. 最终结束本次查找。cur=“焊”。


当前字符nextKey=“工”,cur.next包含“工”节点,表示这是一个匹配成功的节点,那么cur指向该节点“工”,temp=cur,继续循环判断(temp!=null):

  1. temp是某个关键词的结尾,此时找到了第3个匹配的关键词“普工电焊工”。temp=temp.failure,即temp指向第4层的“工”节点。
  2. temp是某个关键词的结尾,此时找到了第4个匹配的关键词“电焊工”。这里就能看出失配指针的重要作用,它可以在不同的关键词之间跳转,避免了关键词匹配的遗漏。temp=temp.failure=root。
  3. 最终结束本次查找。cur=“工”。


当前字符nextKey=“相”,cur.next不包含“相”节点,cur=cur.failure,即cur指向第4层“工”节点。

cur.next不包含“相”节点,cur=cur.failure=root。

cur.next不包含“相”节点,且cur.failure=null,最终进入下一轮。cur=root。

当前字符nextKey=“关”,cur.next不包含“关”节点,且cur.failure=null,最终进入下一轮。cur=root。

当前字符nextKey=“的”,cur.next不包含“的”节点,且cur.failure=null,最终进入下一轮。cur=root。

后续的字符“相关的工作”,都是上面的判断逻辑,此时没有找到任何关键词。到此字符串遍历完毕,查找完毕!

最终文本“你好我想找一个普工电焊工相关的工作”,匹配到关键词如下:

[{startIndex=8, endIndex=9, key='普工'},
{startIndex=10, endIndex=11, key='电焊'},
{startIndex=8, endIndex=12, key='普工电焊工'},
{startIndex=10, endIndex=12, key='电焊工'}]

6 完整实现

public class ACTrie3 {/*** trie根节点*/private AcNode root;public ACTrie3() {this.root = new AcNode();}class AcNode {/*** 经过该节点的模式串的下层节点*/Map<Character, AcNode> next = new HashMap<>();/*** 模式串的长度,也是节点的深度*/int depth;/*** 失配指针,如果没有匹配到,则跳转到此状态。*/AcNode failure;public boolean hashNext(char nextKey) {return next.containsKey(nextKey);}public AcNode getNext(char nextKey) {return next.get(nextKey);}}/*** 加入模式串,构建Trie** @param word 模式串,非空*/public void insert(String word) {AcNode cur = root;for (char c : word.toCharArray()) {if (!cur.next.containsKey(c)) {cur.next.put(c, new AcNode());}cur = cur.next.get(c);}cur.depth = word.length();}/*** 为所有节点构建失配指针,一个bfs层序遍历*/public void buildFailurePointer() {ArrayDeque<AcNode> queue = new ArrayDeque<AcNode>();//将所有root的直接子节点的failure设置为root,并且加入queuefor (AcNode acNode : root.next.values()) {acNode.failure = root;queue.addLast(acNode);}//bfs构建失配指针while (!queue.isEmpty()) {//父节点出队列AcNode parent = queue.pollFirst();//遍历父节点的下层子节点,基于父节点求子节点的失配指针for (Map.Entry<Character, AcNode> characterAcNodeEntry : parent.next.entrySet()) {//获取父节点的失配指针AcNode pf = parent.failure;//获取子节点AcNode child = characterAcNodeEntry.getValue();//获取子节点对应的字符Character nextKey = characterAcNodeEntry.getKey();//如果pf节点不为null,并且pf节点的子节点对应的字符中,没有包含了当前节点的所表示的字符while (pf != null && !pf.hashNext(nextKey)) {//继续获取pf节点的失配指针节点,继续重复判断pf = pf.failure;}//pf为null,表示找到了根节点,并且根节点的子节点也没有匹配if (pf == null) {//此时直接将节点的失配指针指向根节点child.failure = root;}//pf节点的子节点对应的字符中,包含了当前节点的所表示的字符else {//节点的失配指针可以直接指向pf节点下的相同字符对应的子节点child.failure = pf.getNext(nextKey);}//最后不要忘了,将当前节点加入队列queue.addLast(child);}}}public void parseText1(String text) {char[] chars = text.toCharArray();AcNode p = root;//遍历字符串的每个字符for (int i = 0; i < chars.length; i++) {char c = chars[i];while (!p.hashNext(c) && p != root) {p = p.failure;}p = p.getNext(c);if (p == null) {p = root;} else {AcNode temp = p;while (temp != null) {if (temp.depth != 0) {int start = i - temp.depth + 1, end = i;System.out.println(start + " " + end + " " + new String(chars, start, temp.depth));}temp = temp.failure;}}}}/*** 匹配文本** @param text 文本字符串*/public List<ParseResult> parseText(String text) {List<ParseResult> parseResults = new ArrayList<>();char[] chars = text.toCharArray();//从根节点开始匹配AcNode cur = root;//遍历字符串的每个字符for (int i = 0; i < chars.length; i++) {//当前字符char nextKey = chars[i];//如果cur不为null,并且当前节点的的子节点不包括当前字符,即不匹配while (cur != null && !cur.hashNext(nextKey)) {//那么通过失配指针转移到下一个节点继续匹配cur = cur.failure;}//如果节点为null,说明当前字符匹配到了根节点且失败//那么继续从根节点开始进行下一轮匹配if (cur == null) {cur = root;} else {//匹配成功的节点cur = cur.getNext(nextKey);//继续判断AcNode temp = cur;while (temp != null) {//如果当前节点是某个关键词的结尾,那么取出来if (temp.depth != 0) {int start = i - temp.depth + 1, end = i;parseResults.add(new ParseResult(start, end, new String(chars, start, temp.depth)));//System.out.println(start + " " + end + " " + new String(chars, start, temp.depth));}//继续判断该节点的失配指针节点//因为失配指针节点表示的模式串是当前匹配的模式串的在这些关键词中的最长后缀,且由于当前节点的路径包括了失配指针的全部路径//并且失配指针路径也是一个完整的关键词,需要找出来。temp = temp.failure;}}}return parseResults;}class ParseResult {int startIndex;int endIndex;String key;public ParseResult(int startIndex, int endIndex, String key) {this.startIndex = startIndex;this.endIndex = endIndex;this.key = key;}@Overridepublic String toString() {return "{" +"startIndex=" + startIndex +", endIndex=" + endIndex +", key='" + key + '\'' +'}';}}public static void main(String[] args) {ACTrie3 acTrie3 = new ACTrie3();//添加关键词,构建TrieacTrie3.insert("电焊");acTrie3.insert("电焊工");acTrie3.insert("电焊工人");acTrie3.insert("电焊学员");acTrie3.insert("电焊学徒");acTrie3.insert("电焊学徒工");acTrie3.insert("普工电焊工");acTrie3.insert("普工电商");acTrie3.insert("普工");//构建fail指针,一个bfs遍历acTrie3.buildFailurePointer();System.out.println(acTrie3.parseText("你好,我想找一个普工电焊工相关的工作"));}
}

7 总结

AC自动机匹配某个文本text,需要遍历文本的每个字符,每次遍历过程中,都可能涉及到循环向上查找失配指针的情况,但是这里的循环次数不会超过Trie树的深度,在最后匹配成功时,同样涉及到向上查找失配指针的情况,这里的循环次数不会超过Trie树的深度。

设匹配的文本长度m,模式串平均长度n,那么AC自动机算法的匹配的时间复杂度为O(m*n)。可以发现,匹配的时间复杂度和关键词的数量无关,这就是AC自动机的强大之处。如果考虑模式串平均长度不会很长,那么时间复杂度近似O(m)。

本文仅仅考虑简单讲解AC自动机的原理,并提供简单的Java代码实现,并没有进行进一步的优化,实际上原始的AC自动机在时间和空间复杂度上还有很多的可优化空间,后面我们再介绍AC自动机的优化。

8 其他参考

https://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_algorithm
https://blog.csdn.net/xuanzui/article/details/126426754
https://www.cnblogs.com/cmmdc/p/7337611.html
https://zhuanlan.zhihu.com/p/80325757
https://zhuanlan.zhihu.com/p/368184958

AC自动机算法详解以及Java代码实现相关推荐

  1. 极限定律 My Algorithm Space AC自动机算法详解

    转载自:http://www.cppblog.com/mythit/archive/2009/04/21/80633.html 首先简要介绍一下AC自动机:Aho-Corasick automatio ...

  2. AC自动机 算法详解(图解)及模板

    要学AC自动机需要自备两个前置技能:KMP和trie树(其实个人感觉不会kmp也行,失配指针的概念并不难) 其中,KMP是用于一对一的字符串匹配,而trie虽然能用于多模式匹配,但是每次匹配失败都需要 ...

  3. 相位 unwrap 与 wrap 算法详解(附代码)

    相位 unwrap 与 wrap 算法详解(附代码) 最近接手了一个项目,光通信方面的,我负责编写初测结果的数据处理算法,其中有一个算法叫做 unwrap 与 wrap,之前没有听说过.通过询问同事与 ...

  4. kmeans算法详解和python代码实现

    kmeans算法详解和python代码实现 kmeans算法 无监督学习和监督学习 监督学习: 是通过已知类别的样本分类器的参数,来达到所要求性能的过程 简单来说,就是让计算机去学习我们已经创建好了的 ...

  5. 二分查找算法详解(附代码)

    二分查找算法详解(附代码) 注: 现有一个升序 不重复的数组 查询target是否在此数组中并返回序号 使用条件 使用二分算法的两个条件: 有序 不重复 混淆处 二分算法两种方式容易弄混淆的地方:就是 ...

  6. 编辑距离算法详解和python代码

    编辑距离(Levenshtein Distance)算法详解和python代码 最近做NLP用到了编辑距离,网上学习了很多,看到很多博客写的有问题,这里做一个编辑距离的算法介绍,步骤和多种python ...

  7. 基于多相滤波器的数字信道化算法详解(Python, Verilog代码已开源)

    基于多相滤波器的数字信道化算法详解 推导过程 总结 仿真 本文详细介绍了基于多相滤波器的数字信道化算法的推导过程, 如果您在阅读的过程中发现算法推导过程中有任何错误, 请不吝指出. 此外, 进入我的G ...

  8. 敏感词或关键词过滤,DFA算法详解及python代码实现

    一.前言 近期项目有了一个过滤敏感词的功能需求,在网上找了一些方法及解说,发现DFA算法比较好用,容易实现,但很多文章解释得不太清楚,这里将其详细描述,并用python代码实现. 二.DFA算法详解 ...

  9. paxos算法详解以及模拟代码

    0 paxos算法解决了什么问题 现在有n个人组成提一个会议,这个会议的目的是为了确定今年的税率,那么每个人都会提出自己认为的今年的合理的税率,为了大家能够达成一致,有了paxos算法.实际里,这个会 ...

最新文章

  1. struts2文件下载及 param name=inputNameinputStream/param的理解
  2. GitLab修改用户密码
  3. SQL Server2005 使用FOR XML选项进行字符串的串联聚合
  4. python nums函数获取结果记录集有多少行记录_python3 数据挖掘之pandas学习记录(一)-----NumPy...
  5. jaxb java xml序列化_XML编程总结(六)——使用JAXB进行java对象和xml格式之间的相互转换...
  6. Flutter ColorTween实现颜色过渡动画效果
  7. fastDFS 命令笔记
  8. oracle初学心得(转)
  9. Web测试-Web界面易用性测试
  10. PyQt4--QPushButton(click)类的信号
  11. windows存储空间清理,C盘空间清理教程,磁盘清理方法
  12. fastDFS上传文件过大
  13. 《关键对话》读书笔记
  14. 2020年中国儿童家具行业产量、市场规模发展现状及儿童家具企业竞争格局分析[图]
  15. linux下迅雷远程下载服务,在 Linux 下使用迅雷的另一种无入侵方式
  16. 安卓029老年人监护系统APP
  17. 获取手机屏幕大小、密度、分辨率、状态栏、标题栏高度
  18. 付宇泽20190919-5 代码规范,结对要求
  19. HUAWEI被google禁完整版Android的背后——中美两国的博弈。
  20. 【如何使用Excel实现包含关系】

热门文章

  1. 【单片机笔记】运算放大器工作原理
  2. 什么是scss?scss 的基本使用
  3. web渗透之jwt 安全问题
  4. php开启opcache,启用OPCache提高PHP程序性能的方法
  5. apollo学习之---场景测试用例实践
  6. Java学习路线-19:日期操作类Date、SimpleDateFormat
  7. 寻找模质数意义下的二次剩余与三次剩余
  8. 关于数据库中的delete,truncate,drop
  9. 什么是ISO,为什么企业都在做ISO9001认证?
  10. 透视变换的实现以及透视变换矩阵的构造