对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎
LeetCode 208. 实现 Trie (前缀树) 详解
1. 题目描述
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。
请你实现 Trie 类:
Trie()初始化前缀树对象。void insert(String word)向前缀树中插入字符串word。boolean search(String word)如果字符串word在前缀树中,返回true(即,在检索之前已经插入);否则,返回false。boolean startsWith(String prefix)如果之前已经插入的字符串word的前缀之一为prefix,返回true;否则,返回false。
示例:
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 True
trie.search("app"); // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app"); // 返回 True
提示:
1 <= word.length, prefix.length <= 2000word和prefix仅由小写英文字母组成insert、search和startsWith调用次数 总计 不超过3 * 10^4次
2. 问题分析
Trie 是一种多叉树结构,每个节点包含以下部分:
- 指向子节点的指针数组,通常长度为26(对应26个小写英文字母)。
- 一个布尔值,表示该节点是否为某个单词的结束。
Trie 树的根节点不包含字符,每个节点(除根节点)都只包含一个字符。从根节点到某一节点的路径上的字符连接起来,就是该节点对应的字符串。
对于前端开发者来说,理解 Trie 树的结构类似于嵌套对象,每个节点可以看作一个对象,其属性对应子节点,同时有一个属性标记是否为单词结尾。这种数据结构在前端中常用于搜索建议、路由匹配等场景。
3. 解题思路
Trie 树的实现主要涉及三个操作:插入、查找和前缀匹配。每个操作都从根节点开始,沿着字符串的每个字符对应的子节点向下移动。
- 插入:遍历字符串的每个字符,如果当前节点不存在对应字符的子节点,则创建一个新的子节点。继续向下,直到处理完所有字符,然后将最后一个节点标记为单词结束。
- 查找:遍历字符串的每个字符,如果当前节点不存在对应字符的子节点,则返回 false。如果遍历完所有字符,并且最后一个节点被标记为单词结束,则返回 true,否则返回 false。
- 前缀匹配:与查找类似,但不需要检查最后一个节点是否被标记为单词结束,只要所有字符都能在树中找到对应的节点即可。
复杂度分析(n为字符串长度):
- 插入:时间复杂度 O(n),空间复杂度 O(n)(最坏情况下需要创建新节点)。
- 查找和前缀匹配:时间复杂度 O(n),空间复杂度 O(1)。
最优解:上述思路即为最优解,因为每个操作都需要遍历整个字符串,无法在时间复杂度上进一步优化。
4. 代码实现(JavaScript)
4.1 Trie 节点定义
首先,我们定义一个 Trie 节点类。每个节点包含一个 children 对象(或数组)和一个 isEnd 布尔标记。
4.2 完整代码实现
javascript
// 定义 Trie 节点类
class TrieNode {
constructor() {
// 使用数组存储子节点,索引0-25对应a-z
this.children = new Array(26).fill(null);
// 标记当前节点是否为单词结尾
this.isEnd = false;
}
}
// 定义 Trie 类
class Trie {
constructor() {
// 根节点不存储字符
this.root = new TrieNode();
}
// 向 Trie 中插入单词
insert(word) {
let node = this.root;
for (let i = 0; i < word.length; i++) {
const char = word[i];
const index = char.charCodeAt(0) - 'a'.charCodeAt(0);
// 如果当前字符对应的子节点不存在,则创建新节点
if (!node.children[index]) {
node.children[index] = new TrieNode();
}
// 移动到子节点
node = node.children[index];
}
// 标记单词结束
node.isEnd = true;
}
// 搜索单词是否在 Trie 中
search(word) {
let node = this.root;
for (let i = 0; i < word.length; i++) {
const char = word[i];
const index = char.charCodeAt(0) - 'a'.charCodeAt(0);
// 如果当前字符对应的子节点不存在,则返回 false
if (!node.children[index]) {
return false;
}
node = node.children[index];
}
// 检查最后一个节点是否被标记为单词结束
return node.isEnd;
}
// 检查 Trie 中是否有以 prefix 为前缀的单词
startsWith(prefix) {
let node = this.root;
for (let i = 0; i < prefix.length; i++) {
const char = prefix[i];
const index = char.charCodeAt(0) - 'a'.charCodeAt(0);
if (!node.children[index]) {
return false;
}
node = node.children[index];
}
// 只要 prefix 的每个字符都能在树中找到,则返回 true
return true;
}
}
// 测试代码
const trie = new Trie();
trie.insert("apple");
console.log(trie.search("apple")); // true
console.log(trie.search("app")); // false
console.log(trie.startsWith("app")); // true
trie.insert("app");
console.log(trie.search("app")); // true
4.3 步骤分解说明
- 初始化:创建根节点,根节点不存储字符,但包含26个子节点(初始为null)和一个 isEnd 标志。
- 插入操作 :
- 从根节点开始,对于要插入的字符串中的每个字符,计算其索引(0-25)。
- 检查当前节点是否具有该索引的子节点,如果没有则创建一个新的 TrieNode。
- 移动到该子节点,继续处理下一个字符。
- 在字符串的所有字符处理完后,将当前节点的 isEnd 标记为 true。
- 查找操作 :
- 从根节点开始,对于要查找的字符串中的每个字符,计算其索引。
- 如果当前节点没有对应索引的子节点,则返回 false。
- 如果有,则移动到该子节点,继续处理下一个字符。
- 当所有字符处理完后,检查当前节点的 isEnd 是否为 true,是则返回 true,否则返回 false。
- 前缀匹配 :
- 与查找操作类似,但在所有字符处理完后,不需要检查 isEnd,直接返回 true。
5. 复杂度、优缺点对比表格
| 操作 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 插入 | O(n) | O(n)(最坏情况) | 插入速度快,与已存单词数量无关 | 每个节点需要固定大小的数组,可能浪费空间 |
| 查找 | O(n) | O(1) | 查找速度快,与已存单词数量无关 | 无 |
| 前缀匹配 | O(n) | O(1) | 匹配速度快,与已存单词数量无关 | 无 |
说明:
- n 为字符串的长度。
- 空间复杂度:插入操作最坏情况下需要为每个字符创建新节点,因此空间复杂度为 O(n)。而查找和前缀匹配只需要常数空间。
- 使用数组存储子节点,访问速度快,但可能浪费空间(因为每个节点都有26个元素的数组,即使很多是空)。也可以使用对象(Map)来动态存储子节点,以空间换时间。
6. 总结
6.1 通用解题模板
Trie 树的实现有固定的模式:
- 定义节点类,包含子节点集合和结束标志。
- 初始化根节点。
- 插入、查找、前缀匹配操作都是遍历字符串,沿着树向下移动。
对于前端开发,我们可以将 Trie 树应用于:
- 搜索框的自动补全。
- 路由匹配(例如,Vue Router 的路由表匹配)。
- 单词拼写检查。
6.2 类似题目
- LeetCode 211. 添加与搜索单词 - 数据结构设计:在 Trie 的基础上,搜索时支持 '.' 通配符。
- LeetCode 212. 单词搜索 II:结合 Trie 和回溯,在二维网格中查找单词。
- LeetCode 720. 词典中最长的单词:使用 Trie 存储单词,然后查找最长单词。
- LeetCode 676. 实现一个魔法字典:在 Trie 上实现模糊搜索(允许一个字符不同)。