作者:京东物流 马瑞

1 什么是Trie树

1.1 Trie树的概念

Trie树,即字典树,又称单词查找树或键树,是一种树形结构,典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

Trie, also called digital tree and sometimes radix tree or prefix tree (as they can be searched by prefixes), is a kind of search tree—an ordered tree data structure that is used to store a dynamic set or associative array where the keys are usually strings. It is one of those data-structures that can be easily implemented.

1.2 Trie树优点

最大限度地减少无谓的字符串比较,查询效率比较高。核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

  1. 插入、查找的时间复杂度均为O(N),其中N为字符串长度。
  2. 空间复杂度是26^n级别的,非常庞大(可采用双数组实现改善)。

1.3 Trie树的三个基本性质

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
  3. 每个节点的所有子节点包含的字符都不相同

2 Trie树数据结构

以字符串”hi”和”经海路”的数据为例:

Java的数据结构定义:

@Data
public class TrieTreeNode {
private Character data;
private Map<Character, TrieTreeNode> children;
private boolean isEnd;
// 前缀,冗余信息,可选
private String prefix;
// 后缀,冗余信息,可选
private String suffix;
}

如果只是处理26个英文字符,data可以通过children数组是否为空来判断。如果处理程序,默认children为空来判断是否为最后一个节点,则isEnd字段可以省略。

前缀prefix和suffix可以在数据处理的时候,方便拿到当前节点前缀和后缀信息,如果不需要可以去除。

3 Trie树在脏话过滤中的应用

3.1 脏话关键词Keyword定义

public class KeyWord implements Serializable {
/**
* 关键词内容
*/
private String word;
//其他省略
}

3.2 关键词查询器

public class KWSeeker {

    /**
* 所有的关键词
*/
private Set<KeyWord> sensitiveWords; /**
* 关键词树
*/
private Map<String, Map> wordsTree = new ConcurrentHashMap<String, Map>(); /**
* 最短的关键词长度。用于对短于这个长度的文本不处理的判断,以节省一定的效率
*/
private int wordLeastLen = 0; //其他处理方法,省略
}

3.3 字符串构造一棵树

/**
* 将指定的词构造到一棵树中。
*
* @param tree 构造出来的树
* @param word 指定的词
* @param KeyWord 对应的词
* @return
*/
public static Map<String, Map> makeTreeByWord(Map<String, Map> tree, String word,
KeyWord KeyWord) {
if (StringUtils.isEmpty(word)) {
tree.putAll(EndTagUtil.buind(KeyWord));
return tree;
}
String next = word.substring(0, 1);
Map<String, Map> nextTree = tree.get(next);
if (nextTree == null) {
nextTree = new HashMap<String, Map>();
}
// 递归构造树结构
tree.put(next, makeTreeByWord(nextTree, word.substring(1), KeyWord));
return tree;
}

对关键词字符串,逐个字符进行构建。

3.4 词库树的生成

/**
* 构造、生成词库树。并返回所有敏感词中最短的词的长度。
*
* @param sensitiveWords 词库
* @param wordsTree 聚合词库的树
* @return 返回所有敏感词中最短的词的长度。
*/
public int generalTree(Set<KeyWord> sensitiveWords, Map<String, Map> wordsTree) {
if (sensitiveWords == null || sensitiveWords.isEmpty() || wordsTree == null) {
return 0;
} wordsTreeTmp.clear();
int len = 0;
for (KeyWord kw : sensitiveWords) {
if (len == 0) {
len = kw.getWordLength();
} else if (kw.getWordLength() < len) {
len = kw.getWordLength();
}
AnalysisUtil
.makeTreeByWord(wordsTreeTmp, StringUtils.trimToEmpty(kw.getWord()), kw);
}
wordsTree.clear();
wordsTree.putAll(wordsTreeTmp);
return len;
}

3.5 关键词提取

/**
* 将文本中的关键词提取出来。
*/
public List<SensitiveWordResult> process(Map<String, Map> wordsTree, String text,
AbstractFragment fragment, int minLen) {
// 词的前面一个字
String pre = null;
// 词匹配的开始位置
int startPosition = 0;
// 返回结果
List<SensitiveWordResult> rs = new ArrayList<SensitiveWordResult>(); while (true) {
try {
if (wordsTree == null || wordsTree.isEmpty() || StringUtils.isEmpty(text)) {
return rs;
}
if (text.length() < minLen) {
return rs;
}
String chr = text.substring(0, 1);
text = text.substring(1);
Map<String, Map> nextWord = wordsTree.get(chr);
// 没有对应的下一个字,表示这不是关键词的开头,进行下一个循环
if (nextWord == null) {
pre = chr;
continue;
} List<KeyWord> keywords = Lists.newArrayList();
KeyWord kw = AnalysisUtil.getSensitiveWord(chr, pre, nextWord, text, keywords);
if (keywords == null || keywords.size() == 0) {
// 没有匹配到完整关键字,下一个循环
pre = chr;
continue;
}
for (KeyWord tmp : keywords) {
// 同一个word多次出现记录在一起
SensitiveWordResult result = new SensitiveWordResult(startPosition, tmp.getWord());
int index = rs.indexOf(result);
if (index > -1) {
rs.get(index).addPosition(startPosition, tmp.getWord());
} else {
rs.add(result);
}
} // 从text中去除当前已经匹配的内容,进行下一个循环匹配
// 这行注释了,避免"中国人",导致"国人"。搜索不出来,逐个字符遍历
// text = text.substring(kw.getWordLength() - 1);
pre = kw.getWord().substring(kw.getWordLength() - 1, kw.getWordLength());
continue;
} finally {
if (pre != null) {
startPosition = startPosition + pre.length();
}
} }
} /**
* 查询文本开头的词是否在词库树中,如果在,则返回对应的词,如果不在,则返回null。return 返回找到的最长关键词
*
* @param append 追加的词
* @param pre 词的前一个字,如果为空,则表示前面没有内容
* @param nextWordsTree 下一层树
* @param text 剩余的文本内容
* @param keywords 返回的keywords,可能多个
* @return 返回找到的最长关键词
*/
public static KeyWord getSensitiveWord(String append, String pre,
Map<String, Map> nextWordsTree, String text, List<KeyWord> keywords) {
if (nextWordsTree == null || nextWordsTree.isEmpty()) {
return null;
} Map<String, Object> endTag = nextWordsTree.get(EndTagUtil.TREE_END_TAG);
// 原始文本已到末尾
if (StringUtils.isEmpty(text)) {
// 如果有结束符,则表示匹配成功,没有,则返回null
if (endTag != null) {
keywords.add(checkPattern(getKeyWord(append, endTag), pre, null));
return checkPattern(getKeyWord(append, endTag), pre, null);
} else {
return null;
}
} String next = text.substring(0, 1);
String suffix = text.substring(0, 1);
Map<String, Map> nextTree = nextWordsTree.get(next); // 没有遇到endTag,继续匹配
if (endTag == null) {
if (nextTree != null && nextTree.size() > 0) {
// 没有结束标志,则表示关键词没有结束,继续往下走。
return getSensitiveWord(append + next, pre, nextTree, text.substring(1), keywords);
} // 如果没有下一个匹配的字,表示匹配结束!
return null;
} else { // endTag , 添加关键字
KeyWord tmp = getKeyWord(append, endTag);
keywords.add(checkPattern(tmp, pre, suffix));
} // 有下一个匹配的词则继续匹配,一直取到最大的匹配关键字
KeyWord tmp = null;
if (nextTree != null && nextTree.size() > 0) {
// 如果大于0,则表示还有更长的词,继续往下找
tmp = getSensitiveWord(append + next, pre, nextTree, text.substring(1), keywords);
if (tmp == null) {
// 没有更长的词,则就返回这个词。在返回之前,先判断它是模糊的,还是精确的
tmp = getKeyWord(append, endTag);
}
return checkPattern(tmp, pre, suffix);
} // 没有往下的词了,返回该关键词。
return checkPattern(getKeyWord(append, endTag), pre, suffix); }

思路是对某个字符串text,逐个字符ch,获取ch对应的词库树的children,然后获取匹配到的单个或多个结果,将匹配到的关键词在text中的开始和结束下标进行记录,如后续需要html标记,或者字符替换可直接使用。如果未能在词库树中找到对应的ch的children,或者词库树的children未能匹配到去除ch的子字符串,则继续循环。具体可再详细读一下代码。

4 Radix Tree的应用

4.1 RAX - Redis Tree

Redis实现了不定长压缩前缀的radix tree,用在集群模式下存储slot对应的的所有key信息。

/* Representation of a radix tree as implemented in this file, that contains
* the strings "foo", "foobar" and "footer" after the insertion of each
* word. When the node represents a key inside the radix tree, we write it
* between [], otherwise it is written between ().
*
* This is the vanilla representation:
*
* (f) ""
* \
* (o) "f"
* \
* (o) "fo"
* \
* [t b] "foo"
* / \
* "foot" (e) (a) "foob"
* / \
* "foote" (r) (r) "fooba"
* / \
* "footer" [] [] "foobar"
*
* However, this implementation implements a very common optimization where
* successive nodes having a single child are "compressed" into the node
* itself as a string of characters, each representing a next-level child,
* and only the link to the node representing the last character node is
* provided inside the representation. So the above representation is turned
* into:
*
* ["foo"] ""
* |
* [t b] "foo"
* / \
* "foot" ("er") ("ar") "foob"
* / \
* "footer" [] [] "foobar"
*
* However this optimization makes the implementation a bit more complex.
* For instance if a key "first" is added in the above radix tree, a
* "node splitting" operation is needed, since the "foo" prefix is no longer
* composed of nodes having a single child one after the other. This is the
* above tree and the resulting node splitting after this event happens:
*
*
* (f) ""
* /
* (i o) "f"
* / \
* "firs" ("rst") (o) "fo"
* / \
* "first" [] [t b] "foo"
* / \
* "foot" ("er") ("ar") "foob"
* / \
* "footer" [] [] "foobar"
*
* Similarly after deletion, if a new chain of nodes having a single child
* is created (the chain must also not include nodes that represent keys),
* it must be compressed back into a single node.
*
*/
#define RAX_NODE_MAX_SIZE ((1<<29)-1)
typedef struct raxNode {
uint32_t iskey:1; /* Does this node contain a key? */
uint32_t isnull:1; /* Associated value is NULL (don't store it). */
uint32_t iscompr:1; /* Node is compressed. */
uint32_t size:29; /* Number of children, or compressed string len. */
unsigned char data[];
} raxNode; typedef struct rax {
raxNode *head;
uint64_t numele;
uint64_t numnodes;
} rax; typedef struct raxStack {
void **stack; /* Points to static_items or an heap allocated array. */
size_t items, maxitems; /* Number of items contained and total space. */
void *static_items[RAX_STACK_STATIC_ITEMS];
int oom; /* True if pushing into this stack failed for OOM at some point. */
} raxStack;

如Redis源码中的注释所写,RAX进行了一些优化,并不会将一个字符串直接按照每个字符进行树的构建,而是在Insert有冲突时节点分割处理,在Delete时如果子节点和父节点都只有一个,则需要进行合并操作。

对于RAX有兴趣的同学,可以看一下rax.h、rax.c的相关代码。

4.2 Linux内核

Linux radix树最广泛的用途是用于内存管理,结构address_space通过radix树跟踪绑定到地址映射上的核心页,该radix树允许内存管理代码快速查找标识为dirty或writeback的页。Linux radix树的API函数在lib/radix-tree.c中实现。

Linux基数树(radix tree)是将指针与long整数键值相关联的机制,它存储有效率,并且可快速查询,用于指针与整数值的映射(如:IDR机制)、内存管理等。

struct radix_tree_node {
unsigned int path;
unsigned int count;
union {
struct {
struct radix_tree_node *parent;
void *private_data;
};
struct rcu_head rcu_head;
};
/* For tree user */
struct list_head private_list;
void __rcu *slots[RADIX_TREE_MAP_SIZE];
unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS];
};

关于Linux内核使用Radix Tree的具体代码,有兴趣的同学可以继续深入。

5 总结

Trie树在单词搜索、统计、排序等领域有大量的应用。文章从基础概念到具体的脏话过滤的应用、Redis的RAX和Linux内核的Radix Tree对Trie树做了介绍。数据结构和算法是程序高性能的基础,本文抛砖引玉,希望大家对Trie树有所了解,并在未来开发过程实践和应用Trie树解决中类似情景的问题。

【数据结构和算法】Trie树简介及应用详解的更多相关文章

  1. 数据结构与算法—Trie树

    Trie,又经常叫前缀树,字典树等等.它有很多变种,如后缀树,Radix Tree/Trie,PATRICIA tree,以及bitwise版本的crit-bit tree.当然很多名字的意义其实有交 ...

  2. 数据结构与算法——AVL树类的C++实现

    关于AVL树的简单介绍能够參考:数据结构与算法--AVL树简单介绍 关于二叉搜索树(也称为二叉查找树)能够參考:数据结构与算法--二叉查找树类的C++实现 AVL-tree是一个"加上了额外 ...

  3. 数据结构图文解析之:二叉堆详解及C++模板实现

    0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...

  4. [转]EM算法(Expectation Maximization Algorithm)详解

    https://blog.csdn.net/zhihua_oba/article/details/73776553 EM算法(Expectation Maximization Algorithm)详解 ...

  5. Nginx 反向代理工作原理简介与配置详解

    Nginx反向代理工作原理简介与配置详解   by:授客  QQ:1033553122   测试环境 CentOS 6.5-x86_64 nginx-1.10.0 下载地址:http://nginx. ...

  6. Python聚类算法之基本K均值实例详解

    Python聚类算法之基本K均值实例详解 本文实例讲述了Python聚类算法之基本K均值运算技巧.分享给大家供大家参考,具体如下: 基本K均值 :选择 K 个初始质心,其中 K 是用户指定的参数,即所 ...

  7. 搜索引擎算法研究专题五:TF-IDF详解

    搜索引擎算法研究专题五:TF-IDF详解 2017年12月19日 ⁄ 搜索技术 ⁄ 共 1396字 ⁄ 字号 小 中 大 ⁄ 评论关闭   TF-IDF(term frequency–inverse ...

  8. [数据结构] 2.3 Trie树

    抱歉更新晚了,看了几天三体,2333,我们继续数据结构之旅. 一.什么是Tire树? Tire树有很多名字:字典树.单词查找树. 故名思意,它就是一本”字典“,当我们查找"word" ...

  9. Trie树简介

    Trie树, 即字典树, 又称单词查找树或键树, 多叉树 基本性质 根节点不包含字符,除根节点外每一个节点都只包含一个字符 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串 每个节点 ...

  10. Android版数据结构与算法(一):基础简介

    版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 一.前言 项目进入收尾阶段,忙忙碌碌将近一个多月吧,还好,不算太难,就是麻烦点. 数据结构与算法这个系列早就想写了,一是梳理总结,顺便逼迫自己把一 ...

随机推荐

  1. 中小型企业综合项目(Nginx+LVS+Tomcat+MGR+Nexus+NFS)

    Nginx+Tomcat+Mysql综合实验 1.环境准备 服务器 IP地址 作用 系统版本 数据库服务器1 192.168.100.111 MGR集群数据库master节点 Rocky8.6 数据库 ...

  2. JS逆向实战8——某网实战(基于golang-colly)

    其实本章算不上逆向教程 只是介绍golang的colly框架而已 列表页分析 根据关键字搜索 通过抓包分析可知 下一页所请求的参数如下 上图标红的代表所需参数 所以其实我们真正需要的也就是Search ...

  3. 图数据 3D 可视化在 Explorer 中的应用

    本文首发于 NebulaGraph 公众号 前言图数据可视化是现代 Web 可视化技术中比较常见的一种展示方式,NebulaGraph Explorer 作为基于 NebulaGraph 的可视化产品 ...

  4. js中对小数的计算

    在js 的计算中如果涉及到小数的运算,那结果可不要想当然了,比如  0.1+0.2 的计算 var num1 = 0.1; var num2 = 0.2; console.log(num1+num2) ...

  5. 新建Maui工程运行到IiOS物理设备提示 Could not find any available provisioning profiles for iOS 处理办法

    在构建 MAUI App 或 MAUI Blazor 时,您可能会收到以下 Could not find any available provisioning profiles for iOS. Pl ...

  6. 实现Swaggera的在线接口调试

    1.访问Swagger的路径是:http://localhost:8080/swagger-ui.html 如果项目正常,则可看到如下界面: 2.点开下面的随意一个方法 如add添加数据的方法,展开: ...

  7. 面试 考察网络请求HTTP相关知识(第六天!)

    01.HTTP 常⻅的状态码有哪些? 1xx 服务器收到请求 2xx 请求成功         ---   200 成功状态码 3xx 重定向            ---  301永久重定向,浏览器 ...

  8. Spring学习笔记 - 第一章 - IoC(控制反转)、IoC容器、Bean的实例化与生命周期、DI(依赖注入)

    Spring 学习笔记全系列传送门: 目录 1.学习概述 2.Spring相关概念 2.1 Spring概述 2.1.1 Spring能做的工作 2.1.2 重点学习的内容 2.1.3 Spring发 ...

  9. JDK动态代理深入剖析

    1 基于接口的代理模式 什么是代理? 简单来说,代理是指一个对象代替另一个对象去做某些事情. 例如,对于每个程序员来说,他都有编程的能力: interface Programmable { void ...

  10. 使用Python实现多线程、多进程、异步IO的socket通信

    多线程实现socket通信服务器端代码 import socket import threading class MyServer(object): def __init__(self): # 初始化 ...