Skip to content

Commit 9dbc13d

Browse files
committed
更新 1032. 字符流 题解报告
1 parent 5ffbe83 commit 9dbc13d

File tree

1 file changed

+273
-0
lines changed

1 file changed

+273
-0
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
# [1032. 字符流](https://leetcode.cn/problems/stream-of-characters/)
2+
3+
- 标签:设计、字典树、数组、字符串、数据流
4+
- 难度:困难
5+
6+
## 题目链接
7+
8+
- [1032. 字符流 - 力扣](https://leetcode.cn/problems/stream-of-characters/)
9+
10+
## 题目大意
11+
12+
**描述**:设计一个算法:接收一个字符流,并检查这些字符的后缀是否是字符串数组 $words$ 中的一个字符串。
13+
14+
**要求**
15+
16+
按下述要求实现 StreamChecker 类:
17+
18+
- `StreamChecker(String[] words):` 构造函数,用字符串数组 $words$ 初始化数据结构。
19+
- `boolean query(char letter):` 从字符流中接收一个新字符,如果字符流中的任一非空后缀能匹配 $words$ 中的某一字符串,返回 $True$;否则,返回 $False$。
20+
21+
**说明**
22+
23+
- $1 \le words.length \le 2000$。
24+
- $1 <= words[i].length <= 200$。
25+
- $words[i]$ 由小写英文字母组成。
26+
- $letter$ 是一个小写英文字母。
27+
- 最多调用查询 $4 \times 10^4$ 次。
28+
29+
**示例**
30+
31+
- 示例 1:
32+
33+
```python
34+
输入:
35+
["StreamChecker", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query"]
36+
[[["cd", "f", "kl"]], ["a"], ["b"], ["c"], ["d"], ["e"], ["f"], ["g"], ["h"], ["i"], ["j"], ["k"], ["l"]]
37+
输出:
38+
[null, false, false, false, true, false, true, false, false, false, false, false, true]
39+
40+
解释:
41+
StreamChecker streamChecker = new StreamChecker(["cd", "f", "kl"]);
42+
streamChecker.query("a"); // 返回 False
43+
streamChecker.query("b"); // 返回 False
44+
streamChecker.query("c"); // 返回n False
45+
streamChecker.query("d"); // 返回 True ,因为 'cd' 在 words 中
46+
streamChecker.query("e"); // 返回 False
47+
streamChecker.query("f"); // 返回 True ,因为 'f' 在 words 中
48+
streamChecker.query("g"); // 返回 False
49+
streamChecker.query("h"); // 返回 False
50+
streamChecker.query("i"); // 返回 False
51+
streamChecker.query("j"); // 返回 False
52+
streamChecker.query("k"); // 返回 False
53+
streamChecker.query("l"); // 返回 True ,因为 'kl' 在 words 中
54+
```
55+
56+
## 解题思路
57+
58+
这道题要求设计一个数据结构,能够实时检查字符流中的后缀是否匹配给定的单词集合。由于字符流是动态的,我们需要高效地处理每个新字符的查询。
59+
60+
### 思路 1:字典树 + 字符串反转
61+
62+
**问题分析**
63+
- 需要检查字符流中的后缀是否匹配单词集合中的任意单词
64+
- 字符流是动态添加的,直接存储所有可能的后缀会非常低效
65+
- 字典树适合前缀匹配,但我们需要后缀匹配
66+
67+
**核心思想**
68+
将后缀匹配问题转化为前缀匹配问题:将所有单词反转后插入字典树,这样检查后缀就变成了检查前缀。
69+
70+
**算法步骤**
71+
1. **初始化**:将所有单词反转后插入字典树中
72+
2. **查询处理**:每次接收到新字符时,将其添加到字符流的前面
73+
3. **匹配检查**:在字典树中搜索当前字符流,找到匹配的单词就返回 `True`
74+
75+
**关键优化**
76+
- 使用反转的单词构建字典树,将后缀匹配转化为前缀匹配
77+
- 在搜索过程中,一旦找到匹配的单词就立即返回,避免不必要的继续搜索
78+
79+
**示例分析**
80+
- 单词集合:`["cd", "f", "kl"]` → 插入字典树:`["dc", "f", "lk"]`
81+
- 字符流 `"cd"` → 检查 `"dc"` 是否在字典树中 → 匹配成功,返回 `True`
82+
83+
84+
### 思路 1:代码
85+
86+
```python
87+
class Node: # 字符节点
88+
def __init__(self): # 初始化字符节点
89+
self.children = dict() # 初始化子节点
90+
self.isEnd = False # isEnd 用于标记单词结束
91+
92+
93+
class Trie: # 字典树
94+
95+
# 初始化字典树
96+
def __init__(self): # 初始化字典树
97+
self.root = Node() # 初始化根节点(根节点不保存字符)
98+
99+
# 向字典树中插入一个单词
100+
def insert(self, word: str) -> None:
101+
cur = self.root
102+
for ch in word: # 遍历单词中的字符
103+
if ch not in cur.children: # 如果当前节点的子节点中,不存在键为 ch 的节点
104+
cur.children[ch] = Node() # 建立一个节点,并将其保存到当前节点的子节点
105+
cur = cur.children[ch] # 令当前节点指向新建立的节点,继续处理下一个字符
106+
cur.isEnd = True # 单词处理完成时,将当前节点标记为单词结束
107+
108+
# 查找字典树中是否存在一个单词
109+
def search(self, word: str) -> bool:
110+
cur = self.root
111+
for ch in word: # 遍历单词中的字符
112+
if ch not in cur.children: # 如果当前节点的子节点中,不存在键为 ch 的节点
113+
return False # 直接返回 False
114+
cur = cur.children[ch] # 令当前节点指向新建立的节点,然后继续查找下一个字符
115+
if cur.isEnd:
116+
return True
117+
return False
118+
119+
# 查找字典树中是否存在一个前缀
120+
def startsWith(self, prefix: str) -> bool:
121+
cur = self.root
122+
for ch in prefix: # 遍历前缀中的字符
123+
if ch not in cur.children: # 如果当前节点的子节点中,不存在键为 ch 的节点
124+
return False # 直接返回 False
125+
cur = cur.children[ch] # 令当前节点指向新建立的节点,然后继续查找下一个字符
126+
return cur is not None # 判断当前节点是否为空,不为空则查找成功
127+
128+
class StreamChecker:
129+
130+
def __init__(self, words: List[str]):
131+
self.trie = Trie()
132+
self.stream = ""
133+
for word in words:
134+
self.trie.insert(word[::-1])
135+
136+
def query(self, letter: str) -> bool:
137+
self.stream = letter + self.stream
138+
size = len(letter)
139+
140+
return self.trie.search(self.stream)
141+
142+
143+
144+
# Your StreamChecker object will be instantiated and called as such:
145+
# obj = StreamChecker(words)
146+
# param_1 = obj.query(letter)
147+
```
148+
149+
### 思路 1:复杂度分析
150+
151+
- **时间复杂度**
152+
- 初始化:$O(m \times n)$,其中 $m$ 是单词数量,$n$ 是单词的平均长度。
153+
- 查询:$O(k)$,其中 $k$ 是当前字符流的长度。最坏情况下,每次查询都需要遍历整个字符流。
154+
- **空间复杂度**:$O(m \times n)$,字典树的空间复杂度,其中 $m$ 是单词数量,$n$ 是单词的平均长度。
155+
156+
### 思路 2:AC 自动机
157+
158+
**问题分析**
159+
- 需要处理多模式串匹配问题,适合使用 AC 自动机
160+
- 字符流查询频率高,需要优化查询时间复杂度
161+
- 不需要存储完整的字符流历史,只需要维护当前匹配状态
162+
163+
**核心思想**
164+
使用 AC 自动机(Aho-Corasick Automaton)进行多模式串匹配:
165+
1. 将所有单词构建成AC自动机,利用字典树共享公共前缀
166+
2. 为每个节点设置失配指针,实现匹配失败时的快速跳转
167+
3. 维护当前匹配状态,每次接收新字符时更新状态并检查匹配
168+
169+
**算法步骤**
170+
1. **构建AC自动机**:将所有单词插入字典树,并构建失配指针
171+
2. **维护匹配状态**:使用变量记录当前在AC自动机中的位置
172+
3. **字符流处理**:每次接收新字符时,沿着AC自动机进行状态转移
173+
4. **匹配检测**:检查当前状态及其失配链上是否有单词结尾
174+
175+
**关键优势**
176+
- **时间复杂度优秀**:构建 $O(m)$,查询平均 $O(1)$
177+
- **空间效率高**:共享公共前缀,节省存储空间
178+
- **适合流式处理**:不需要存储整个字符流历史
179+
180+
### 思路 2:代码
181+
182+
```python
183+
class TrieNode:
184+
def __init__(self):
185+
self.children = {} # 子节点,key 为字符,value 为 TrieNode
186+
self.fail = None # 失配指针,指向当前节点最长可用后缀的节点
187+
self.is_end = False # 是否为某个模式串的结尾
188+
self.word = "" # 如果是结尾,存储完整的单词
189+
190+
class AC_Automaton:
191+
def __init__(self):
192+
self.root = TrieNode() # 初始化根节点
193+
194+
def add_word(self, word):
195+
"""
196+
向Trie树中插入一个模式串
197+
"""
198+
node = self.root
199+
for char in word:
200+
if char not in node.children:
201+
node.children[char] = TrieNode() # 新建子节点
202+
node = node.children[char]
203+
node.is_end = True # 标记单词结尾
204+
node.word = word # 存储完整单词
205+
206+
def build_fail_pointers(self):
207+
"""
208+
构建失配指针(fail指针),采用BFS广度优先遍历
209+
"""
210+
from collections import deque
211+
queue = deque()
212+
# 1. 根节点的所有子节点的 fail 指针都指向根节点
213+
for child in self.root.children.values():
214+
child.fail = self.root
215+
queue.append(child)
216+
217+
# 2. 广度优先遍历,依次为每个节点建立 fail 指针
218+
while queue:
219+
current = queue.popleft()
220+
for char, child in current.children.items():
221+
# 从当前节点的 fail 指针开始,向上寻找有无相同字符的子节点
222+
fail = current.fail
223+
while fail and char not in fail.children:
224+
fail = fail.fail
225+
# 如果找到了,child的fail指针指向该节点,否则指向根节点
226+
child.fail = fail.children[char] if fail and char in fail.children else self.root
227+
queue.append(child)
228+
229+
class StreamChecker:
230+
def __init__(self, words):
231+
self.ac = AC_Automaton()
232+
# 将所有单词插入AC自动机
233+
for word in words:
234+
self.ac.add_word(word)
235+
# 构建失配指针
236+
self.ac.build_fail_pointers()
237+
# 当前匹配状态
238+
self.current_node = self.ac.root
239+
240+
def query(self, letter):
241+
"""
242+
处理新字符,检查是否匹配到任何单词
243+
"""
244+
# 如果当前节点没有该字符的子节点,则沿 fail 指针向上跳转
245+
while self.current_node is not self.ac.root and letter not in self.current_node.children:
246+
self.current_node = self.current_node.fail
247+
248+
# 如果有该字符的子节点,则转移到该子节点
249+
if letter in self.current_node.children:
250+
self.current_node = self.current_node.children[letter]
251+
# 否则仍然停留在根节点
252+
253+
# 检查当前节点以及沿 fail 链上的所有节点是否为单词结尾
254+
temp = self.current_node
255+
while temp is not self.ac.root:
256+
if temp.is_end:
257+
return True # 找到匹配的单词
258+
temp = temp.fail
259+
260+
return False # 没有找到匹配的单词
261+
262+
263+
# Your StreamChecker object will be instantiated and called as such:
264+
# obj = StreamChecker(words)
265+
# param_1 = obj.query(letter)
266+
```
267+
268+
### 思路 2:复杂度分析
269+
270+
- **时间复杂度**
271+
- 初始化:$O(m)$,其中 $m$ 是所有单词的总长度。构建字典树和失配指针都是线性时间。
272+
- 查询:$O(1)$ 平均情况,$O(k)$ 最坏情况,其中 $k$ 是单词的最大长度。由于失配指针的存在,大部分情况下可以快速跳转。
273+
- **空间复杂度**:$O(m)$,其中 $m$ 是所有单词的总长度。AC自动机的空间复杂度主要由字典树决定。

0 commit comments

Comments
 (0)