多模式匹配的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接受到了河蟹先生伟大而光荣的任务:河蟹先生将要给与他们一篇从互联网上收集来的文章,和一本 ...
随机推荐
- 【TouchGFX 】使用 CubeMX 创建 TouchGFX 工程时 LCD 死活不显示
生成的代码死活无法让LCD显示,经两个晚上的分析验证是LTDC_CLK引脚速度设置为低速导致,经测试中速.高速.超高速都正常,真是冤,聊以此以示纪念
- [java] - servlet路径跳转
Index.jsp <a href="servlet/HelloServlet">servlet/HelloServlet</a><br> &l ...
- Git-远程仓库-remote-pull-push
- [转帖]能使 Oracle 索引失效的六大限制条件
Oracle 索引的目标是避免全表扫描,提高查询效率,但有些时候却适得其反. 例如一张表中有上百万条数据,对某个字段加了索引,但是查询时性能并没有什么提高,这可能是 oracle 索引失效造成的.or ...
- [转帖]SQL SERVER中什么情况会导致索引查找变成索引扫描
https://www.cnblogs.com/kerrycode/p/4806236.html SQL Server 中什么情况会导致其执行计划从索引查找(Index Seek)变成索引扫描(Ind ...
- [转帖]shell 实现行转列、列转行的几种方法
目录 shell 实现行转列.列转行的几种方法 awk 行转列 xargs 行转列 tr 列转行 参考资料 shell 实现行转列.列转行的几种方法 awk 行转列 以空格为分隔符 awk -F &q ...
- [转帖] 使用uniq命令求并集交集差集
原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. uniq# uniq是linux上非常有用的一个命令,从字面意思上就能看出来,它可以用来去重. 但使用uniq的前提 ...
- IPV6的简单学习与整理
背景 大概2018年时曾经突击学习过一段时间IPV6 当时没太有写文档的习惯,导致这边没有成型的记录了. 今天又有项目要求使用IPV6, 想了想就将之前学习的部分 还有想继续学习提高的部分进行一下总结 ...
- MySQL控制权限
编写顺序和执行顺序是不一样的 编写顺序: SELECT 字段列表 FROM 表名列表 WHERE 条件列表 GROUP BY 分组字段列表 HAVING 分组后条件列表 ORDER BY 排序字段列表 ...
- 从零开始配置 vim(16)——启动界面配置
不知道各位小伙伴用过 spacevim 或者 LunarVim 又或者 doomvim 或者其他的什么 vim 配置框架,我们发现他们的启动界面都做的比较好看,而我们默认进入的启动界面则显得比较素了. ...