题目
在给定单词列表 wordlist 的情况下,我们希望实现一个拼写检查器,将查询单词转换为正确的单词。
对于给定的查询单词 query,拼写检查器将会处理两类拼写错误:
- 大小写:如果查询匹配单词列表中的某个单词(不区分大小写),则返回的正确单词与单词列表中的大小写相同。
- 例如:
wordlist = ["yellow"],query = "YellOw":correct = "yellow" - 例如:
wordlist = ["Yellow"],query = "yellow":correct = "Yellow" - 例如:
wordlist = ["yellow"],query = "yellow":correct = "yellow"
- 例如:
- 元音错误:如果在将查询单词中的元音
('a', 'e', 'i', 'o', 'u')分别替换为任何元音后,能与单词列表中的单词匹配(不区分大小写),则返回的正确单词与单词列表中的匹配项大小写相同。- 例如:
wordlist = ["YellOw"],query = "yollow":correct = "YellOw" - 例如:
wordlist = ["YellOw"],query = "yeellow":correct = ""(无匹配项) - 例如:
wordlist = ["YellOw"],query = "yllw":correct = ""(无匹配项)
- 例如:
此外,拼写检查器还按照以下优先级规则操作:
- 当查询完全匹配单词列表中的某个单词(区分大小写)时,应返回相同的单词。
- 当查询匹配到大小写问题的单词时,您应该返回单词列表中的第一个这样的匹配项。
- 当查询匹配到元音错误的单词时,您应该返回单词列表中的第一个这样的匹配项。
- 如果该查询在单词列表中没有匹配项,则应返回空字符串。
给出一些查询 queries,返回一个单词列表 answer,其中 answer[i] 是由查询 query = queries[i]得到的正确单词。
示例 1:
输入:wordlist = ["KiTe","kite","hare","Hare"], queries = ["kite","Kite","KiTe","Hare","HARE","Hear","hear","keti","keet","keto"] 输出:["kite","KiTe","KiTe","Hare","hare","","","KiTe","","KiTe"]
示例 2:
输入:wordlist = ["yellow"], queries = ["YellOw"] 输出:["yellow"]
提示:
1 <= wordlist.length, queries.length <= 50001 <= wordlist[i].length, queries[i].length <= 7wordlist[i]和queries[i]只包含英文字母
解题思路
这道题的难点和关键点有三个:
- 三种不同的匹配规则:完全匹配、大小写匹配、元音匹配。
- 严格的优先级:必须先检查“完全匹配”,再检查“大小写”,最后检查“元音”。一旦在高优先级找到匹配,就绝不能再用低优先级的规则去覆盖它。
- 性能要求:
wordlist和queries的长度可能很大,使用双重循环(一个 query 遍历一遍 wordlist)的暴力解法一定会超时。
为了避免每次查询都遍历整个 wordlist,我们可以预先处理 wordlist,将其信息存储在可以进行快速查找的数据结构中。哈希表 (std::unordered_map / std::unordered_set) 是这个场景下的完美工具,因为它能提供平均 O(1) 的查找速度。
第一步:预处理 (Preprocessing)
遍历一遍 wordlist,构建三个查找工具,分别对应三种匹配规则:
- 为“完全匹配”准备:
- 工具:一个
unordered_set<string>,我们叫它exact_words。 - 作用:将
wordlist中所有单词原封不动地放进去。之后要判断一个 query 是否完全匹配,只需要在 set 中查找一下,时间是 O(1)。
- 工具:一个
- 为“大小写匹配”准备:
- 工具:一个
unordered_map<string, string>,我们叫它case_insensitive_map。 - 作用:键是单词的小写形式,值是它在
wordlist中的原始形式。 - 关键细节:题目要求返回第一个匹配项。所以,当我们遍历
wordlist构建这个 map 时,只有当一个小写键不存在时,我们才将它和对应的原始单词存入。如果键已存在,就跳过,这样可以保证存入的始终是wordlist中最先出现的那个。
- 工具:一个
- 为“元音匹配”准备:
- 工具:另一个
unordered_map<string, string>,我们叫它vowel_error_map。 - 作用:键是单词的“元音模糊”形式(即转成小写,再把所有元音替换成一个通用占位符,如
'*'),值是它的原始形式。 - 关键细节:同样,只存入键不存在的词,以保证“第一个匹配项”的规则。
- 工具:另一个
第二步:查询 (Querying)
现在我们有了三个强大的“数据库”,对于每一个 query,我们严格按照优先级顺序进行查找:
- 在
exact_words中查找query。如果找到了,它就是答案,立即处理下一个 query。 - 如果上一步没找到,将
query转为小写,在case_insensitive_map中查找。如果找到了,对应的值就是答案,立即处理下一个 query。 - 如果上一步还没找到,将
query转为“元音模糊”形式,在vowel_error_map中查找。如果找到了,对应的值就是答案,立即处理下一个 query。 - 如果都找不到,答案就是空字符串
""。
时间复杂度
总时间复杂度是 $O((M+N)⋅L)$
其中:
- M 是
wordlist的长度。 - N 是
queries的长度。 - L 是单词的平均长度。
这个复杂度主要来自两个独立的部分:
- 预处理阶段:
- 我们遍历
wordlist一次,其中有M个单词。 - 对于每个单词,我们需要执行小写转换、元音模糊化处理以及哈希表的插入操作。这些操作的时间都与单词的长度
L成正比(因为需要遍历字符串并计算哈希值)。 - 所以,这个阶段的复杂度是 $O(M⋅L)$。
- 我们遍历
- 查询阶段:
- 我们遍历
queries一次,其中有N个查询词。 - 对于每个查询词,我们最多执行三次查找操作(在 set 和两个 map 中)。每次查找,同样需要对字符串进行转换和计算哈希值,时间也与单词长度
L成正比。 - 所以,这个阶段的复杂度是 $O(N⋅L)$。
- 我们遍历
将这两个阶段相加,总的时间复杂度就是 $O(M⋅L+N⋅L)$,可以合并写为 $O((M+N)⋅L)$。
这个方法的空间复杂度也是 $O((M+N)⋅L)$,因为它需要额外的哈希表来存储预处理后的数据,是典型的“空间换时间”策略。