多模式匹配的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接受到了河蟹先生伟大而光荣的任务:河蟹先生将要给与他们一篇从互联网上收集来的文章,和一本 ...
随机推荐
- 问题--QT只有全屏的时候才能使用
1.问题 安装的版本是3.8.0,只有在全屏的时候在编辑界面不会卡,其余情况会直接卡死在这. 2.解决方式 安装了较低版本的3.14.2,解决了上述问题
- org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 2
1.报错 在运行SpringBoot项目时遇到报错: 17:44:47.558 [main] ERROR org.springframework.boot.SpringApplication -- A ...
- mysql-字符函数-拼接-长度-切片-替换
- Mygin实现简单的路由
本文是Mygin第二篇 目的: 实现路由映射 提供了用户注册静态路由方法(GET.POST方法) 基于上一篇 Mygin 实现简单Http 且参照Gin 我使用了map数组实现简单路由的映射关系 不同 ...
- [转帖]第24/24周 数据库维护(Database Maintenance)
https://www.cnblogs.com/woodytu/p/4795542.html 哇哦,光阴似箭!欢迎回到性能调优培训的最后一期.今天我会详细讲下SQL Server里的数据库维护,尤其是 ...
- [转帖]Oracle-UNDO篇
原文地址:https://www.modb.pro/db/70802?xzs= 一:请描述什么是Oracle Undo. 二:请描述UNDO的作用. 三:请谈谈你对Manual Undo Mana ...
- [转帖]tidb RESTORE
https://docs.pingcap.com/zh/tidb/v4.0/sql-statement-restore RESTORE 语句用于执行分布式恢复,把 BACKUP 语句生成的备份文件恢复 ...
- [转帖]exportfs命令
https://www.cnblogs.com/xzlive/p/9766388.html exportfs命令:功能说明 :NFS共享管理语法格式exportfs [必要参数][选择参数][目录]功 ...
- 源码补丁神器—patch-package
一.背景 vue项目中使用 vue-pdf第三方插件预览pdf,书写业务代码完美运行,pdf文件内容正常预览无问题.后期需求有变,业务需求增加电子签章功能.这个时候pdf文件的内容可以显示出来,但是公 ...
- jenkins上发布项目后将文件推送到另一台服务器build镜像
1.配置jenkins 1)主页面点击manage jenkins 2) 选择SystemConfiguration 3) 配置服务器信息,注意密码一定要输入对,还有端口有,一般是默认的22端口,但有 ...