题面
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 104次
分析
-
节点结构:
next[ALPHABET]:存储子节点的索引,-1表示无该子节点,避免使用指针,效率更高;pass:统计 “经过该节点的单词数”,是实现countPrefix的核心;end:统计 “以该节点为结尾的单词数”,是实现search的核心
-
插入逻辑:
- 从根节点(索引 0)开始,逐个字符遍历单词;
- 若字符对应的子节点不存在,创建新节点并记录索引;
- 每经过一个节点,
pass计数 + 1; - 单词遍历结束后,结尾节点的
end计数 + 1
-
查找逻辑:
- 逐个字符遍历单词,若中途子节点不存在则返回
false; - 遍历完成后,判断结尾节点的
end是否大于 0(是否是真实单词的结尾)
- 逐个字符遍历单词,若中途子节点不存在则返回
-
前缀统计逻辑:
- 遍历前缀到最后一个字符,返回该节点的
pass值(所有经过该节点的单词,即以前缀开头的单词数)
- 遍历前缀到最后一个字符,返回该节点的
-
代码实现
// 用class封装Trie,私有成员隐藏实现细节,公共接口提供功能 class Trie { private: // 字母表大小(仅处理小写字母),私有常量 static constexpr int ALPHABET = 26; // 前缀树节点结构(私有,外部不可访问) struct Node { int next[ALPHABET]; // 子节点索引(-1表示无该子节点) int pass; // 经过该节点的单词数量 int end; // 以该节点为结尾的单词数量 // 节点构造函数:初始化所有字段 Node() { memset(next, -1, sizeof(next)); // 批量初始化next数组为-1 pass = 0; end = 0; } }; vector<Node> tree; // 存储所有节点,root固定为索引0(私有) // 私有辅助函数:将字符转换为0-25的索引(外部无需调用) inline int charToId(char c) const { return c - 'a'; // 仅支持小写字母,若需支持大写可额外处理 } public: // 构造函数:初始化根节点 Trie() { tree.emplace_back(); // root节点(索引0) } // 公共接口1:插入单词 void insert(const string& s) { int current = 0; // 从根节点开始 tree[current].pass++; // 根节点的pass计数+1 for (char c : s) { int id = charToId(c); // 若当前字符对应的子节点不存在,创建新节点 if (tree[current].next[id] == -1) { tree[current].next[id] = tree.size(); // 新节点的索引为当前vector大小 tree.emplace_back(); // 构造新节点 } // 移动到子节点 current = tree[current].next[id]; tree[current].pass++; // 该节点的pass计数+1 } tree[current].end++; // 单词结尾节点的end计数+1 } // 公共接口2:查找单词是否存在(完全匹配) bool search(const string& s) const { int current = 0; for (char c : s) { int id = charToId(c); // 字符对应的子节点不存在,直接返回false if (tree[current].next[id] == -1) { return false; } current = tree[current].next[id]; } // 遍历完单词后,判断是否是某个单词的结尾(end>0) return tree[current].end > 0; } // 公共接口3:判断是否存在以指定前缀开头的单词 bool startsWith(const string& prefix) const { int current = 0; for (char c : prefix) { int id = charToId(c); if (tree[current].next[id] == -1) { return false; } current = tree[current].next[id]; } // 遍历完前缀即说明存在该前缀 return true; } // 公共接口4:统计以指定前缀开头的单词数量 int countPrefix(const string& prefix) const { int current = 0; for (char c : prefix) { int id = charToId(c); if (tree[current].next[id] == -1) { return 0; // 前缀不存在,数量为0 } current = tree[current].next[id]; } // 返回前缀最后一个节点的pass值(即经过该节点的单词数) return tree[current].pass; } // 可选扩展接口:删除单词(补充常用功能) bool remove(const string& s) { // 先检查单词是否存在 if (!search(s)) { return false; } int current = 0; tree[current].pass--; for (char c : s) { int id = charToId(c); int nextNode = tree[current].next[id]; // 若子节点的pass减1后为0,可删除该子节点(优化内存) if (--tree[nextNode].pass == 0) { tree[current].next[id] = -1; // 注:vector无法高效删除中间节点,此处仅标记为-1,若需严格释放内存可改用其他结构 } current = nextNode; } tree[current].end--; return true; } };
这篇文档有帮助吗?