你好,我是小黄,一名独角兽企业的Java开发工程师。 感谢茫茫人海中我们能够相遇, 俗话说:当你的才华和能力,不足以支撑你的梦想的时候,请静下心来学习, 希望优秀的你可以和我一起学习,一起努力,实现属于自己的梦想。
一、引言
上周在公司的周会上,谈到这样一件事,对于风控行业来说,关键词匹配是一个比较重要的业务之一,而关键字匹配的效率,也是众多风控者关注的重要指标。
如何使用一种既方便又快捷的方案进行关键字的匹配呢,风控常见做法是 Trie树 ,今天我们就来看一下关键词匹配界的霸主——Trie树(前缀树)。
二、何为关键词匹配
对于风控来说,每天处理的风控信息杂七杂八,其中比较重要的一个业务方向——关键字匹配
简单来说,如果用户输入傻逼、卧槽、我要炸学校.....等一系列的违规词语,我们需要去我们的关键词表中进行匹配,最终返回结果:这是一个违规词语
这样看起来可能不够人性化,我们可以对其关键词表进行 打标签 的操作,将我们关键词表,打上涉政、涉黄、涉恐等一系列的标签。
这样,我们的整套流程看起来已经非常人性化了
三、何为Trie树
字典树,顾名思义,是关于“字典”的一棵树。即:它是对于字典的一种存储方式(所以是一种数据结构而不是算法)。这个词典中的每个“单词”就是从根节点出发一直到某一个目标节点的路径,路径中每条边的字母连起来就是一个单词。
- (标橙色的节点是“目标节点“,即根节点到这个目标节点的路径上的所有字母构成了一个单词。)
从这张图我们可以看出,字典树就是一棵树,只不过,这棵树的每条边上都有一个字母,然后这棵树的一些节点被指定成了标记节点(目标节点)而已。
这就是字典树的概念。结合上面说的概念,上图所示的字典树包括的单词分别为:a
、abc
、bac
、bbc
、ca
Trie树功能如下:
- 维护字符串集合(即字典)
- 向字符串集合中插入字符串(即建树)
- 查询字符串集合中是否有某个字符串(即查询)
- 统计字符串在集合中出现的个数(即统计)
- 将字符串集合按字典序排序(即字典序排序)
- 求集合内两个字符串的LCP(Longest CommonPrefix,最长公共前缀)(即求最长公共前缀)
四、为什么不直接使用HashMap
首先,我们要明确我们的目的,为了业务去追寻相应的技术,才是最佳实现方式
我们要明确一点,对于 HashMap 来说,查询效率为 O(1) 的前提是:忽略单样本的大小
对于这一点,我们来看下面代码:
String类
// 计算字符串的hashCode
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
HashMap类
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我们可以看到 当我们往HashMap存放一个字符串时,他会遍历整个字符串获取其 hashCode
这样的话,如果我们不忽略样本大小的话,那么我们的时间复杂度其实是:O(K)
,K为字符串的大小
如果我们当前查询该字符串出现的频率,我们从技术上使用 HashMap
和 Trie树
区别是不大的,但是在我们的业务落地时,对于关键词那么大的数据,你使用 HashMap
占据的空间岂不是造成浪费
如果我想要查询以 "abc" 为前缀出现的字符串,我们的 HashMap
是不支持的,而 trie树
就很好的支持了这一点
总体而言,对于关键词的匹配,为了避免空间的浪费和业务的多元化,使用 trie树
的效率远大于 HashMap
五、Trie树的代码实现
1、Trie的初始化
我们本文的 Trie树
代码假设所有出现的字符串均为小写字母
- pass:通过该点的字符串数量
- end:结束该点的字符串数量
- tries:子树
class Trie {
int pass;
int end;
Trie[] tries;
public Trie() {
pass = 0;
end = 0;
tries = new Trie[26];
}
}
2、Trie添加字符串
public void insert(String word) {
if (word == null) {
return;
}
char[] str = word.toCharArray();
Trie trie = this;
int path = 0;
for (int i = 0; i < str.length; i++) {
path = str[i] - 'a';
// 看一下子树是不是已经存在,如果不存在,则进行创建
if (trie.tries[path] == null) {
trie.tries[path] = new Trie();
}
trie.pass++;
trie = trie.tries[path];
}
trie.end++;
}
3、Trie查询字符串出现的频率
/**
* 该字符串出现了几次
*
* @param word
* @return
*/
public int search(String word) {
if (word == null) {
return 0;
}
char[] str = word.toCharArray();
Node node = root;
int path = 0;
for (int i = 0; i < str.length; i++) {
path = str[i] - 'a';
if (node.nodes[path] == null) {
return 0;
}
node = node.nodes[path];
}
return node.end;
}
4、Trie删除字符串
- Java选手直接置为
null
,GC即可回收,C++选手要手动回收
public void delete(String word) {
if (search(word) == 0) {
return;
}
char[] str = word.toCharArray();
Node node = root;
int path = 0;
for (int i = 0; i < str.length; i++) {
path = str[i] - 'a';
if (--node.nodes[path].pass == 0) {
// Java垃圾回收直接回收掉
node.nodes[path] = null;
return;
}
node = node.nodes[path];
}
node.end--;
}
六、总结
关于 Trie树
的介绍到这就已经结束了,大家可以思考下 HashMap
和 Trie树
的差距,想一下哪个才是最优解,毕竟博主也有可能写错了呢(题主必不可能写错,逃)
同时,对于 Trie树
的代码也需要多写几遍,博主写这篇博客的时候,又忘记了怎么写(逃
对源代码有兴趣的小伙伴,可以关注 爱敲代码的小黄
公众号,回复:算法源码 即可获得算法源码
本期的内容就到这里,下期 一定一定一定 会讲述 最小生成树(Prim、Kruskal) 算法。
我是一名独角兽企业的Java开发工程师,希望可以点个关注呀,有问题可以留言或者私信加我微信:hls1793929520,我们下期再见!