初入数据结构的前缀/字典树 ( Trie Tree ) 及实现
如果觉得对你有帮助,能否点个赞或关个注,以示鼓励笔者呢?!博客目录 | 先点这里
- 前提概念
- 什么是字典树?
- 字典树的优缺点?
- 实现
- 约束和需求
- 代码实现
前提概念
什么是字典树?
字典树 (Trie Tree),又称前缀树,单词查找树。典型应用是用于统计,排序和保持大量字符串,所以经常被搜索引擎系统用于文本词频统计。它的核心优势就在于利用字符串的公共前缀来减少查询时间,最大限度的减少无谓的的字符串比较
字典树 - @百度百科
特征
- 字典树是一颗多路树
- 根结点不包含字符,除根结点外的所有结点都包含一个字符
应用场景
- K/V 存储
- 敏感词过滤
- 采用字典树进行敏感词过滤,相比哈希表可以节省空间
- 词频统计
- 同样相比哈希表,更加的节省空间
- 字典序排序
- 字典树前序遍历
- 前缀匹配
- 搜索引擎,搜索提示
字典树的优缺点
从字典树的介绍,我们知道了什么是字典树。那么字典树相比其他数据结构,有什么好处和作用?
字典树说白了就是一种以空间换时间的特定场合数据结构,在特定的场合下,可以有比较良好的查询优势
敏感词过滤 (n 个敏感词)
- 线性表查找时间复杂度 O(n)
- 二叉树查找时间 O(logn)
- 哈希表查找时间 O(1)
- 字典树查找时间复杂度 O(h), h 为单词长度
通常情况下h < n, 所以字典树的查询效率肯定是远与线性表和二叉树的。 但略低于哈希表。当然也不一定,因为哈希表存在哈希冲突,在哈希冲突大的情况下,哈希表的查询效率不一定比字典树要高 (当冲突时的链表长度或树高度大于单词长度)
联想词
- 线性表需要扫全表 O(n)
- 二叉树需要扫全树 O(logn)
- 哈希表不太好实现
- 字典树查找时间最坏情况 O((h + x * y) , h 是单词长度, x 是关联词延伸多少个字符, y 是有多少种字符
假设100w 的单词数据,单词平均长度 h = 10, 单词只有 26 个字符,只联系延伸 3 个字符, 字典树只需要最坏查询 88 次即可,相比线性表的 10w 次和 log10w 次,简直是性能大大提升,所以联想词其实属于字典树的优势场景
不适合的场景
- 如果输入的数据,长度比较长,比如有千个,万个单位长度,其实就不适用使用字典树去存储,这会导致树深过大
实现
约束与需求
约束
简单点实现一个字典树,所以先放上约束
- 该字典树仅存储
[a-z]26 个字符的数据 - 单个单词长度
1 < len <= 100
需求
- 词频统计
count() - 是否存在
contains() - 是否存在以 xxx 开头的词
startWith() - 提供相关词
search()
代码实现
字典树结点定义
public static class TrieNode {
private boolean end;
private char data;
private int count;
private TrieNode[] next = new TrieNode[26];
public TrieNode(char data) {
this.data = data;
}
}
end代表根结点到当前结点,是否构成一个单词data代表该结点存储的字符数据[a-z]count如果该结点 end = true, 那么 count 就有意义了,代表该单词在该字典树出现的次数next代表 26 个子结点的指针- 为什么是 26 个子结点,因为一个结点的后继字符,有 26 种可能;又因为我们基于数组 (静态定长), 所以只能一开始就分配 26 长度子结点指针数组
- 如果不基于数组实现,其实也可以引入动态数组,List,甚至 HashMap 或是 TreeMap
字典树定义
public class TrieTree {
/**
* root node,meaningless dummy node
*/
private TrieNode root;
public TrieTree() {
root = new TrieNode('*');
}
}
字典树的定义非常简单
- 一个的根结点,不存储任何的数据
- 可以理解为一个没有数据的哑结点
- 作用就仅仅是让我们找到这颗树的头部
- 一个构造函数,用于创建一棵空的字典树,然后就可以为所欲为了
插入单词
public void insert(String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
int index = word.charAt(i) - 'a';
if (node.next[index] == null) {
node.next[index] = new TrieNode(word.charAt(i));
}
node = node.next[index];
}
node.end = true;
++node.count;
}
插入单词的逻辑也很简单,基本思路就是逐个字符寻址字典树的结点路径
- 循环 word 的字符长度次数,因为如果字典树存在该单词,那么该单词至少有 word.length() + 1 个结点构成 (1 是根结点)
- 然后通过
word.charAt(i) - 'a'得到该word.chatAt(i)字符在当前结点的 26 个子结点的数组索引- 因为
[a-z]26 个字符中,是有字典序的,a - a = 0, b - a = 1, 那么 a 字符就是 next[0] 结点,b 字符就是 next[1] 结点 - 如果我们使用动态数组,就不需要通过这样的方式来获取索引
- 因为
- 在迭代的过程中,判断是否有 node.next[index] == null
- 如果为真,则代表字典树中,存在 word 的部分字符,但不存在该单词,则需要继续构造剩余字符的结点
- 如果不为真,则代表字典树中,存在 word 的当前字符,需要继续迭代判断
- 循环结束后,则代表 word 本身就存在字典中,或是本来不存在,但已经构造完成。则 end = true, count++

本文详细介绍了前缀树(TrieTree)的概念、优点、应用场景及其实现。通过实例展示了如何用Java实现字典树,包括插入单词、查找、词频统计和联想词等功能。字典树是一种高效的数据结构,适用于字符串的快速查找和联想,尤其在搜索引擎和敏感词过滤等方面有广泛应用。
最低0.47元/天 解锁文章
5337

被折叠的 条评论
为什么被折叠?



