【LeetCode 每日一题】966. 元音拼写检查器
Problem: 966. 元音拼写检查器
文章目录
- 整体思路
- 完整代码
- 时空复杂度
- 时间复杂度:O(W*L + Q*L)
- 空间复杂度:O(W*L)
整体思路
这段代码旨在实现一个具有特定纠错规则的拼写检查器。它接收一个正确的单词列表 wordlist
和一个待检查的查询列表 queries
,然后根据三条优先级递减的规则来匹配或纠正每个查询。
算法的核心策略是 预处理 + 快速查找。它首先通过一次遍历 wordlist
来构建几个高效的查找数据结构(哈希集合和哈希映射),然后在处理 queries
时,利用这些预处理好的数据结构进行快速匹配。
-
匹配规则(按优先级):
- 完全匹配:查询词与
wordlist
中的某个词完全一样(包括大小写)。 - 忽略大小写匹配:查询词在忽略大小写后,与
wordlist
中的某个词(同样忽略大小写)匹配。 - 元音错误匹配:查询词在忽略大小写,并将所有元音字母替换为通用占位符(如
*
)后,与wordlist
中某个词经过同样处理后的模式相匹配。
- 完全匹配:查询词与
-
预处理阶段(构建查找表):
Set<String> origin
: 用于规则1。这是一个哈希集合,存储了wordlist
中所有原始单词,可以提供 O(1) 的平均时间复杂度的精确查找。Map<String, String> lowerToOrigin
: 用于规则2。这个哈希映射的键是wordlist
中单词的小写形式,值是对应的原始单词。这使得忽略大小写的匹配变得非常快。Map<String, String> vowelToOrigin
: 用于规则3。这个哈希映射的键是一个“元音模糊”的模式(例如,“Kitten” -> “kttn”),值是对应的原始单词。这是通过一个辅助函数replaceVowels
来生成模式的。- 重要细节(处理首次出现):代码在填充
lowerToOrigin
和vowelToOrigin
时,从后向前遍历wordlist
。这是为了处理一个隐含的规则:如果多个wordlist
中的单词在忽略大小写或元音后匹配同一个查询,应返回在wordlist
中首次出现的那个。通过从后向前put
,如果遇到冲突的键,后面的值会覆盖前面的,最终保留在map中的就是wordlist
中最靠前的那个单词。
-
查询处理阶段:
- 算法遍历每一个
query
。 - 它严格按照上述三条规则的优先级进行检查:
- 首先,在
origin
集合中查找,如果找到,说明是完全匹配,当前查询词正确,跳过。 - 如果上一步失败,将
query
转为小写,在lowerToOrigin
映射中查找,如果找到,就用映射中的原始单词替换当前查询词。 - 如果上一步仍失败,将小写的
query
转换为“元音模糊”模式,在vowelToOrigin
映射中查找。如果找到,就用映射中的原始单词替换;如果找不到,说明没有任何匹配,根据规则替换为空字符串""
。
- 首先,在
- 算法遍历每一个
完整代码
import java.util.*;class Solution {/*** 根据特定规则对查询词进行拼写检查和纠正。* @param wordlist 正确的单词列表* @param queries 待检查的查询词列表* @return 纠正后的查询词列表*/public String[] spellchecker(String[] wordlist, String[] queries) {int n = wordlist.length;// 查找表 1: 用于完全匹配(区分大小写)Set<String> origin = new HashSet<>(Arrays.asList(wordlist));// 查找表 2: 用于忽略大小写匹配。Key: 小写词, Value: 原始词Map<String, String> lowerToOrigin = new HashMap<>(n);// 查找表 3: 用于元音错误匹配。Key: 元音模糊模式, Value: 原始词Map<String, String> vowelToOrigin = new HashMap<>(n);// 预处理阶段:从后向前遍历 wordlist 以构建查找表// 从后向前是为了确保当有多个词匹配同一模式时,保留在 wordlist 中首次出现的那个for (int i = n - 1; i >= 0; i--) {String s = wordlist[i];String lower = s.toLowerCase();// 填充忽略大小写的查找表lowerToOrigin.put(lower, s);// 填充元音模糊的查找表vowelToOrigin.put(replaceVowels(lower), s);}// 查询处理阶段for (int i = 0; i < queries.length; i++) {String s = queries[i];// 规则 1: 完全匹配if (origin.contains(s)) {// 匹配成功,无需修改,继续下一个查询continue;}String lower = s.toLowerCase();// 规则 2: 忽略大小写匹配if (lowerToOrigin.containsKey(lower)) {queries[i] = lowerToOrigin.get(lower);} else {// 规则 3: 元音错误匹配// 如果前两条规则都失败,则尝试此规则// getOrDefault 可以在找不到匹配时优雅地返回默认值 ""queries[i] = vowelToOrigin.getOrDefault(replaceVowels(lower), "");}}return queries;}/*** 辅助函数:将字符串中的所有元音替换为 '*',生成一个元音模糊的模式。* @param lower 输入的小写字符串* @return 元音模糊模式字符串*/private String replaceVowels(String lower) {char[] s = lower.toCharArray();for (int i = 0; i < s.length; i++) {char c = s[i];if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {s[i] = '*';}}return new String(s);}
}
时空复杂度
- 设
W
为wordlist
的长度。 - 设
Q
为queries
的长度。 - 设
L
为单词的最大长度。
时间复杂度:O(WL + QL)
-
预处理阶段:
- 遍历
wordlist
W
次。 - 在循环内部,
toLowerCase()
和replaceVowels()
都需要遍历单词,时间复杂度为 O(L)。 HashSet.add()
和HashMap.put()
操作,对于字符串键,平均时间复杂度也是 O(L)(因为需要计算哈希值和进行比较)。- 因此,预处理阶段的总时间复杂度为
W * O(L) = O(W*L)
。
- 遍历
-
查询处理阶段:
- 遍历
queries
Q
次。 - 在循环内部,
contains()
,get()
,toLowerCase()
,replaceVowels()
等操作的平均时间复杂度都是 O(L)。 - 因此,查询阶段的总时间复杂度为
Q * O(L) = O(Q*L)
。
- 遍历
综合分析:
总的时间复杂度是两个阶段之和,即 O(WL + QL)。
空间复杂度:O(W*L)
- 主要存储开销:算法创建了三个数据结构来存储预处理信息。
origin
集合:存储了W
个单词,总空间占用与所有单词的总长度成正比,即 O(W*L)。lowerToOrigin
映射:存储了W
个键值对,总空间占用也是 O(W*L)。vowelToOrigin
映射:存储了W
个键值对,总空间占用同样是 O(W*L)。
- 辅助空间:在
replaceVowels
中创建的临时char[]
数组大小为 O(L),但这不影响整体复杂度。
综合分析:
算法所需的额外空间主要由这三个查找表决定。因此,总的空间复杂度为 O(W*L)。
参考灵神