一、题目描述
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 <= 2000
word
和prefix
仅由小写英文字母组成insert
、search
和startsWith
调用次数 总计 不超过3 * 10^4
次
二、解题思路
-
Trie树的基本结构:每个节点包含一个字符和一个布尔值(表示是否是单词的结尾),以及一个子节点数组(大小为26,对应26个小写英文字母)。
-
初始化:创建一个根节点,不包含任何字符,并且不是单词的结尾。
-
插入操作:
- 从根节点开始,遍历字符串的每个字符。
- 对于每个字符,判断当前节点的子节点数组中是否存在对应的字符节点,如果不存在,则创建一个新的节点。
- 遍历到字符串的最后一个字符时,将最后一个节点标记为单词的结尾。
-
搜索操作:
- 从根节点开始,遍历字符串的每个字符。
- 对于每个字符,判断当前节点的子节点数组中是否存在对应的字符节点,如果不存在,则返回false。
- 遍历完整个字符串后,判断最后一个节点是否是单词的结尾,如果是,则返回true,否则返回false。
-
前缀搜索操作:
- 从根节点开始,遍历前缀的每个字符。
- 对于每个字符,判断当前节点的子节点数组中是否存在对应的字符节点,如果不存在,则返回false。
- 遍历完整个前缀后,返回true。
三、具体代码
class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
if (node.children[c - 'a'] == null) {
node.children[c - 'a'] = new TrieNode();
}
node = node.children[c - 'a'];
}
node.isEnd = true;
}
public boolean search(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
if (node.children[c - 'a'] == null) {
return false;
}
node = node.children[c - 'a'];
}
return node.isEnd;
}
public boolean startsWith(String prefix) {
TrieNode node = root;
for (char c : prefix.toCharArray()) {
if (node.children[c - 'a'] == null) {
return false;
}
node = node.children[c - 'a'];
}
return true;
}
}
class TrieNode {
public TrieNode[] children;
public boolean isEnd;
public TrieNode() {
children = new TrieNode[26];
isEnd = false;
}
}
四、时间复杂度和空间复杂度
1. 时间复杂度
-
insert
方法:- 对于每个插入的字符串,我们遍历一次字符串的每个字符。
- 在每次遍历中,我们访问和可能创建一个新节点,这是一个常数时间操作(O(1))。
- 因此,
insert
方法的时间复杂度是 O(n),其中 n 是字符串的长度。
-
search
方法:- 对于每个搜索操作,我们遍历一次字符串的每个字符。
- 在每次遍历中,我们检查是否存在对应的子节点,这是一个常数时间操作(O(1))。
- 因此,
search
方法的时间复杂度是 O(n),其中 n 是字符串的长度。
-
startsWith
方法:- 对于每个前缀搜索操作,我们遍历一次前缀的每个字符。
- 在每次遍历中,我们检查是否存在对应的子节点,这是一个常数时间操作(O(1))。
- 因此,
startsWith
方法的时间复杂度是 O(n),其中 n 是前缀的长度。
2. 空间复杂度
-
Trie树的空间复杂度取决于插入的字符串的数量和它们的长度。
- Trie树中的每个节点都包含一个长度为26的数组,用于存储子节点,以及一个布尔值来标记是否是单词的结尾。
- 如果插入的字符串长度总和为 S,并且平均每个字符串的长度为 L,那么最坏情况下(没有公共前缀),Trie树的空间复杂度将是 O(S)。
- 在实际情况下,由于字符串之间可能有公共前缀,空间复杂度会小于 O(S)。
-
具体来说:
- Trie树中的节点总数取决于插入的字符串中不同字符的组合数。
- 如果所有的字符串都是完全不同的,那么空间复杂度将是 O(m * L),其中 m 是插入的字符串的数量,L 是字符串的最大长度。
- 在最坏的情况下,即每个字符串都不共享任何前缀,空间复杂度将是 O(m * L)。
- 在平均情况下,空间复杂度会小于 O(m * L),因为字符串之间会有共享的前缀。
3. 总结
insert
方法的时间复杂度:O(n),空间复杂度:O(m * L)(最坏情况),平均小于 O(m * L)。search
方法的时间复杂度:O(n),空间复杂度:O(1)(不需要额外空间)。startsWith
方法的时间复杂度:O(n),空间复杂度:O(1)(不需要额外空间)。
五、总结知识点
-
数据结构:
- 树形结构:Trie树是一种树形结构,用于高效地存储和检索字符串数据集。
- 节点:
TrieNode
类代表Trie树中的节点,每个节点包含一个子节点数组和一个标记是否为单词结尾的布尔值。
-
字符与整数的转换:
- 使用字符与整数的转换(
char - 'a'
)来计算字符在子节点数组中的索引位置,从而实现只使用固定大小的数组来存储子节点。
- 使用字符与整数的转换(
-
数组的初始化与访问:
- 数组的初始化:
TrieNode
类的构造函数中初始化了一个大小为26的TrieNode
数组,用于存储26个小写英文字母的子节点。 - 数组的访问:在
insert
、search
和startsWith
方法中,通过字符索引来访问子节点数组。
- 数组的初始化:
-
字符串遍历:
- 使用
String.toCharArray()
方法将字符串转换为字符数组,然后通过for-each循环遍历字符数组。
- 使用
-
递归思想:
- 虽然代码中没有显式的递归调用,但
insert
、search
和startsWith
方法都隐含了递归的思想,即在每个节点上,根据当前字符向下移动到下一个子节点。
- 虽然代码中没有显式的递归调用,但
-
成员变量与构造函数:
Trie
类包含一个成员变量root
,它是一个TrieNode
对象,代表Trie树的根节点。Trie
类的构造函数初始化根节点。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。