一、解题思维总结
1. 何时使用字典树(Trie)?
| 场景 | 解法 |
|---|---|
| 字符串集合的快速插入和查找 | 使用 Trie 数据结构,时间复杂度 O(m),m 为字符串长度 |
| 前缀匹配问题(自动补全、拼写检查) | Trie 的 startsWith 操作,高效判断前缀是否存在 |
| 带通配符的字符串搜索 | Trie + DFS 递归搜索,处理通配符的多分支情况 |
| 字符串集合的存储优化 | Trie 共享公共前缀,节省存储空间 |
2. 复杂度分析
- 插入操作时间复杂度:O(m),m 为字符串长度
- 搜索操作时间复杂度:O(m)
- 前缀查找时间复杂度:O(m)
- 空间复杂度:O(N×M),N 为字符串数量,M 为平均长度(共享前缀时实际更小)
3. 常用技巧
- 节点结构:使用对象嵌套
node[char] = {},每个节点代表一个字符 - 结束标志:用
isEnd或isWord属性标记单词结束 - 字符遍历:使用
for...of遍历字符串的每个字符 - 节点跳转:
node = node[char]移动到子节点继续处理 - DFS 搜索:递归遍历所有子节点,使用 index 参数避免字符串切片
二、核心技术
技巧一:Trie 基本结构实现
适用场景:需要高效存储和检索字符串集合,支持插入、精确搜索、前缀查找
核心要点:
- 每个节点是一个对象,键为字符,值为子节点对象
- 从根节点到任意节点的路径组成一个字符串前缀
- isEnd 标志位标记单词结束,区分 "app" 和 "apple"
- 初始化时创建空根节点
this.root = {}
典型例题:实现 Trie (前缀树)
var Trie = function() {
this.root = {};
};
Trie.prototype.insert = function(word) {
let node = this.root;
for (let char of word) {
if (!node[char]) {
node[char] = {};
}
node = node[char];
}
node.isEnd = true;
};
Trie.prototype.search = function(word) {
let node = this.root;
for (let char of word) {
if (!node[char]) {
return false;
}
node = node[char];
}
return node.isEnd === true;
};
Trie.prototype.startsWith = function(prefix) {
let node = this.root;
for (let char of prefix) {
if (!node[char]) {
return false;
}
node = node[char];
}
return true;
};
技巧二:带通配符的 DFS 搜索
适用场景:字符串搜索支持通配符(如 '.' 匹配任意字符),需要遍历所有可能的分支
核心要点:
- 通配符时遍历当前节点的所有子节点,递归搜索
- 使用 index 参数而非字符串切片,避免额外空间开销
- DFS 函数签名:
dfs(node, word, index) - 跳过 isWord 等标志属性,只处理字符键
- 任意分支返回 true 即整体返回 true(短路优化)
典型例题:添加与搜索单词 - 数据结构设计
var WordDictionary = function() {
this.root = {};
};
WordDictionary.prototype.addWord = function(word) {
let node = this.root;
for (let char of word) {
if (!node[char]) {
node[char] = {};
}
node = node[char];
}
node.isWord = true;
};
WordDictionary.prototype.search = function(word) {
return this.dfs(this.root, word, 0);
};
WordDictionary.prototype.dfs = function(node, word, index) {
if (index === word.length) {
return node.isWord === true;
}
const char = word[index];
if (char === '.') {
for (let childChar in node) {
if (childChar === 'isWord') {
continue;
}
if (this.dfs(node[childChar], word, index + 1)) {
return true;
}
}
} else {
if (node[char] && this.dfs(node[char], word, index + 1)) {
return true;
}
}
return false;
};
技巧三:节点创建与跳转模式
适用场景:Trie 的插入和搜索操作中,需要创建新节点或移动到已存在节点
核心要点:
- 插入时:不存在则创建
if (!node[char]) node[char] = {} - 搜索时:不存在则返回 false
if (!node[char]) return false - 统一跳转:
node = node[char] - 先检查后跳转,避免访问 undefined
模板代码:
// 插入模式
let node = this.root;
for (let char of word) {
if (!node[char]) {
node[char] = {}; // 不存在则创建
}
node = node[char]; // 跳转到子节点
}
node.isEnd = true; // 标记结束
// 搜索模式
let node = this.root;
for (let char of word) {
if (!node[char]) {
return false; // 不存在则失败
}
node = node[char]; // 跳转到子节点
}
return node.isEnd === true; // 检查结束标志
三、易错点提醒
-
不要用数组简单存储:虽然可以通过测试,但失去了 Trie 的核心优势(前缀共享、快速前缀查找),时间复杂度退化为 O(n×m)
-
isEnd/isWord 的判断:节点没有 isEnd 属性时返回 undefined,需要用
=== true或!!转换为布尔值 -
DFS 遍历子节点时跳过标志属性:使用
for...in遍历会包含 isWord 等属性,需要if (childChar === 'isWord') continue跳过 -
前缀查找 vs 精确搜索:startsWith 只需遍历完前缀即可返回 true,search 需要额外检查 isEnd 标志
-
通配符搜索的终止条件:index === word.length 时,必须检查 isWord 而非直接返回 true
四、学习心得
Trie 数据结构的优势
- 前缀共享:多个字符串共享公共前缀节点,节省存储空间
- 快速查找:查找时间与字符串长度相关,与集合大小无关
- 前缀操作:天然支持前缀查找,适合自动补全、拼写检查等场景
- 动态扩展:可以随时插入新字符串,无需重建整个数据结构
解题思维模式
- 从简单到高效:先用数组等简单结构理解问题,再优化为 Trie 结构
- 对象嵌套思维:将树形结构转化为对象的嵌套,每个对象是一个节点
- 标志位设计:用特殊属性(isEnd/isWord)标记节点状态,区分路径和完整单词
- 递归与迭代:基本操作用迭代,复杂搜索(如通配符)用递归 DFS
- 短路优化:DFS 中一旦找到匹配立即返回,避免不必要的搜索
五、使用场景总结
字典树的使用场景包括:字符串集合的高效存储与检索、前缀匹配与自动补全、拼写检查与单词验证、带通配符的字符串搜索、IP 路由表的最长前缀匹配、关键词过滤与敏感词检测。