多模式匹配的Trie实现
业务场景
这种需求一般用于敏感词过滤等场景, 输入是大文本, 需要快速判断是否存在匹配的模式串(敏感词), 或者在其中找出所有匹配的模式串. 对于模式串数量不超过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实现的更多相关文章
- [LA 3942] Remember the Word
Link: LA 3942 传送门 Solution: 感觉自己字符串不太行啊,要加练一些蓝书上的水题了…… $Trie$+$dp$ 转移方程:$dp[i]=sum\{ dp[i+len(x)+1]\ ...
- [转]双数组TRIE树原理
原文名称: An Efficient Digital Search Algorithm by Using a Double-Array Structure 作者: JUN-ICHI AOE 译文: 使 ...
- Trie三兄弟——标准Trie、压缩Trie、后缀Trie
1.Trie导引 Trie树是一种基于树的数据结构,又称单词查找树.前缀树,字典树,是一种哈希树的变种.应用于字符串的统计与排序,经常被搜索引擎系统用于文本词频统计.用于存储字符串以便支持快速模式匹配 ...
- 从Trie树到双数组Trie树
Trie树 原理 又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种.它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,能在常数时间O(len)内实现插入和查 ...
- AC自动机——1 Trie树(字典树)介绍
AC自动机——1 Trie树(字典树)介绍 2013年10月15日 23:56:45 阅读数:2375 之前,我们介绍了Kmp算法,其实,他就是一种单模式匹配.当要检查一篇文章中是否有某些敏感词,这其 ...
- [模式匹配] AC 自动机 模式匹配
广义的模式匹配: https://en.wikipedia.org/wiki/Pattern_matching 字符串模式匹配: https://en.wikipedia.org/wiki/Strin ...
- [knowledge][模式匹配] 字符匹配/模式匹配 正则表达式 自动机
字符串 T = abcabaabcabac,字符串 P = abaa,判断P是否是T的子串,就是字符串匹配问题了,T 叫做文本(Text) ,P 叫做模式(Pattern),所以正确描述是,找出所有在 ...
- 从Trie树(字典树)谈到后缀树
转:http://blog.csdn.net/v_july_v/article/details/6897097 引言 常关注本blog的读者朋友想必看过此篇文章:从B树.B+树.B*树谈到R 树,这次 ...
- Trie树(转:http://blog.csdn.net/arhaiyun/article/details/11913501)
Trie 树, 又称字典树,单词查找树.它来源于retrieval(检索)中取中间四个字符构成(读音同try).用于存储大量的字符串以便支持快速模式匹配.主要应用在信息检索领域. Trie 有三种结构 ...
- Trie 图
时间限制:20000ms 单点时限:1000ms 内存限制:512MB 描述 前情回顾 上回说到,小Hi和小Ho接受到了河蟹先生伟大而光荣的任务:河蟹先生将要给与他们一篇从互联网上收集来的文章,和一本 ...
随机推荐
- ASIC 功能验证VTB
目标 设计流程 验证设计文档和RTL code之间的关系 RTL code(DUT) - 可以当作是一个黑盒,DUT内部是完全不可见的 白盒验证 - DUT内部RTL完全可见 灰盒验证 - DUT内部 ...
- 【转】获取本地图片的URL
在写博客插入图片时,许多时候需要提供图片的url地址.作为菜鸡的我,自然是一脸懵逼.那么什么是所谓的url地址呢?又该如何获取图片的url地址呢? 首先来看一下度娘对url地址的解释:url是 ...
- Jquery - 获取所有子节点 ( 并删除 )
1,获取所有子节点 $(".parent").find('.child') 2,获取所有子节点,通过上层 div 的类名 , 获取上层 div 节点 $(".pare ...
- 云服务器搭建自己的GitServer!
云服务器搭建自己的GitServer! 如果你有一台云服务器并想在上面搭建自己的 Git 服务器,你可以使用 Git 自带的 git-shell ,也可以使用像 Gitea.GitLab.Gogs 这 ...
- [转帖]jmeter之发送jdbc请求--06篇
1.setup线程组中新建一个JDBC Connection Configuration配置元件 2.设置配置信息 Database URL:jdbc:mysql://127.0.0.1:3306/v ...
- [转帖] 如何kill一条TCP连接?
https://www.cnblogs.com/codelogs/p/16838850.html 原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 简介# 如果你的程序写 ...
- [转帖]使用Transformers推理
https://github.com/ymcui/Chinese-LLaMA-Alpaca/wiki/%E4%BD%BF%E7%94%A8Transformers%E6%8E%A8%E7%90%86 ...
- [转帖]基于腾讯云微服务引擎(TSE) ,轻松实现云上全链路灰度发布
https://my.oschina.net/u/4587289/blog/8570699 1. 概述 软件开发过程中,应用发布非常频繁,通常情况下,开发或运维人员会将系统里所有服务同时上线,使得所 ...
- 如何写出高质量的代码 data 组件 函数 注释 命名 变量的次数
今天在将以前文件上传的地方全部 改为新的文件上传的api. 在改动的过程中,发现代码有很多不合理的地方 在改的时候,因此也是非常的痛苦的哈. 比如说在data中我有太多的flag标识.俩控制元素的显示 ...
- SqlSugar导航查询/多级查询
1.导航查询特点 作用:主要处理主对象里面有子对象这种层级关系查询 1.1 无外键开箱就用 其它ORM导航查询 需要 各种配置或者外键,而SqlSugar则开箱就用,无外键,只需配置特性和主键就能使用 ...