leetcode
leetcode 151 ~ 200
添加与搜索单词 - 数据结构设计

添加与搜索单词 - 数据结构设计

难度:

标签:

题目描述

请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。

实现词典类 WordDictionary

  • WordDictionary() 初始化词典对象
  • void addWord(word)word 添加到数据结构中,之后可以对它进行匹配
  • bool search(word) 如果数据结构中存在字符串与 word 匹配,则返回 true ;否则,返回  falseword 中可能包含一些 '.' ,每个 . 都可以表示任何一个字母。

 

示例:

输入:
["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
  • addWord 中的 word 由小写英文字母组成
  • search 中的 word 由 '.' 或小写英文字母组成
  • 最多调用 104addWordsearch

代码结果

运行时间: 660 ms, 内存: 0.0 MB


/*
 * 题目思路:
 * 1. 使用 Trie 树存储添加的单词,每个节点代表一个字母。
 * 2. 利用 Java Stream 流式操作优化搜索通配符 '.' 的逻辑。
 */
import java.util.*;
import java.util.stream.*;
 
public class WordDictionary {
    private TrieNode root;
 
    public WordDictionary() {
        root = new TrieNode();
    }
 
    // Trie 树节点类
    private class TrieNode {
        private TrieNode[] children;
        private boolean isEnd;
 
        public TrieNode() {
            children = new TrieNode[26];
            isEnd = false;
        }
    }
 
    // 添加单词到 Trie 树
    public void addWord(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) {
        return search(word, root);
    }
 
    private boolean search(String word, TrieNode node) {
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            if (c == '.') {
                return Arrays.stream(node.children)
                        .filter(Objects::nonNull)
                        .anyMatch(child -> search(word.substring(i + 1), child));
            } else {
                if (node.children[c - 'a'] == null) {
                    return false;
                }
                node = node.children[c - 'a'];
            }
        }
        return node.isEnd;
    }
}

解释

方法:

该题解使用字典树(Trie)的数据结构来解决问题。在添加单词时,将单词的每个字符作为字典树的节点插入。在搜索单词时,从字典树的根节点开始,逐个字符进行匹配。如果遇到 '.',则对当前节点的所有子节点进行递归搜索。如果搜索到字典树的叶子节点,且该节点标记为单词结尾,则说明找到了匹配的单词。

时间复杂度:

添加单词:O(L),搜索单词:O(M)

空间复杂度:

O(N)

代码细节讲解

🦆
在`search`函数中使用递归搜索时,如果遇到'.'字符,为什么选择遍历所有子节点而不是选择特定的一些子节点进行搜索?
在`search`函数中,'.'字符被设计为一个通配符,它可以匹配任意一个字符。因此,当遇到'.'时,必须考虑字典树中当前节点下的所有可能的子节点。这是因为任何一个子节点都有可能是目标单词路径的一部分。如果只选择特定的子节点进行搜索,将无法保证完全匹配所有可能的单词,特别是在不知道下一个字符应该是什么的情况下。
🦆
字典树中的`is_word`属性具体是如何表示一个单词结束的?如果在遍历到最后一个字符时`is_word`不为真,是如何处理的?
`is_word`属性在字典树的节点中用于标识该节点是否代表单词的结束。当添加一个单词到字典树时,该单词的最后一个字符对应的节点会将`is_word`设置为真。在搜索过程中,即使成功地匹配到了单词的最后一个字符,如果该字符对应的节点的`is_word`属性不为真,则表明该路径虽然存在,但不代表一个完整的单词。例如,如果单词'good'在字典树中,但是搜索'goo',虽然路径存在,但'goo'的结尾节点`is_word`为假,因此不会返回找到单词。
🦆
在添加单词到字典树时,如果单词中包含重复字符怎样处理?例如添加单词'good'和'goody',字典树的结构会如何变化?
在添加单词到字典树时,重复字符被正常处理,按照单词的顺序逐个添加到字典树中。例如,首先添加'good',将会创建路径g -> o -> o -> d,节点d的`is_word`被设置为真。当添加'goody'时,从根节点开始,g和o已存在,继续沿用现有路径。到达d后,添加一个新的子节点y,将y的`is_word`设置为真。这样,字典树能够共享前缀,节省空间,同时能够区分不同的单词。
🦆
对于`search_word`函数中的递归调用,如果输入单词非常长或字典树结构很深,有没有可能导致栈溢出?如何优化以防止这种情况?
确实,如果输入单词非常长或字典树结构特别深,递归调用可能导致栈溢出。为防止这种情况,可以考虑使用迭代而非递归进行搜索。迭代方法可以使用栈或队列显式管理节点而不依赖于函数调用栈,从而避免栈溢出的风险。此外,还可以设置递归深度限制或优化字典树的结构,比如通过压缩路径或合并冗余节点来减少树的深度。

相关问题

实现 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

 

提示:

  • 1 <= word.length, prefix.length <= 2000
  • wordprefix 仅由小写英文字母组成
  • insertsearchstartsWith 调用次数 总计 不超过 3 * 104