LeetCode LCR 062. 实现 Trie (前缀树)
题目描述


题意分析
实现一个保存单词的前缀树,支持插入、查询完整单词、查询前缀三种操作。完整单词查询要求这个字符串曾作为一个整体插入过;前缀查询只要求某个已插入单词以它开头。
输入只有小写英文字母,因此每个节点最多有 $26$ 种向下的字符转移。重复插入同一个单词不应改变查询结果,查询串也可能在中途就没有对应路径。
解法:前缀树与单词终止标记
核心思路
[!blue]
将一个单词的字符依次看成从根出发的边,根表示空前缀,走过前
k个字符后所在的节点就表示这个长度为k的前缀。具有公共开头的单词共享同一段路径,因此查询前缀时可以直接沿字符走,不必逐个检查所有单词。每个节点保存
children[26]和isEnd:children[c - 'a']指向追加字符c后的前缀节点;isEnd表示当前路径是否曾作为完整单词插入。结束节点不一定是叶子,因为一个完整单词也可能是另一个更长单词的前缀。插入时从根开始,遇到不存在的子节点才创建,然后沿该节点继续。走完整个单词后,只把最后一个节点的
isEnd置为真。已有路径会被复用,原有分支和结束标记不受影响,所以更长、较短以及重复单词都能正确插入。两种查询先执行相同的寻路过程:处理完前
k个字符时,当前节点恰好对应这k个字符;若下一条边不存在,就说明没有已插入单词经过这个前缀,可以立即失败。全部字符走完后,searchPrefix返回终点节点。
startsWith只检查终点是否存在;search还检查终点的isEnd。路径存在保证某个已插入单词拥有这个前缀,结束标记则进一步保证查询串本身被完整插入过。只有一个空根时,任何题目允许的非空查询都会因缺少路径而失败。
解题步骤
- 构造根节点,初始化 $26$ 个空子节点引用,结束标记为假。
- 插入单词时逐字符定位子节点,缺失则创建,最后只标记完整单词的终点。
- 公共寻路函数沿查询串前进,遇到缺失边返回空,走完则返回终点。
- 前缀查询判断终点非空;完整单词查询判断终点非空且
isEnd为真。
代码实现
class Trie {
private Trie[] children;
private boolean isEnd;
public Trie() {
children = new Trie[26];
}
public void insert(String word) {
Trie node = this;
for (char c : word.toCharArray()) {
int idx = c - 'a';
if (node.children[idx] == null) {
node.children[idx] = new Trie();
}
node = node.children[idx];
}
node.isEnd = true;
}
public boolean search(String word) {
Trie node = searchPrefix(word);
return node != null && node.isEnd;
}
public boolean startsWith(String prefix) {
Trie node = searchPrefix(prefix);
return node != null;
}
private Trie searchPrefix(String s) {
Trie node = this;
for (char c : s.toCharArray()) {
int idx = c - 'a';
if (node.children[idx] == null) {
return null;
}
node = node.children[idx];
}
return node;
}
}
type Trie struct {
children [26]*Trie
isEnd bool
}
func Constructor() Trie {
return Trie{}
}
func (this *Trie) Insert(word string) {
node := this
for _, c := range word {
idx := c - 'a'
if node.children[idx] == nil {
node.children[idx] = &Trie{}
}
node = node.children[idx]
}
node.isEnd = true
}
func (this *Trie) Search(word string) bool {
node := this.SearchPrefix(word)
return node != nil && node.isEnd
}
func (this *Trie) StartsWith(prefix string) bool {
node := this.SearchPrefix(prefix)
return node != nil
}
func (this *Trie) SearchPrefix(s string) *Trie {
node := this
for _, c := range s {
idx := c - 'a'
if node.children[idx] == nil {
return nil
}
node = node.children[idx]
}
return node
}
复杂度分析
- 时间复杂度:每次插入或查询为 $O(L)$,
L为本次字符串长度;每个字符只进行固定大小数组中的一次寻址。- 空间复杂度:持久空间为 $O(26C)$,
C为包含根在内的节点数,至多为插入字符总数加一。公共前缀共用节点;Java 的toCharArray()还会在单次操作中产生 $O(L)$ 临时数组,Go 的字符遍历只需常数额外空间。
关键点总结
[!green]
- 路径回答“前缀是否存在”,
isEnd回答“该前缀是否也是完整单词”。- 结束节点可以继续拥有子节点,不能用是否为叶子代替单词结束标记。
- 插入复用已有节点,只补缺失分支;重复插入只会再次将相同终点标为真。
易错点总结
[!yellow]
search只检查路径而不检查isEnd,会把从未单独插入的前缀误当成完整单词。- 插入时把沿途所有节点都标成结束,会错误增加一批短单词。
- 覆盖已存在的孩子节点,会丢失其他单词共享的后续分支。
- 前缀查询不要求
isEnd,否则会漏掉仅作为更长单词开头的前缀。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 211. 添加与搜索单词 - 数据结构设计 | 中等 | 在Trie基础上增加通配符搜索,遇到点号需要探索多个孩子。 |
| 677. 键值映射 | 中等 | 在Trie的前缀路径上增加数值聚合,终点不再只表示单词存在。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!