class Trie {
private:
bool isEnd;
Trie* next[26];
public:
Trie() {
isEnd = false;
memset(next, 0, sizeof(next));
}
void insert(string word) {
Trie* node = this;
for (char c : word) {
if (node->next[c-'a'] == NULL) {
node->next[c-'a'] = new Trie();
}
node = node->next[c-'a'];
}
node->isEnd = true;
}
bool search(string word) {
Trie* node = this;
for (char c : word) {
node = node->next[c - 'a'];
if (node == NULL) {
return false;
}
}
return node->isEnd;
}
bool startsWith(string prefix) {
Trie* node = this;
for (char c : prefix) {
node = node->next[c-'a'];
if (node == NULL) {
return false;
}
}
return true;
}
};
这是一个 Trie (前缀树) 的实现,常用于高效的单词查找、前缀匹配等场景。下面是每一行的注释和详细思路。
作者链接:208. 实现 Trie (前缀树) - 力扣(LeetCode)
1. 类定义
class Trie {
private:
bool isEnd; // 标记当前节点是否为一个单词的结尾
Trie* next[26]; // 存储子节点,最多26个字母
• isEnd 用于标记当前节点是否是一个有效单词的结尾。
• next[26] 是一个数组,用来存储当前节点的所有子节点。数组的大小为 26,代表英文字母 ‘a’ 到 ‘z’。每个子节点指向一个 Trie 对象。
2. 构造函数
public:
Trie() {
isEnd = false; // 初始化时,当前节点不是单词的结尾
memset(next, 0, sizeof(next)); // 初始化所有子节点指针为 NULL
}
• 构造函数将 isEnd 设置为 false(表示当前节点不是单词的结尾)。
• memset(next, 0, sizeof(next)) 将 next 数组的每个元素(即每个子节点指针)初始化为 NULL。
3. 插入单词
void insert(string word) {
Trie* node = this; // 从当前根节点开始插入
for (char c : word) { // 遍历单词中的每个字符
if (node->next[c-'a'] == NULL) { // 如果该字母的子节点为空
node->next[c-'a'] = new Trie(); // 创建新的 Trie 节点
}
node = node->next[c-'a']; // 向下移动到该字符对应的子节点
}
node->isEnd = true; // 最后一个字符的节点标记为单词的结尾
}
• insert 方法用来插入一个单词。
• node = this; 从根节点开始。
• 对于单词中的每个字符:
• 计算该字符对应的数组索引(c - 'a'),检查 next 数组中是否存在该字符对应的子节点。
• 如果子节点不存在,则创建一个新的 Trie 节点。
• 移动 node 指针到该子节点。
• 最后,将 node->isEnd = true; 标记当前节点为单词的结束位置。
4. 查找单词
bool search(string word) {
Trie* node = this; // 从根节点开始查找
for (char c : word) { // 遍历单词中的每个字符
node = node->next[c - 'a']; // 移动到该字符对应的子节点
if (node == NULL) { // 如果某个字符对应的子节点不存在
return false; // 返回 false,说明单词不存在
}
}
return node->isEnd; // 最后检查当前节点是否为一个单词的结尾
}
• search 方法用来查找是否存在某个单词。
• 从根节点开始,遍历单词中的每个字符。
• 对每个字符,计算索引 c - 'a',并检查是否有对应的子节点。
• 如果没有对应的子节点,返回 false,表示单词不存在。
• 最后,检查当前节点是否是一个单词的结尾。如果是,返回 true,否则返回 false。
5. 查找前缀
bool startsWith(string prefix) {
Trie* node = this; // 从根节点开始查找
for (char c : prefix) { // 遍历前缀中的每个字符
node = node->next[c - 'a']; // 移动到该字符对应的子节点
if (node == NULL) { // 如果某个字符对应的子节点不存在
return false; // 返回 false,表示没有该前缀
}
}
return true; // 如果所有字符都有对应的子节点,则说明有这个前缀
}
};
• startsWith 方法用来判断是否存在某个前缀。
• 从根节点开始,遍历前缀中的每个字符。
• 对每个字符,计算索引 c - 'a',并检查是否有对应的子节点。
• 如果没有对应的子节点,返回 false,表示没有这个前缀。
• 如果所有字符都有对应的子节点,返回 true,表示该前缀存在。
好的,让我们通过一个详细的示例来演示这个 Trie 树的操作。假设我们有以下操作:
1. 插入单词 “apple”
2. 插入单词 “app”
3. 查找单词 “apple”
4. 查找单词 “app”
5. 查找前缀 “app”
假设 Trie 树初始化为空
1. 插入单词 “apple”
步骤:
• 我们从根节点开始 (node = this)。
• 对于 “apple” 中的每个字符,逐个插入:
插入 “a”:
• 计算索引 c - 'a' = 'a' - 'a' = 0。
• 检查 next[0],发现为空 (NULL),创建一个新的 Trie 节点。
• 将 node 移动到新创建的节点。
插入 “p”:
• 计算索引 c - 'a' = 'p' - 'a' = 15。
• 检查 next[15],为空,创建一个新的 Trie 节点。
• 将 node 移动到新创建的节点。
插入 “p”:
• 计算索引 c - 'a' = 'p' - 'a' = 15。
• 检查 next[15],发现已经存在对应的节点,直接移动到该节点。
插入 “l”:
• 计算索引 c - 'a' = 'l' - 'a' = 11。
• 检查 next[11],为空,创建一个新的 Trie 节点。
• 将 node 移动到新创建的节点。
插入 “e”:
• 计算索引 c - 'a' = 'e' - 'a' = 4。
• 检查 next[4],为空,创建一个新的 Trie 节点。
• 将 node 移动到新创建的节点。
• 到这里,“apple” 的所有字符都插入完毕,我们将最后一个节点的 isEnd 标记为 true,表示这是一个完整的单词。
2. 插入单词 “app”
步骤:
• 我们从根节点开始 (node = this)。
• 对于 “app” 中的每个字符,逐个插入:
插入 “a”:
• 计算索引 c - 'a' = 'a' - 'a' = 0。
• 检查 next[0],已经存在对应的节点,直接移动到该节点。
插入 “p”:
• 计算索引 c - 'a' = 'p' - 'a' = 15。
• 检查 next[15],已经存在对应的节点,直接移动到该节点。
插入 “p”:
• 计算索引 c - 'a' = 'p' - 'a' = 15。
• 检查 next[15],已经存在对应的节点,直接移动到该节点。
• 到这里,“app” 的所有字符都已经插入了。我们将最后一个节点的 isEnd 标记为 true,表示 “app” 也是一个完整的单词。
3. 查找单词 “apple”
步骤:
• 我们从根节点开始 (node = this)。
• 对于 “apple” 中的每个字符,逐个查找:
查找 “a”:
• 计算索引 c - 'a' = 'a' - 'a' = 0。
• 检查 next[0],存在节点,继续查找。
查找 “p”:
• 计算索引 c - 'a' = 'p' - 'a' = 15。
• 检查 next[15],存在节点,继续查找。
查找 “p”:
• 计算索引 c - 'a' = 'p' - 'a' = 15。
• 检查 next[15],存在节点,继续查找。
查找 “l”:
• 计算索引 c - 'a' = 'l' - 'a' = 11。
• 检查 next[11],存在节点,继续查找。
查找 “e”:
• 计算索引 c - 'a' = 'e' - 'a' = 4。
• 检查 next[4],存在节点,继续查找。
• 最后,我们到达单词 “apple” 的结尾节点,并且 isEnd = true,说明单词 “apple” 存在,返回 true。
4. 查找单词 “app”
步骤:
• 我们从根节点开始 (node = this)。
• 对于 “app” 中的每个字符,逐个查找:
查找 “a”:
• 计算索引 c - 'a' = 'a' - 'a' = 0。
• 检查 next[0],存在节点,继续查找。
查找 “p”:
• 计算索引 c - 'a' = 'p' - 'a' = 15。
• 检查 next[15],存在节点,继续查找。
查找 “p”:
• 计算索引 c - 'a' = 'p' - 'a' = 15。
• 检查 next[15],存在节点,继续查找。
• 最后,我们到达 “app” 的结尾节点,并且 isEnd = true,说明单词 “app” 存在,返回 true。
5. 查找前缀 “app”
步骤:
• 我们从根节点开始 (node = this)。
• 对于前缀 “app” 中的每个字符,逐个查找:
查找 “a”:
• 计算索引 c - 'a' = 'a' - 'a' = 0。
• 检查 next[0],存在节点,继续查找。
查找 “p”:
• 计算索引 c - 'a' = 'p' - 'a' = 15。
• 检查 next[15],存在节点,继续查找。
查找 “p”:
• 计算索引 c - 'a' = 'p' - 'a' = 15。
• 检查 next[15],存在节点,继续查找。
• 最后,所有字符都有对应的节点,说明有前缀 “app”,返回 true。
总结:
• 插入:每次插入一个单词时,我们会遍历该单词的每个字符,并在 Trie 树中逐步创建新的节点或沿用已有的节点。
• 查找单词:通过遍历单词的每个字符,检查是否有对应的节点,最终判断该单词是否结束。
• 查找前缀:与查找单词类似,但我们不关心节点是否是单词的结尾,只关心是否存在这个前缀。
每个操作的时间复杂度是 O(m),其中 m 是单词或前缀的长度。