Hard

题目描述

设计一个算法,接受一个字符流,并检查这些字符的后缀是否是给定字符串数组 words 中的字符串。

例如,如果 words = [“abc”, “xyz”] 并且字符流按顺序添加了四个字符 ‘a’、‘x’、‘y’ 和 ‘z’,你的算法应该检测到字符 “axyz” 的后缀 “xyz” 与 words 中的 “xyz” 匹配。

实现 StreamChecker 类:

  • StreamChecker(String[] words) 用字符串数组 words 初始化对象。
  • boolean query(char letter) 接受字符流中的一个新字符,如果从流中形成的任何非空后缀在 words 中,则返回 true。

示例 1:

输入
["StreamChecker", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query"]
[[["cd", "f", "kl"]], ["a"], ["b"], ["c"], ["d"], ["e"], ["f"], ["g"], ["h"], ["i"], ["j"], ["k"], ["l"]]
输出
[null, false, false, false, true, false, true, false, false, false, false, false, true]

解释
StreamChecker streamChecker = new StreamChecker(["cd", "f", "kl"]);
streamChecker.query("a"); // 返回 False
streamChecker.query("b"); // 返回 False
streamChecker.query("c"); // 返回 False
streamChecker.query("d"); // 返回 True,因为 'cd' 在 wordlist 中
streamChecker.query("e"); // 返回 False
streamChecker.query("f"); // 返回 True,因为 'f' 在 wordlist 中
streamChecker.query("g"); // 返回 False
streamChecker.query("h"); // 返回 False
streamChecker.query("i"); // 返回 False
streamChecker.query("j"); // 返回 False
streamChecker.query("k"); // 返回 False
streamChecker.query("l"); // 返回 True,因为 'kl' 在 wordlist 中

约束条件:

  • 1 <= words.length <= 2000
  • 1 <= words[i].length <= 200
  • words[i] 由小写英文字母组成。
  • letter 是小写英文字母。
  • 最多会对 query 进行 4 * 10^4 次调用。

解题思路

这个问题要求我们检查字符流的后缀是否匹配给定的单词列表。有两种主要的解法思路:

方法一:字典树 + 活跃节点集合(推荐)

核心思想是维护一个"活跃节点"集合,表示当前可能匹配的字典树节点。每次接收新字符时,更新这个集合:

  1. 构建字典树:将所有单词插入字典树,标记结束节点
  2. 维护活跃节点:保存当前所有可能匹配路径的字典树节点
  3. 处理新字符
    • 从根节点开始一个新的匹配尝试
    • 更新所有活跃节点,移除无法继续匹配的节点
    • 检查是否有节点到达单词结尾

方法二:反向字典树 + 字符流缓存

将单词反向存储在字典树中,维护一个字符缓存,每次查询时反向匹配:

  1. 构建反向字典树:将所有单词反向插入字典树
  2. 维护字符流:保存接收到的字符(限制最大长度)
  3. 反向匹配:从最新字符开始,反向在字典树中查找匹配

方法一在空间和时间上都更优,因为避免了存储完整的字符流,且能够并行处理多个匹配路径。

代码实现

class StreamChecker {
private:
    struct TrieNode {
        unordered_map<char, TrieNode*> children;
        bool isEnd = false;
    };
    
    TrieNode* root;
    vector<TrieNode*> activeNodes;
    
public:
    StreamChecker(vector<string>& words) {
        root = new TrieNode();
        
        // Build trie
        for (const string& word : words) {
            TrieNode* node = root;
            for (char c : word) {
                if (node->children.find(c) == node->children.end()) {
                    node->children[c] = new TrieNode();
                }
                node = node->children[c];
            }
            node->isEnd = true;
        }
    }
    
    bool query(char letter) {
        // Add root to start a new matching attempt
        activeNodes.push_back(root);
        
        vector<TrieNode*> newActiveNodes;
        bool found = false;
        
        // Update all active nodes
        for (TrieNode* node : activeNodes) {
            if (node->children.find(letter) != node->children.end()) {
                TrieNode* nextNode = node->children[letter];
                newActiveNodes.push_back(nextNode);
                if (nextNode->isEnd) {
                    found = true;
                }
            }
        }
        
        activeNodes = newActiveNodes;
        return found;
    }
};
class StreamChecker:
    class TrieNode:
        def __init__(self):
            self.children = {}
            self.is_end = False
    
    def __init__(self, words: List[str]):
        self.root = self.TrieNode()
        self.active_nodes = []
        
        # Build trie
        for word in words:
            node = self.root
            for c in word:
                if c not in node.children:
                    node.children[c] = self.TrieNode()
                node = node.children[c]
            node.is_end = True
    
    def query(self, letter: str) -> bool:
        # Add root to start a new matching attempt
        self.active_nodes.append(self.root)
        
        new_active_nodes = []
        found = False
        
        # Update all active nodes
        for node in self.active_nodes:
            if letter in node.children:
                next_node = node.children[letter]
                new_active_nodes.append(next_node)
                if next_node.is_end:
                    found = True
        
        self.active_nodes = new_active_nodes
        return found
public class StreamChecker {
    private class TrieNode {
        public Dictionary<char, TrieNode> Children = new Dictionary<char, TrieNode>();
        public bool IsEnd = false;
    }
    
    private TrieNode root;
    private List<TrieNode> activeNodes;
    
    public StreamChecker(string[] words) {
        root = new TrieNode();
        activeNodes = new List<TrieNode>();
        
        // Build trie
        foreach (string word in words) {
            TrieNode node = root;
            foreach (char c in word) {
                if (!node.Children.ContainsKey(c)) {
                    node.Children[c] = new TrieNode();
                }
                node = node.Children[c];
            }
            node.IsEnd = true;
        }
    }
    
    public bool Query(char letter) {
        // Add root to start a new matching attempt
        activeNodes.Add(root);
        
        List<TrieNode> newActiveNodes = new List<TrieNode>();
        bool found = false;
        
        // Update all active nodes
        foreach (TrieNode node in activeNodes) {
            if (node.Children.ContainsKey(letter)) {
                TrieNode nextNode = node.Children[letter];
                newActiveNodes.Add(nextNode);
                if (nextNode.IsEnd) {
                    found = true;
                }
            }
        }
        
        activeNodes = newActiveNodes;
        return found;
    }
}
var StreamChecker = function(words) {
    this.root = { children: {}, isEnd: false };
    this.activeNodes = [];
    
    // Build trie
    for (let word of words) {
        let node = this.root;
        for (let c of word) {
            if (!(c in node.children)) {
                node.children[c] = { children: {}, isEnd: false };
            }
            node = node.children[c];
        }
        node.isEnd = true;
    }
};

StreamChecker.prototype.query = function(letter) {
    // Add root to start a new matching attempt
    this.activeNodes.push(this.root);
    
    let newActiveNodes = [];
    let found = false;
    
    // Update all active nodes
    for (let node of this.activeNodes) {
        if (letter in node.children) {
            let nextNode = node.children[letter];
            newActiveNodes.push(nextNode);
            if (nextNode.isEnd) {
                found = true;
            }
        }
    }
    
    this.activeNodes = newActiveNodes;
    return found;
};

复杂度分析

操作时间复杂度空间复杂度
构造函数O(N×L)O(N×L)
queryO(W)O(W)

其中:

  • N 是单词数量
  • L 是单词的平均长度
  • W 是当前活跃节点的数量(最坏情况下等于所有单词的总长度)

说明:

  • 构造函数需要将所有单词插入字典树
  • 每次 query 操作的时间复杂度取决于当前活跃节点的数量
  • 空间复杂度主要由字典树结构和活跃节点列表决定