Trie 树
1、Trie 树的定义
Trie 树(又叫「前缀树」或「字典树」)是一种用于快速查询「某个字符串/字符前缀」是否存在的数据结构。
Trie 是一颗非典型的多叉树模型。
其核心是使用「边」来代表有无字符,使用「点」来记录是否为「单词结尾」以及「其后续字符串的字符是什么」。
2、Trie 树的结构
2.1 二维数组
一个朴素的想法是直接使用「二维数组」来实现 Trie 树。
- 使用二维数组 trie[] 来存储我们所有的单词字符。
- 使用 index来自增记录我们到底用了多少个格子(相当于给被用到格子进行编号)。
- 使用 count[]数组记录某个格子被「被标记为结尾的次数」(当 id 编号的格子被标记了 n 次,则有 cnt[idx]=n)。
class Trie {
int N = 100009; // 直接设置为十万级
int[][] trie;
int[] count;
int index;
public Trie() {
trie = new int[N][26];
count = new int[N];
index = 0;
}
public void insert(String s) {
int p = 0;
for (int i = 0; i < s.length(); i++) {
int u = s.charAt(i) - 'a';
if (trie[p][u] == 0) trie[p][u] = ++index;
p = trie[p][u];
}
count[p]++;
}
public boolean search(String s) {
int p = 0;
for (int i = 0; i < s.length(); i++) {
int u = s.charAt(i) - 'a';
if (trie[p][u] == 0) return false;
p = trie[p][u];
}
return count[p] != 0;
}
public boolean startsWith(String s) {
int p = 0;
for (int i = 0; i < s.length(); i++) {
int u = s.charAt(i) - 'a';
if (trie[p][u] == 0) return false;
p = trie[p][u];
}
return true;
}
}
- 时间复杂度:Trie 树的每次调用时间复杂度取决于入参字符串的长度。复杂度为 O(Len)。
- 空间复杂度:二维数组的高度为 n,字符集大小为 k。复杂度为 O(nk)。
2.2 TrieNode
相比二维数组,更加常规的做法是建立 TrieNode 结构节点。
随着数据的不断插入,根据需要不断创建 TrieNode 节点。
class Trie {
class TrieNode {
boolean end;
TrieNode[] tns = new TrieNode[26];
}
TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String s) {
TrieNode p = root;
for(int i = 0; i < s.length(); i++) {
int u = s.charAt(i) - 'a';
if (p.tns[u] == null) p.tns[u] = new TrieNode();
p = p.tns[u];
}
p.end = true;
}
public boolean search(String s) {
TrieNode p = root;
for(int i = 0; i < s.length(); i++) {
int u = s.charAt(i) - 'a';
if (p.tns[u] == null) return false;
p = p.tns[u];
}
return p.end;
}
public boolean startsWith(String s) {
TrieNode p = root;
for(int i = 0; i < s.length(); i++) {
int u = s.charAt(i) - 'a';
if (p.tns[u] == null) return false;
p = p.tns[u];
}
return true;
}
}
时间复杂度:Trie 树的每次调用时间复杂度取决于入参字符串的长度。复杂度为 O(Len)。
空间复杂度:结点数量为 n,字符集大小为 k。复杂度为 O(nk)。
2.3 两种方式的对比
使用「二维数组」的好处是写起来飞快,同时没有频繁 new对象的开销。但是需要根据数据结构范围估算我们的「二维数组」应该开多少行。
坏处是使用的空间通常是「TrieNode」方式的数倍,而且由于通常对行的估算会很大,导致使用的二维数组开得很大,如果这时候每次创建 Trie对象时都去创建数组的话,会比较慢,而且当样例多的时候甚至会触发 GC(因为 OJ每测试一个样例会创建一个 Trie对象)。
因此还有一个小技巧是将使用到的数组转为静态,然后利用 index自增的特性在初始化 Trie 时执行清理工作 & 重置逻辑。
这样的做法能够使评测时间降低一半,运气好的话可以得到一个「TrieNode」方式差不多的时间。
3、Trie 的应用
首先,在纯算法领域,前缀树算是一种较为常用的数据结构。
不过如果在工程中,不考虑前缀匹配的话,基本上使用 hash 就能满足。
如果考虑前缀匹配的话,工程也不会使用 Trie 。
一方面是字符集大小不好确定(题目只考虑 26 个字母,字符集大小限制在较小的 26 内)因此可以使用 Trie,但是工程一般兼容各种字符集,一旦字符集大小很大的话,Trie 将会带来很大的空间浪费。
另外,对于个别的超长字符 Trie 会进一步变深。
这时候如果 Trie 是存储在硬盘中,Trie 结构过深带来的影响是多次随机 IO,随机 IO 是成本很高的操作。
同时 Trie 的特殊结构,也会为分布式存储将会带来困难。
因此在工程领域中 Trie 的应用面不广。
至于一些诸如「联想输入」、「模糊匹配」、「全文检索」的典型场景在工程主要是通过 ES (ElasticSearch) 解决的。
而 ES 的实现则主要是依靠「倒排索引」。
本文作者:王陸
本文链接:https://www.cnblogs.com/wkfvawl/p/16706561.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2021-09-19 uWSGI+Nginx部署Django项目
2019-09-19 Java题库——Chapter4 循环
2018-09-19 USACO 1.2.2 Transformations 方块转换