📌 题目链接:208. 实现 Trie (前缀树) - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:设计、字典树(Trie) 、字符串
⏱️ 目标时间复杂度:插入/搜索/前缀查询均为 O(L) ,其中 L 为字符串长度
💾 空间复杂度:O(N × L × Σ) ,N 为插入单词总数,L 为平均长度,Σ = 26(小写字母)
🌳在海量字符串处理场景中,如搜索引擎的自动补全、拼写检查、IP 路由表查找等,Trie(前缀树 / 字典树) 是一种极其高效的数据结构。它通过共享公共前缀的方式,显著减少存储开销并加速查询。本题要求我们从零实现一个支持插入、精确搜索和前缀匹配的 Trie,是理解高级字符串结构的基石。
💡 面试高频考点:Trie 不仅常出现在系统设计题(如“设计一个单词补全系统”),也是考察面向对象设计能力和指针/引用管理的经典题目。务必掌握其结构、操作逻辑及内存优化思路。
🔧 题目分析
我们需要实现一个 Trie 类,支持以下三个核心操作:
insert(word):将一个单词插入到 Trie 中。search(word):判断该单词是否完整存在于 Trie 中(即曾被插入过)。startsWith(prefix):判断是否存在某个已插入单词以prefix为前缀(不要求完整匹配)。
关键区别:
search要求路径终点节点的isEnd == truestartsWith只需路径存在即可,不要求isEnd
✅ 示例回顾:
insert("apple") → search("apple") = true search("app") = false(因为 "app" 未被完整插入) startsWith("app") = true(因为 "apple" 以 "app" 开头) insert("app") → search("app") = true
🧠 核心算法及代码讲解:字典树(Trie)
📚 什么是 Trie?
Trie 是一棵多叉树,每个节点代表一个字符。从根到某节点的路径构成一个字符串前缀。典型结构包含:
children[26]:指向 26 个小写字母子节点的指针数组(可用vector<Trie*>或固定数组实现)。isEnd:布尔标志,标记当前节点是否为某个单词的结尾。
🎯 为什么用 Trie?
- 普通哈希表(如
unordered_set)虽然search快,但无法高效支持前缀查询。- Trie 的前缀共享特性使得空间更紧凑(如 "apple" 和 "app" 共享前三个节点)。
- 所有操作时间复杂度仅与字符串长度相关,与总词数无关!
🔍 辅助函数:searchPrefix
为避免代码重复,我们封装一个私有函数 searchPrefix,用于遍历前缀路径并返回终点节点(若存在):
Trie* searchPrefix(string prefix) {
Trie* node = this; // 从当前对象(根)开始
for (char ch : prefix) {
ch -= 'a'; // 转换为 0~25 的索引
if (node->children[ch] == nullptr) {
return nullptr; // 路径中断,前缀不存在
}
node = node->children[ch]; // 移动到子节点
}
return node; // 返回前缀终点节点
}
⚠️ 注意:此函数不检查
isEnd,仅验证路径是否存在。
➕ 插入操作 insert
逐字符遍历 word,若子节点不存在则新建,最后标记 isEnd = true:
void insert(string word) {
Trie* node = this;
for (char ch : word) {
ch -= 'a';
if (node->children[ch] == nullptr) {
node->children[ch] = new Trie(); // 动态创建新节点
}
node = node->children[ch];
}
node->isEnd = true; // 标记单词结束
}
💡 内存管理提示:实际工程中应考虑析构函数释放内存(本题 LeetCode 不要求,但面试可提及)。
🔎 精确搜索 search
调用 searchPrefix 获取终点节点,再检查 isEnd:
bool search(string word) {
Trie* node = this->searchPrefix(word);
return node != nullptr && node->isEnd; // 路径存在 + 是单词结尾
}
🔤 前缀匹配 startsWith
只需路径存在即可:
bool startsWith(string prefix) {
return this->searchPrefix(prefix) != nullptr;
}
🧩 解题思路(分步拆解)
-
定义 Trie 节点结构
- 成员变量:
vector<Trie*> children(26, nullptr)+bool isEnd = false - 构造函数初始化
children和isEnd
- 成员变量:
-
实现通用前缀查找器
searchPrefix- 遍历字符串每个字符
- 若对应子节点为空,返回
nullptr - 否则沿指针向下移动
- 返回最终节点指针
-
实现
insert- 复用
searchPrefix的遍历逻辑 - 遇空则新建节点
- 最后设置
isEnd = true
- 复用
-
实现
search与startsWithsearch:调用searchPrefix+ 检查isEndstartsWith:仅调用searchPrefix并判非空
✅ 关键思想:复用逻辑!通过
searchPrefix避免三处重复遍历代码。
📊 算法分析
| 操作 | 时间复杂度 | 空间复杂度(单次) |
|---|---|---|
| 初始化 | O(1) | O(26) ≈ O(1) |
insert | O(L) | O(L × 26)(最坏) |
search | O(L) | O(1) |
startsWith | O(L) | O(1) |
- L:输入字符串长度
- 总空间复杂度:O(T × Σ),T 为所有插入字符串的总字符数,Σ = 26
🌟 优势:相比哈希表 O(1) 平均查找,Trie 在前缀操作上具有不可替代性,且无哈希冲突问题。
💻 代码实现
C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Trie {
private:
vector<Trie*> children;
bool isEnd;
// 辅助函数:查找 prefix 对应的终点节点,若不存在则返回 nullptr
Trie* searchPrefix(string prefix) {
Trie* node = this; // 从当前节点(根)开始
for (char ch : prefix) {
ch -= 'a'; // 将字符映射到 0~25
if (node->children[ch] == nullptr) {
return nullptr; // 路径中断
}
node = node->children[ch]; // 移动到子节点
}
return node; // 返回前缀终点
}
public:
// 构造函数:初始化 26 个子节点为空,isEnd 为 false
Trie() : children(26), isEnd(false) {}
// 插入单词:逐字符创建节点,最后标记 isEnd
void insert(string word) {
Trie* node = this;
for (char ch : word) {
ch -= 'a';
if (node->children[ch] == nullptr) {
node->children[ch] = new Trie(); // 新建节点
}
node = node->children[ch];
}
node->isEnd = true; // 标记单词结束
}
// 精确搜索:路径存在 且 是单词结尾
bool search(string word) {
Trie* node = this->searchPrefix(word);
return node != nullptr && node->isEnd;
}
// 前缀匹配:只需路径存在
bool startsWith(string prefix) {
return this->searchPrefix(prefix) != nullptr;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 示例测试用例
Trie trie;
trie.insert("apple");
cout << (trie.search("apple") ? "true" : "false") << "\n"; // true
cout << (trie.search("app") ? "true" : "false") << "\n"; // false
cout << (trie.startsWith("app") ? "true" : "false") << "\n"; // true
trie.insert("app");
cout << (trie.search("app") ? "true" : "false") << "\n"; // true
return 0;
}
JavaScript
var Trie = function() {
this.children = new Array(26).fill(null);
this.isEnd = false;
};
/**
* @param {string} word
* @return {void}
*/
Trie.prototype.insert = function(word) {
let node = this;
for (let ch of word) {
const idx = ch.charCodeAt(0) - 'a'.charCodeAt(0);
if (node.children[idx] === null) {
node.children[idx] = new Trie();
}
node = node.children[idx];
}
node.isEnd = true;
};
/**
* @param {string} word
* @return {boolean}
*/
Trie.prototype.search = function(word) {
const node = this.searchPrefix(word);
return node !== null && node.isEnd;
};
/**
* @param {string} prefix
* @return {boolean}
*/
Trie.prototype.startsWith = function(prefix) {
return this.searchPrefix(prefix) !== null;
};
// 辅助方法:查找前缀路径
Trie.prototype.searchPrefix = function(prefix) {
let node = this;
for (let ch of prefix) {
const idx = ch.charCodeAt(0) - 'a'.charCodeAt(0);
if (node.children[idx] === null) {
return null;
}
node = node.children[idx];
}
return node;
};
// 测试(可在浏览器或 Node.js 中运行)
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
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!