1. Trie:非典型多叉树的奥秘
1.1 为什么说Trie是非典型多叉树?
Trie的"非典型性"体现在其结构设计哲学上:
- 空间换时间:每个节点固定26个子节点指针(对应26个字母)
- 隐式存储:字符信息通过指针数组索引隐式存储(非显式存储字符)
- 路径即信息:从根节点到当前节点的路径构成存储的字符串
1.2 结点结构深度解析
class TrieNode {
// 存储当前节点是否构成完整单词
private boolean isEnd;
// 子节点指针数组(a-z映射到0-25索引)
private TrieNode[] children;
public TrieNode() {
isEnd = false;
children = new TrieNode[26]; // 固定26个子节点槽位
}
}
关键设计考量:
- 字母映射:通过字符-'a’得到0-25的索引值
- 空间预分配:即使某些字母未被使用仍然保留空间
- 状态标记:isEnd标记路径是否构成完整单词
2. Trie的四大核心应用场景
2.1 自动补全系统
- 输入"app"时提示"apple", "application"等
- 实现原理:DFS遍历所有可能分支
2.2 拼写检查
- 判断单词是否存在于词典
- 时间复杂度:O(L) L为单词长度
2.3 IP路由
- 最长前缀匹配查找
- 示例:路由表存储IP段与端口的映射
2.4 输入法预测
- 基于用户输入序列预测候选词
- 可结合词频统计进行优化
3. 手撕Trie三连击
3.1 插入操作
public void insert(String word) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (node.children[index] == null) {
node.children[index] = new TrieNode();
}
node = node.children[index];
}
node.isEnd = true; // 标记单词终点
}
时间复杂度分析:
- 最佳:O(L) 需要插入新分支
- 最差:O(L) 路径已存在
3.2 搜索操作
public boolean search(String word) {
TrieNode node = searchPrefix(word);
return node != null && node.isEnd;
}
private TrieNode searchPrefix(String prefix) {
TrieNode node = root;
for (char ch : prefix.toCharArray()) {
int index = ch - 'a';
if (node.children[index] == null) {
return null;
}
node = node.children[index];
}
return node;
}
失败场景分析:
- 路径不存在(提前终止)
- 路径存在但未标记isEnd
3.3 前缀搜索
public boolean startsWith(String prefix) {
return searchPrefix(prefix) != null;
}
与完全搜索的区别:
- 不检查isEnd标记
- 只需验证路径存在性
4. Trie的时空博弈论
4.1 时间复杂度对比
操作 | 哈希表 | 平衡树 | Trie |
---|---|---|---|
插入 | O(L) | O(LlogN) | O(L) |
查找 | O(L) | O(LlogN) | O(L) |
前缀查找 | 不支持 | 不支持 | O(L) |
4.2 空间复杂度对比
- 哈希表:O(NL) 存储所有字符串
- Trie:最差O(NL) 最佳O(L)(共享前缀)
4.3 适用场景选择指南
- ✅ 推荐使用:高频前缀查询、需要自动补全
- ❌ 不推荐:随机字符串集合、内存敏感场景
5. Trie优化之道
5.1 压缩Trie
- 合并单分支路径
- 示例:将a->p->p->l->e合并为"apple"节点
5.2 双数组Trie
- 使用base和check两个数组存储状态
- 空间利用率提升30%-50%
5.3 动态节点分配
class TrieNode {
Map<Character, TrieNode> children; // 改用HashMap
boolean isEnd;
}
6. 实战:实现敏感词过滤器
public class SensitiveFilter {
private TrieNode root = new TrieNode();
public void addWord(String word) { /* 标准插入逻辑 */ }
public String filter(String text) {
StringBuilder result = new StringBuilder();
int n = text.length();
for (int i = 0; i < n; ) {
TrieNode node = root;
int j = i;
while (j < n && node.children.containsKey(text.charAt(j))) {
node = node.children.get(text.charAt(j));
j++;
}
if (node.isEnd) {
result.append("***");
i = j;
} else {
result.append(text.charAt(i));
i++;
}
}
return result.toString();
}
}
7. Trie的局限性
- 内存消耗:每个节点需要固定空间开销
- 初始化成本:需要预建完整字典树
- 更新代价:动态增删可能导致结构重组
- 字母表限制:需要预先确定字符范围