【树】力扣208:实现 Trie (前缀树、字典树)
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
基础知识
常见的二叉树结构:
class TreeNode {
int val;
TreeNode* left;
TreeNode* right;
}
每个结点包含了三个元素:该结点本身的值,左子树的指针,右子树的指针。也就是说二叉树的每个结点只有两个孩子 left 和 right。
如果每个结点有多个孩子呢,就形成了多叉树。多叉树的子结点数目一般不是固定的,所以会用变长数组来保存所有的子结点的指针。
多叉树结构:
class TreeNode {
int val;
vector<TreeNode*> children;
}
对于普通的多叉树,每个结点的所有子结点可能是没有任何规律的。
而本题讨论的「前缀树」Trie 是每个结点的 children 有规律的多叉树。Trie 中一般都含有大量的空链接,因此在绘制一棵单词查找树时一般会忽略空链接。
Trie
(只保存小写字符的)「前缀树」是一种特殊的多叉树,它的 TrieNode 中 chidren 是一个大小为 26 的一维数组,分别对应了26个英文字符 'a' ~ 'z',也就是说形成了一棵 26 叉树。
前缀树的结构存储两个信息:
-
isVal 表示从根结点到当前结点为止,该路径是否形成了一个有效的字符串
-
children 是该结点的所有子结点
字典树的应用:
生活中就有很多应用。
比如常见的手机拨号键盘,当输入一些数字的时候,后面会自动提示以输入数字为开头的所有号码
比如输入法,当输入半个词汇的时候,输入法上面会自动联想和补全后面可能的词汇。
1. 前缀树(字典树)的初始化
-
最初,只有一个根结点 root,子树都都还没有初始化。且 根结点不保存任何信息
-
关键词放到「前缀树」时,需要把它拆成各个字符,每个字符按照其在 'a' ~ 'z' 的序号,放在对应的 children 里面。下一个字符是当前字符的子结点
-
一个输入字符串构建「前缀树」结束的时候,需要把该结点的 isVal 标记为 true,说明从根结点到当前结点的路径可以构成一个关键词。因此 isVal 应初始化为 false
这棵「前缀树」保存了 {"am", "an", "as", "b", "c", "cv"} 这些关键词。图中红色表示 isValr 为 true。注意:
-
所有以相同字符开头的字符串,会聚合到同一个子树上。比如
-
并不一定是到达叶子结点才形成了一个关键词,只要 isVal 为 true,那么从根结点到当前结点的路径就是关键词。比如
有些题解把字符画在了结点中,我认为是不准确的。因为前缀树是根据 字符在 children 中的位置确定子树,而不真正在树中存储了 'a' ~ 'z' 这些字符。树中每个结点存储的 isVal 表示的是从根结点到当前结点的路径是否构成了一个关键词。
2. 插入字符串
从字典树的根开始插入字符串。对于当前字符对应的子结点,有两种情况:
-
子结点存在:沿着指针移动到子结点,继续处理下一个字符
-
子结点不存在:创建一个新的子结点,记录在 children 数组的对应位置上,然后沿着指针移动到子结点,继续搜索下一个字符
重复以上步骤,直到处理字符串的最后一个字符,然后将当前结点标记为字符串的结尾。
3. 查询
从字典树的根开始查找。对于当前字符对应的子结点,有两种情况:
-
子结点存在:沿着指针移动到子结点,继续搜索下一个字符
-
子结点不存在:查找路径中断,说明字典树中不包含该前缀,返回空指针。比如在上面的前缀树图中寻找 "d" 或者 "ar" 或者 "any" ,由于树中没有构建对应的结点,那就查找不到这些关键词
重复以上步骤,直到 返回空指针 或 搜索完字符串的最后一个字符
-
若搜索到了字符串的末尾,首先说明字典树中可以有该字符串
-
若前缀末尾对应结点的 isVal 为 true,则说明字典树中存在该字符串。比如上面前缀树图中的 "am" , "cv" 等
-
若前缀末尾对应结点的 isVal 为 false,则说明字典树中不存在该字符串。比如在上面的前缀树图中寻找 "a"
-
新建一个类的方法解决问题
# 定义新类
class Node(object):
def __init__(self):
self.children = collections.defaultdict(Node) # 使用 字典 保存 children,保存的结构是 {字符:Node} ,所以可以直接通过 children['a'] 来获取当前结点的 'a' 子树
self.isVal = False
class Trie(object):
def __init__(self):
self.root = Node()
def insert(self, word: str) -> None:
curr = self.root
for char in word:
curr = curr.children[char]
curr.isVal = True
def search(self, word: str) -> bool:
curr = self.root
for char in word:
curr = curr.children.get(char)
if curr == None:
return False
return curr.isVal
def startsWith(self, prefix: str) -> bool:
curr = self.root
for char in prefix:
curr = curr.children.get(char)
if curr == None:
return False
return True
直接解决问题
class Trie:
def __init__(self):
self.root = {} # 初始化一个字典作为结点,相当于根结点
def insert(self, word: str) -> None:
cur = self.root # 指向当前结点的指针
'''
从键与值的关系具体理解
for char in word:
node = cur.get(char) # 键为 char ,值为 node
if node is None:
node = {}
cur[char] = node
cur = node
'''
for char in word:
if char not in cur:
cur[char] = {} # 初始化子结点就是新建一个键为 char 的字典,即 cur[char] = dict()
cur = cur[char] # 把指针移到下一个字符所在的结点
cur["#"] = "#" # 加上单词结束标志,给结尾做个标记,代表这是一个完整的字符串,也可以写为 cur["#"] = None
def search(self, word: str) -> bool:
cur = self.root
'''
for char in word:
node = cur.get(char)
if node is None:
return False
'''
for char in word:
if char not in cur:
return False
cur = cur[char] # 结点下移
'''
如果有单词结束标志,说明查询成功,返回true
if "#" in tree:
return True
return False
'''
return "#" in cur
def startsWith(self, prefix: str) -> bool:
cur = self.root
'''
for char in prefix:
node = cur.get(char)
if node is None:
return False
'''
for char in prefix:
if char not in cur:
return False
cur = cur[char] # 结点下移
return True # 不需要判断最后一个字符结点的 isVal,因为既然能匹配到最后一个字符,那后面一定有单词是以它为前缀的
作者:powcai
链接:https://leetcode.cn/problems/implement-trie-prefix-tree/solution/pythonjian-dan-shi-xian-by-powcai/
时间复杂度:初始化为 O(1),其余操作为 O(|S|),其中 |S| 是每次插入或查询的字符串的长度,插入和查询操作需要遍历一次字符串。
空间复杂度:O(|T|⋅Σ),其中 |T| 为所有插入字符串的长度之和,Σ 为字符集的大小,本题 Σ=26。
Trie 树常用于自动补全、拼写检查、单词搜索、高频统计等领域,一般都是这个模板再加个次数。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix