11、AC自动机多模匹配、全匹配、前缀匹配

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

1、介绍

AC自动机实现的是多模式串匹配算法

1、借助于字典树存储

2、通过失败指针快速,实现查找失败后,快速跳到下一个起始点,而不是root点开始查找 失败指针确保:失败指针节点反向到root的路径,已经包含在匹配传中了。

3、如果失败指针的节点是叶子节点,则它的值会被加载到指向它的节点set<>中,正因为如此, 才会有多模式匹配功能

2、图型介绍

例子:给出5个单词,say,she,shr,he,her。给定字符串为yasherhs。问多少个单词在字符串中出现过

组成的字典树trid

11、AC自动机多模匹配、全匹配、前缀匹配_第1张图片

查找每一个节点的失败指针

11、AC自动机多模匹配、全匹配、前缀匹配_第2张图片

she包含her中的h,she包含her中的he

3、代码实现

  • 1、字典树节点TridNode

      import java.util.List;
    
      /**
       * 字典树节点
       *
       * [@Author](https://my.oschina.net/arthor) liufu
       * [@E-mail](https://my.oschina.net/rosolio): [email protected]
       * @CreateTime 2019/3/1  14:39
       */
      public class TridNode {
          private byte value;    //这个value是key的一个个byte值
          private TridNode prearentPoint; //父节点,是为了找到失败指针
          private TridNode failPoint; //这是失败指针,父节点失败指针节点的子节点是否一样
          private List childList;  //所有的孩子节点
          private List resultSet;  //结果集合,用于保存
    
          public TridNode() {
          }
    
          public TridNode(byte value) {
              this.value = value;
          }
    
          public byte getValue() {
              return value;
          }
    
          public void setValue(byte value) {
              this.value = value;
          }
    
          public TridNode getPrearentPoint() {
              return prearentPoint;
          }
    
          public void setPrearentPoint(TridNode prearentPoint) {
              this.prearentPoint = prearentPoint;
          }
    
          public TridNode getFailPoint() {
              return failPoint;
          }
    
          public void setFailPoint(TridNode failPoint) {
              this.failPoint = failPoint;
          }
    
          public List getChildList() {
              return childList;
          }
    
          public void setChildList(List childList) {
              this.childList = childList;
          }
    
          public List getResultSet() {
              return resultSet;
          }
    
          public void setResultSet(List resultSet) {
              this.resultSet = resultSet;
          }
      }
    
  • 2、包含AC自动机功能的字典树实现

      package com.java.project.acmatchmyself;
    
      import java.time.Instant;
      import java.util.LinkedList;
      import java.util.List;
      import java.util.concurrent.BlockingQueue;
      import java.util.concurrent.LinkedBlockingQueue;
    
      /**
       * 借助字典树和失败指针实现AC自动机算法,进行多模式串匹配功能
       * 同时实现了全匹配,以及左前缀匹配,构不构建失败指针都可以
       *
       * [@Author](https://my.oschina.net/arthor) liufu
       * @E-mail: [email protected]
       * @CreateTime 2019/3/1  14:39
       */
      public class TridForAc {
          private TridNode root;
          private int deep;
    
          public TridForAc() {
              //根节点没有failPoint,也没有preaentPoint
              root = new TridNode();
          }
    
          public int getDeep() {
              return deep;
          }
    
    
          /**
           * 给定byte[]和value,将key构建在树中
           * 同时把结果保存在叶子节点的resultSet中
           * 

    * 注意点:在需要用到pareatNode的childList时才需要判空创建 * * @param key * @param value * @return * @throws Throwable */ public boolean addNode(byte[] key, T value) { int i = 0; int length = key.length; TridNode parentNode = root; /** * 在树中顺序层级匹配key的byte * 如果树中包含整个key,则parentNode就是最后的那个叶子节点 */ while (i < length) { TridNode temp = findChildNode(parentNode.getChildList(), key[i]); if (temp == null) { break; } parentNode = temp; i++; } /** * i < length说明树只包含了key的一部分,需要将剩下的一部分构建到树中 * 构建后,parentNode肯定就是最后的那个数据叶子节点 * 注意:如果上面包含所有key,则i == length,此代码不跑 */ while (i < length) { TridNode node = new TridNode<>(key[i]); //如果父节点没有childList,new 一下 List childList = parentNode.getChildList(); if (childList == null) { childList = new LinkedList<>(); parentNode.setChildList(childList); } childList.add(node); node.setPrearentPoint(parentNode); parentNode = node; i++; } //到这里了,parentNode必须是那个最终的叶子节点 List resultSet = parentNode.getResultSet(); if (resultSet == null) { resultSet = new LinkedList(); parentNode.setResultSet(resultSet); } resultSet.add(value); return true; } /** * 构建失败指针 * 失败指针的作用是确保:root到失败指针这的部分key一定会在匹配串中 * * @return */ public boolean buildFailPoint() { BlockingQueue queue = new LinkedBlockingQueue<>(); //从第二排开始遍历即可,不需要处理root //因为root节点特殊,没有父节点,也没有失败指针 //如果不排除这个root节点,下面会引起死循环 List nodeList = root.getChildList(); if (nodeList != null && nodeList.size() > 0) { queue.addAll(nodeList); } while (!queue.isEmpty()) { TridNode node = queue.poll(); TridNode failPoint = node.getPrearentPoint().getFailPoint(); //判断父节点的失败指针的子节点是否存在此节点 while (failPoint != null) { TridNode failPointTemp = findChildNode(failPoint.getChildList(), node.getValue()); //找到自己的失败指针 if (failPointTemp != null) { node.setFailPoint(failPointTemp); break; } else { //否则继续找失败指针的失败指针,直到root为止(root的failPoint == null) failPoint = failPoint.getFailPoint(); } } if (node.getFailPoint() == null) { //说明一直到root都没有找到失败指针 node.setFailPoint(root); } List childList = node.getChildList(); if (childList != null && childList.size() > 0) { queue.addAll(childList); } } return true; } /** * ac多模式串匹配,前提是要进行失败指针构建 * * @param key * @return */ public List acSearch(byte[] key) { List result = new LinkedList<>(); int length = key.length; TridNode temp = root; for (int i = 0; temp != null && i < length; i++) { List childList = temp.getChildList(); if (childList != null) { TridNode childNode = findChildNode(childList, key[i]); if (childNode != null) { //当前查找路线能够找到,继续往下找 // 匹配上的节点结果加入到result中 List resultSet = childNode.getResultSet(); if (resultSet != null) { result.addAll(resultSet); } // 将所有失败指针节点的值拿过来 result.addAll(findFailPointResult(childNode.getFailPoint())); temp = childNode; } else { //当前查找路线找不到,则跳到失败指针,继续找 temp = temp.getFailPoint(); } } } return result; } /** * 全匹配,就是输入abcdef,那么字典树中就必须要要有abcdef这个key才能匹配出来 * 不要构建失败指针 * * @param key * @return */ public List fullSearch(byte[] key) { List result = new LinkedList<>(); int length = key.length; boolean matchFlag = true; TridNode temp = root; for (int i = 0; temp != null && i < length; i++) { List childList = temp.getChildList(); if (childList != null) { TridNode childNode = findChildNode(childList, key[i]); if (childNode != null) { //子节点能够找到,继续往下找 temp = childNode; } else { matchFlag = false; break; } } } /** * 能够全部匹配上,说明字典树中有一个key包含这个匹配key * 如果字段数的key是完整key,那么在最后那个节点上会有结果 * 比如:匹配key ==> abcd , 字典树key ==> abcd 的d节点的resultset会包含结果 * 如果字段数key ==> abcde,那么e节点保存结果,而不是d结果,这个时候也是没有结果出来。 */ if (matchFlag) { List set = temp.getResultSet(); if (set != null) { result.addAll(set); } } return result; } /** * 前缀匹配,比如输入abcdef,那么字典树中有abc, abcd, abcde, abcdef这样的关键字可以匹配出来 * 不要构建失败指针 * * @param key * @return */ public List preSearch(byte[] key) { LinkedList result = new LinkedList<>(); int length = key.length; TridNode temp = root; for (int i = 0; temp != null && i < length; i++) { List childList = temp.getChildList(); if (childList != null) { TridNode childNode = findChildNode(childList, key[i]); if (childNode != null) { //子节点能够找到,继续往下找 List set = childNode.getResultSet(); if (set != null) { result.addAll(set); } temp = childNode; } } } return result; } /** * 判断孩子节点中是否存在这个节点 * * @param childNodes * @param nodeValue * @return */ public TridNode findChildNode(List childNodes, byte nodeValue) { if (childNodes != null) { for (TridNode next : childNodes) { if (nodeValue == next.getValue()) { return next; } } } return null; } /** * 沿着失败指针找到失败指针节点的值 * 其实可以在构建失败指针的时候就把这些节点拿过来,储存在一起 * 但是会影响前缀匹配和全匹配,而且损耗空间 * * @param failPointNode * @return */ private List findFailPointResult(TridNode failPointNode) { LinkedList result = new LinkedList<>(); while (failPointNode != null) { List temp = failPointNode.getResultSet(); if (temp != null && temp.size() > 0) { result.addAll(temp); } failPointNode = failPointNode.getFailPoint(); } return result; } public static void main(String[] args) { TridForAc ac = new TridForAc<>(); ac.addNode("a-b-c-d".getBytes(), "abc1"); ac.addNode("b-c-d".getBytes(), "abc2"); ac.addNode("c-d".getBytes(), "abc3"); /** * 构建失败指针,即使全匹配和最左前缀匹配也没又问题 * 因为没有把失败指针的结果拿过来 */ ac.buildFailPoint(); // 全匹配和前缀匹配都不需要构建失败指针,纯粹是多叉树操作 List fullSearch1 = ac.fullSearch("a-b-c-d".getBytes()); List fullSearch2 = ac.fullSearch("a-b-c-f".getBytes()); List preResult = ac.preSearch("b-c-d-xx-xx-xx".getBytes()); // 查找100W次,耗时386ms,性能强大 int i = 0; long start = Instant.now().toEpochMilli(); while (i < 1000000) { List search = ac.acSearch("a-b-c-d".getBytes()); i++; } System.out.println(Instant.now().toEpochMilli() - start); } }

4、总结

1、构建字典树。只需要注意root节点不要设置preaentPoint 和 failPoint,否则会在构建FailPoint的时候导致死循环。

2、查找失败指针。父节点的失败指针的子节点中值相等的节点,如果没有,继续找失败指针的失败指针,知道root为止,此时root就是失败指针 叶子节点需要记录resultSet,同时指向失败指针的节点 也要记录失败指针的resultSet。

3、多模式匹配。只需要走一次,累计每一层的匹配的节点resultSet,如果子节点找不到,则跳转到失败指针节点继续查找。

字典树的优点:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

转载于:https://my.oschina.net/liufukin/blog/2222387

你可能感兴趣的:(python,java)