LeetCode 第211题:添加与搜索单词 - 数据结构设计
题目描述
请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。
实现词典类 WordDictionary
:
WordDictionary()
初始化词典对象void addWord(word)
将word
添加到数据结构中,之后可以对它进行匹配bool search(word)
如果数据结构中存在字符串与word
匹配,则返回true
;否则,返回false
。word
中可能包含一些.
,每个.
都可以表示任何一个字母。
难度
中等
题目链接
示例
输入:
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
输出:
[null,null,null,null,false,true,true,true]
解释:
WordDictionary wordDictionary = new WordDictionary();
wordDictionary.addWord("bad");
wordDictionary.addWord("dad");
wordDictionary.addWord("mad");
wordDictionary.search("pad"); // 返回 False
wordDictionary.search("bad"); // 返回 True
wordDictionary.search(".ad"); // 返回 True
wordDictionary.search("b.."); // 返回 True
提示
1 <= word.length <= 25
word
仅由小写英文字母和.
组成- 最多调用
3 * 10^4
次addWord
和search
解题思路
这道题是要实现一个支持通配符查找的单词字典,主要有两个操作:添加单词和搜索单词。搜索时可能包含通配符.
,代表任意一个字符。
由于涉及到字符串的存储和前缀匹配,我们可以使用**Trie(前缀树)**这种数据结构来解决问题。Trie树特别适合用来解决需要快速检索字符串的问题。
方法:Trie树 + 深度优先搜索
我们可以构建一个Trie树来存储所有添加的单词,而当搜索包含通配符的单词时,使用深度优先搜索(DFS)来探索所有可能的匹配路径。
关键点:
- 设计Trie节点结构,包含子节点和是否是单词结尾的标记
- 实现
addWord
方法,将单词添加到Trie树中 - 实现
search
方法,结合DFS来处理通配符的情况
当遇到通配符.
时,需要尝试所有可能的字符,这可以通过递归搜索Trie树的所有子节点来实现。
时间复杂度:
addWord
: O(m),其中m是单词的长度search
:- 最好情况(无通配符):O(m)
- 最坏情况(全部是通配符):O(26^m),因为每个位置都有26种可能
空间复杂度:O(N*m),其中N是单词的数量,m是平均单词长度
代码实现
C# 实现
public class WordDictionary {
private class TrieNode {
public TrieNode[] Children { get; }
public bool IsEndOfWord { get; set; }
public TrieNode() {
Children = new TrieNode[26]; // 26个小写字母
IsEndOfWord = false;
}
}
private readonly TrieNode root;
public WordDictionary() {
root = new TrieNode();
}
public void AddWord(string word) {
TrieNode node = root;
foreach (char c in word) {
int index = c - 'a';
if (node.Children[index] == null) {
node.Children[index] = new TrieNode();
}
node = node.Children[index];
}
node.IsEndOfWord = true;
}
public bool Search(string word) {
return SearchHelper(word, 0, root);
}
private bool SearchHelper(string word, int index, TrieNode node) {
// 已经处理完整个单词
if (index == word.Length) {
return node.IsEndOfWord;
}
char c = word[index];
// 如果是通配符,则尝试所有可能的字符
if (c == '.') {
for (int i = 0; i < 26; i++) {
if (node.Children[i] != null && SearchHelper(word, index + 1, node.Children[i])) {
return true;
}
}
return false;
}
// 普通字符,按照常规Trie查找
else {
int charIndex = c - 'a';
return node.Children[charIndex] != null &&
SearchHelper(word, index + 1, node.Children[charIndex]);
}
}
}
/**
* Your WordDictionary object will be instantiated and called as such:
* WordDictionary obj = new WordDictionary();
* obj.AddWord(word);
* bool param_2 = obj.Search(word);
*/
Python 实现
class WordDictionary:
def __init__(self):
self.trie = {} # 使用字典实现Trie
def addWord(self, word: str) -> None:
node = self.trie
for char in word:
if char not in node:
node[char] = {}
node = node[char]
node['$'] = True # 使用'$'标记单词结尾
def search(self, word: str) -> bool:
def dfs(node, index):
# 如果已经处理完整个单词
if index == len(word):
return '$' in node
char = word[index]
# 如果是通配符,则尝试所有可能的字符
if char == '.':
for key in node:
if key != '$' and dfs(node[key], index + 1):
return True
return False
# 普通字符,按照常规Trie查找
else:
if char not in node:
return False
return dfs(node[char], index + 1)
return dfs(self.trie, 0)
# Your WordDictionary object will be instantiated and called as such:
# obj = WordDictionary()
# obj.addWord(word)
# param_2 = obj.search(word)
C++ 实现
class WordDictionary {
private:
struct TrieNode {
TrieNode* children[26];
bool isEndOfWord;
TrieNode() {
for (int i = 0; i < 26; i++) {
children[i] = nullptr;
}
isEndOfWord = false;
}
~TrieNode() {
for (int i = 0; i < 26; i++) {
if (children[i]) {
delete children[i];
}
}
}
};
TrieNode* root;
bool searchHelper(const string& word, int index, TrieNode* node) {
// 已经处理完整个单词
if (index == word.length()) {
return node->isEndOfWord;
}
char c = word[index];
// 如果是通配符,则尝试所有可能的字符
if (c == '.') {
for (int i = 0; i < 26; i++) {
if (node->children[i] && searchHelper(word, index + 1, node->children[i])) {
return true;
}
}
return false;
}
// 普通字符,按照常规Trie查找
else {
int charIndex = c - 'a';
return node->children[charIndex] &&
searchHelper(word, index + 1, node->children[charIndex]);
}
}
public:
WordDictionary() {
root = new TrieNode();
}
~WordDictionary() {
delete root;
}
void addWord(string word) {
TrieNode* node = root;
for (char c : word) {
int index = c - 'a';
if (!node->children[index]) {
node->children[index] = new TrieNode();
}
node = node->children[index];
}
node->isEndOfWord = true;
}
bool search(string word) {
return searchHelper(word, 0, root);
}
};
/**
* Your WordDictionary object will be instantiated and called as such:
* WordDictionary* obj = new WordDictionary();
* obj->addWord(word);
* bool param_2 = obj->search(word);
*/
性能分析
各语言实现的性能对比:
实现语言 | 执行用时 | 内存消耗 | 特点 |
---|---|---|---|
C# | 400 ms | 76.2 MB | 使用数组表示子节点,DFS递归搜索 |
Python | 288 ms | 27.3 MB | 使用字典实现更灵活,代码简洁 |
C++ | 88 ms | 45.1 MB | 性能最佳,直接内存管理 |
补充说明
代码亮点
- 使用Trie树数据结构存储单词,提高检索效率
- 针对通配符的情况,采用DFS递归搜索所有可能的分支
- 递归函数设计简洁清晰,易于理解
- C++实现中注意了内存管理,防止内存泄漏
Trie树与通配符搜索结合
本题的难点在于如何处理通配符,我们采用递归DFS的方式,当遇到通配符时,尝试当前节点的所有子节点。这种方法可能在最坏情况下(如搜索"..."
)会导致大量的搜索分支,但在实际应用中,通配符通常不会太多,因此性能通常是可接受的。
优化方向
- 如果通配符特别多,可以考虑使用记忆化搜索来避免重复计算
- 对于特殊情况,如全部是通配符的查询,可以提前计算字典中各长度单词的数量,直接返回结果
- 可以根据实际应用场景调整Trie节点的实现,比如使用哈希表代替数组存储子节点,以支持更大的字符集
常见错误
- 忘记标记单词结尾,导致无法判断完整单词
- 递归终止条件设置不当,导致越界或死循环
- 处理通配符时遗漏某些分支
- C++实现中没有正确管理内存,导致内存泄漏