业务场景

这种需求一般用于敏感词过滤等场景, 输入是大文本, 需要快速判断是否存在匹配的模式串(敏感词), 或者在其中找出所有匹配的模式串. 对于模式串数量不超过5000的场景, 直接用暴力查找速度也能接受, 对于更大规模的模式串, 需要对匹配进行优化.

实现原理

带Fail Next回溯的Trie树结构是常见的实现方法, 算法原理可以自行查找"多模式匹配算法". 在实际使用中, 对于中文的模式串, 因为中文字库很大, 数万的级别, 如果使用单个中文文字作为每个节点的子节点数组, 那么这个数组尺寸会非常大, 同时这个Trie树的深度很小, 最长的中文词字数不过19. 这样造成了很多空间浪费. 在这个实现中, 将字符串统一使用十六进制数组表示, 这样每个节点的子节点数组大小只有16, 同时最大深度变成114, 虽然在计算Fail Next时需要花费更多时间, 但是在空间效率上提升了很多.

模式清洗

对于输入的模式串, 统一转换为byte[], 再转换为十六进制的 int[]

Trie树构造

遍历所有的模式串, 将int[]添加入Trie树, 每个int对应其中的一个node, 将byte[]值写入最后一个节点(叶子节点).

Fail Next构造

Next的定义: 当匹配下一个节点失败时, 模式串应该跳到哪个节点继续匹配.

初始值: Root的Next为空, 第一层的Next都为Root

计算某节点的Next: 取此节点的父节点的Next为Node,

  • 若Node中编号index的子节点存在, 则此子节点就是Next
  • 若不存在, 那么再将Node的Next设为Node, 继续刚才的逻辑
  • 若Node的Next为空, 则以此Node为Next (此时这个Node应当为Root)

对整个Trie树的next赋值必须以广度遍历的方式进行, 因为每一个next的计算, 要基于上层已经设置的next.

文本查找

对输入的文本, 也需要转换为十六进制int[]进行查找. 在每一步, 无论是匹配成功, 还是匹配失败, 都要查看当前节点的next, 以及next的next, 是否是叶子节点, 否则会错过被大模式串包围的小模式串.

代码实现

TrieNode

public class TrieNode {
private byte[] value;
private int freq;
private TrieNode parent;
private TrieNode next;
private TrieNode[] children;
}

TrieMatch

/**
* Efficient multi-pattern matching approach with Trie algorithm
*
* Code example:
* ```
* TrieMatch trie = new TrieMatch();
* trie.initialize("webdict_with_freq.txt");
* Set<String> results = trie.match("any UTF-8 string");
* ```
*/
public class TrieMatch {
private static final Logger logger = LoggerFactory.getLogger(TrieMatch.class);
/** Trie size */
private int size;
/** Trie depth */
private int depth;
/** Trie root node */
private TrieNode root;
/** Queue for span traversal */
private Queue<TrieNode> queue; public TrieMatch() {
root = new TrieNode();
queue = new ArrayDeque<>();
} public TrieNode getRoot() { return root; }
public int getSize() { return size; }
public int getDepth() { return depth; } public Set<String> match(String content) {
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
int[] hex = bytesToHex(bytes);
Set<byte[]> arrays = match(hex);
Set<String> output = new LinkedHashSet<>();
for (byte[] array : arrays) {
if (array == null) {
continue;
}
String string = new String(array, StandardCharsets.UTF_8);
output.add(string);
}
return output;
} /**
* Traverse the Trie tree to find all matched nodes.
*/
public Set<byte[]> match(int[] hex) {
Set<byte[]> output = new LinkedHashSet<>();
TrieNode node = root;
for (int i = 0; i < hex.length;) { if (node.getChildren() != null) {
TrieNode forward = node.getChildren()[hex[i]];
if (forward != null) {
if (forward.getValue() != null) {
output.add(forward.getValue());
}
TrieNode possible = node.getNext();
while (possible != null && possible.getValue() != null) {
output.add(possible.getValue());
possible = possible.getNext();
}
node = forward;
i++;
continue;
}
}
// Move to 'next' node when unmatched
node = node.getNext();
if (node == null) {
node = root;
i++;
} else {
TrieNode possible = node;
while (possible != null && possible.getValue() != null) {
output.add(possible.getValue());
possible = possible.getNext();
}
}
}
return output;
} public void print() {
queue.clear();
queue.add(root);
TrieNode node;
while ((node = queue.poll()) != null) {
logger.debug(node.toString());
if (node.getChildren() != null) {
TrieNode[] children = node.getChildren();
for (int i = 0; i < children.length; i++) {
if (children[i] != null) {
queue.add(children[i]);
}
}
}
}
} public void initialize(String filepath) {
try {
BufferedReader reader = new BufferedReader(new FileReader(filepath));
String line;
while ((line = reader.readLine()) != null) {
String[] array = line.split("\\s+");
if (array.length != 2) {
logger.debug("Error: " + line);
continue;
}
int freq = Integer.parseInt(array[1]);
append(array[0], freq);
}
reader.close(); queue.clear();
queue.add(root);
TrieNode node;
while ((node = queue.poll()) != null) {
fillNext(node);
} } catch (IOException e) {
logger.debug(e.getMessage());
}
} private void append(String word, int freq) {
byte[] bytes = word.getBytes(StandardCharsets.UTF_8);
int[] hex = bytesToHex(bytes);
append(hex, freq);
} private void append(int[] hex, int freq) {
if (hex.length > depth) { depth = hex.length; }
TrieNode parent = root;
for (int i = 0; i < hex.length; i++) {
int index = hex[i];
if (index > 16) {
logger.debug("Error: index exceeds 16");
continue;
}
if (parent.getChildren() == null) {
parent.setChildren(new TrieNode[16]);
}
TrieNode pos = parent.getChildren()[index];
if (pos == null) {
size++;
pos = new TrieNode();
pos.setParent(parent);
parent.getChildren()[index] = pos;
}
if (i == hex.length - 1) {
pos.setValue(hexToBytes(hex));
pos.setFreq(freq);
}
parent = pos;
}
} private void fillNext(TrieNode node) {
if (node.getChildren() != null) {
TrieNode[] children = node.getChildren();
for (int i = 0; i < children.length; i++) {
if (children[i] != null) {
TrieNode next = getNext(node, i);
children[i].setNext(next);
}
}
for (int i = 0; i < children.length; i++) {
if (children[i] != null) {
queue.add(children[i]);
}
}
}
} /**
* Definition of 'next': When failed matching this node, the patten should continue from which one
* Initialize: root.next = null, [direct descendent].next = root
* Calculate: Set node = parent.next (at the moment parent.next should have been set)
* - if node.children[index] exists, then this child is the next
* - if not, then set node = node.next, continue above searching
* - if node.next is null, it should have reach the root, just return this node
*/
private TrieNode getNext(TrieNode node, int index) {
if (node.getNext() == null) { // This should be root
return node;
}
node = node.getNext();
if (node.getChildren() != null) {
TrieNode next = node.getChildren()[index];
if (next != null) {
return next;
} else {
return getNext(node, index);
}
} else {
return getNext(node, index);
}
} private int[] bytesToHex(byte[] bytes) {
int[] ints = new int[bytes.length * 2];
for (int i = 0; i < bytes.length; i++) {
ints[i * 2 + 1] = bytes[i] & 0x0f;
ints[i * 2] = (bytes[i] >> 4) & 0x0f;
}
return ints;
} private byte[] hexToBytes(int[] hex) {
byte[] bytes = new byte[hex.length / 2];
for (int i = 0; i < bytes.length; i++) {
int a = (hex[i * 2 ] << 4) + hex[i * 2 + 1];
bytes[i] = (byte)a;
}
return bytes;
}
}

  

多模式匹配的Trie实现的更多相关文章

  1. [LA 3942] Remember the Word

    Link: LA 3942 传送门 Solution: 感觉自己字符串不太行啊,要加练一些蓝书上的水题了…… $Trie$+$dp$ 转移方程:$dp[i]=sum\{ dp[i+len(x)+1]\ ...

  2. [转]双数组TRIE树原理

    原文名称: An Efficient Digital Search Algorithm by Using a Double-Array Structure 作者: JUN-ICHI AOE 译文: 使 ...

  3. Trie三兄弟——标准Trie、压缩Trie、后缀Trie

    1.Trie导引 Trie树是一种基于树的数据结构,又称单词查找树.前缀树,字典树,是一种哈希树的变种.应用于字符串的统计与排序,经常被搜索引擎系统用于文本词频统计.用于存储字符串以便支持快速模式匹配 ...

  4. 从Trie树到双数组Trie树

    Trie树 原理 又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种.它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,能在常数时间O(len)内实现插入和查 ...

  5. AC自动机——1 Trie树(字典树)介绍

    AC自动机——1 Trie树(字典树)介绍 2013年10月15日 23:56:45 阅读数:2375 之前,我们介绍了Kmp算法,其实,他就是一种单模式匹配.当要检查一篇文章中是否有某些敏感词,这其 ...

  6. [模式匹配] AC 自动机 模式匹配

    广义的模式匹配: https://en.wikipedia.org/wiki/Pattern_matching 字符串模式匹配: https://en.wikipedia.org/wiki/Strin ...

  7. [knowledge][模式匹配] 字符匹配/模式匹配 正则表达式 自动机

    字符串 T = abcabaabcabac,字符串 P = abaa,判断P是否是T的子串,就是字符串匹配问题了,T 叫做文本(Text) ,P 叫做模式(Pattern),所以正确描述是,找出所有在 ...

  8. 从Trie树(字典树)谈到后缀树

    转:http://blog.csdn.net/v_july_v/article/details/6897097 引言 常关注本blog的读者朋友想必看过此篇文章:从B树.B+树.B*树谈到R 树,这次 ...

  9. Trie树(转:http://blog.csdn.net/arhaiyun/article/details/11913501)

    Trie 树, 又称字典树,单词查找树.它来源于retrieval(检索)中取中间四个字符构成(读音同try).用于存储大量的字符串以便支持快速模式匹配.主要应用在信息检索领域. Trie 有三种结构 ...

  10. Trie 图

    时间限制:20000ms 单点时限:1000ms 内存限制:512MB 描述 前情回顾 上回说到,小Hi和小Ho接受到了河蟹先生伟大而光荣的任务:河蟹先生将要给与他们一篇从互联网上收集来的文章,和一本 ...

随机推荐

  1. 【SHELL】跨行内容查找、替换、删除

    跨行内容查找.替换.删除 sed '/START-TAG/{:a;N;/END-TAG/!ba};/ID: 222/d' data.txt /START-TAG/ { # Match 'START-T ...

  2. java - 运行可执行文件 (.exe)

    package filerun; import java.io.File; import java.io.IOException; public class RunExe { public stati ...

  3. 最新版TikTok 抖音国际版解锁版 v33.1.4 去广告 免拔卡

    软件简介: 抖音国际版App是全球最受欢迎的短视频应用,抖音国际版TikTok(海外版)横扫全球下载量常居榜首.这是最新抖音国际版解锁版,无视封锁和下载限制,国内免拔卡,去除了广告,下载视频无水印(T ...

  4. [转帖]容器环境的JVM内存设置最佳实践

    https://cloud.tencent.com/developer/article/1585288 Docker和K8S的兴起,很多服务已经运行在容器环境,对于java程序,JVM设置是一个重要的 ...

  5. [转帖]一文看懂Linux内核页缓存(Page Cache)

    https://kernel.0voice.com/forum.php?mod=viewthread&tid=629   玩转Linux内核 发布于 2022-8-9 22:19:08 阅读  ...

  6. [转帖]sqlplus与shell互相传值的几种情况

    https://www.cnblogs.com/youngerger/p/9068888.html sqlplus与shell互相传值的几种情况 情况一:在shell中最简单的调用sqlplus $c ...

  7. [转帖]总结:nginx502:Tomcat调优之acceptCount

    问题背景:UI页面点击会偶尔返回error,检查调用日志,发现nginx报502报错,因此本文即排查502报错原因. 如下红框可知,访问本机个备机的服务502了,用时3秒左右(可见并不是超时) 先给出 ...

  8. [转帖]【JVM】JVM概述

    1.JVM定义 JVM 是Java Virtual Machine(JVM )的缩写,Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令进行执行,这样实现了Java"一次编译, ...

  9. Intel 第四代志强可扩展SKU

  10. Mysql数据库查看Database的大小的方法

    最简单的方法为: select concat(round(sum(data_length/1024/1024),2),'MB') as data from INFORMATION_SCHEMA.tab ...