966. Vowel Spellchecker
Problem Description
You need to implement a spellchecker that takes a list of correct words (wordlist
) and converts query words into their correct forms based on specific rules.
The spellchecker handles two types of spelling mistakes:
1. Capitalization Errors: If a query word matches a word in the wordlist when compared case-insensitively, return the matching word from the wordlist with its original casing.
- Example:
wordlist = ["yellow"]
,query = "YellOw"
→ return"yellow"
- Example:
wordlist = ["Yellow"]
,query = "yellow"
→ return"Yellow"
2. Vowel Errors: If a query word matches a word in the wordlist after replacing any vowels ('a'
, 'e'
, 'i'
, 'o'
, 'u'
) with any other vowel (case-insensitive comparison), return the matching word from the wordlist.
- Example:
wordlist = ["YellOw"]
,query = "yollow"
→ return"YellOw"
(the'e'
in yellow can be replaced with'o'
) - Example:
wordlist = ["YellOw"]
,query = "yeellow"
→ return""
(no match - double vowels don't match single vowel) - Example:
wordlist = ["YellOw"]
,query = "yllw"
→ return""
(no match - missing vowel positions)
Priority Rules: The spellchecker follows this precedence order:
- Exact match (case-sensitive): If the query exactly matches a word in the wordlist, return that exact word
- Case-insensitive match: If the query matches a word ignoring case, return the first such match from the wordlist
- Vowel error match: If the query matches a word after vowel substitution (case-insensitive), return the first such match from the wordlist
- No match: If none of the above apply, return an empty string
""
Given an array of queries
, return an array where each element is the corrected word for the corresponding query according to the rules above.
Intuition
The key insight is that we need to handle three different matching scenarios with specific priorities. Since we need to check multiple queries efficiently, preprocessing the wordlist into different representations will allow us to perform fast lookups.
Think about what we need to check for each query:
- First, we need to check if the exact word exists (case-sensitive)
- Then, we need to check if a case-insensitive version exists
- Finally, we need to check if a vowel-variant exists
For efficient lookups, we can use hash tables. But how do we handle the different matching rules?
For exact matches, we can simply use a set to store all words from the wordlist and check membership in O(1) time.
For case-insensitive matches, we can normalize all words to lowercase and store them in a hash table. The key would be the lowercase version, and the value would be the original word from the wordlist. Since we need the "first such match", we use setdefault()
to only store the first occurrence.
For vowel error matches, we need a way to identify words that differ only in vowels. The clever approach is to create a pattern where all vowels are replaced with a wildcard character (like *
). For example, both "hello"
and "hallo"
would map to the pattern "h*ll*"
. This way, words that differ only in vowels will have the same pattern. We store these patterns in another hash table, again keeping only the first occurrence.
By preprocessing the wordlist into these three data structures (set
for exact matches, low
dictionary for case-insensitive matches, and pat
dictionary for vowel patterns), we can process each query by checking these structures in order of priority: exact match → case match → vowel pattern match → no match (empty string).
This approach transforms what could be an O(n) search through the wordlist for each query into O(1) lookups, making the overall solution very efficient.
Solution Approach
The implementation uses three hash tables to handle the different matching scenarios efficiently:
1. Setting up the data structures:
s = set(wordlist)
: A set to store all words for O(1) exact match lookupslow = {}
: A dictionary mapping lowercase words to their original forms in the wordlistpat = {}
: A dictionary mapping vowel patterns to their original forms in the wordlist
2. Helper function for vowel pattern generation:
The function f(w)
creates a pattern by replacing all vowels with *
:
def f(w):
t = []
for c in w:
t.append("*" if c in "aeiou" else c)
return "".join(t)
For example: f("hello")
returns "h*ll*"
and f("hallo")
also returns "h*ll*"
.
3. Preprocessing the wordlist: We iterate through the wordlist once to populate our hash tables:
for w in wordlist: t = w.lower() low.setdefault(t, w) # Store first occurrence for case-insensitive match pat.setdefault(f(t), w) # Store first occurrence for vowel pattern match
The setdefault()
method is crucial here - it only sets the value if the key doesn't exist, ensuring we keep the first occurrence as required by the problem.
4. Processing queries:
For each query q
, we check in order of priority:
for q in queries: if q in s: # Priority 1: Exact match ans.append(q) continue q = q.lower() if q in low: # Priority 2: Case-insensitive match ans.append(low[q]) continue q = f(q) # Convert to vowel pattern if q in pat: # Priority 3: Vowel error match ans.append(pat[q]) continue ans.append("") # Priority 4: No match
Time Complexity:
- Preprocessing: O(n × m) where n is the number of words in wordlist and m is the average word length
- Query processing: O(k × m) where k is the number of queries
- Overall: O((n + k) × m)
Space Complexity: O(n × m) for storing the three hash tables
The solution elegantly handles all matching rules by preprocessing the wordlist into different representations and checking them in the specified priority order, ensuring both correctness and efficiency.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand how the solution works.
Given:
wordlist = ["Yellow", "hello", "hEllo"]
queries = ["yollow", "HELLO", "yallow", "help"]
Step 1: Preprocessing the wordlist
We create three data structures:
-
Exact match set:
s = {"Yellow", "hello", "hEllo"}
-
Case-insensitive dictionary (
low
):- "Yellow" → lowercase is "yellow" →
low["yellow"] = "Yellow"
- "hello" → lowercase is "hello" →
low["hello"] = "hello"
- "hEllo" → lowercase is "hello" → already exists, skip (keep first occurrence)
- Result:
low = {"yellow": "Yellow", "hello": "hello"}
- "Yellow" → lowercase is "yellow" →
-
Vowel pattern dictionary (
pat
):- "Yellow" → lowercase "yellow" → pattern "yllw" →
pat["y*ll*w"] = "Yellow"
- "hello" → lowercase "hello" → pattern "hll" →
pat["h*ll*"] = "hello"
- "hEllo" → lowercase "hello" → pattern "hll" → already exists, skip
- Result:
pat = {"y*ll*w": "Yellow", "h*ll*": "hello"}
- "Yellow" → lowercase "yellow" → pattern "yllw" →
Step 2: Processing queries
Query 1: "yollow"
- Check exact match: "yollow" in
s
? No - Convert to lowercase: "yollow"
- Check case match: "yollow" in
low
? No - Convert to pattern: "yllw"
- Check pattern match: "yllw" in
pat
? Yes! → Return"Yellow"
Query 2: "HELLO"
- Check exact match: "HELLO" in
s
? No - Convert to lowercase: "hello"
- Check case match: "hello" in
low
? Yes! → Return"hello"
Query 3: "yallow"
- Check exact match: "yallow" in
s
? No - Convert to lowercase: "yallow"
- Check case match: "yallow" in
low
? No - Convert to pattern: "yllw"
- Check pattern match: "yllw" in
pat
? Yes! → Return"Yellow"
Query 4: "help"
- Check exact match: "help" in
s
? No - Convert to lowercase: "help"
- Check case match: "help" in
low
? No - Convert to pattern: "h*lp"
- Check pattern match: "h*lp" in
pat
? No → Return""
Final Result: ["Yellow", "hello", "Yellow", ""]
This example demonstrates how:
- The solution checks matches in priority order (exact → case → vowel)
- The preprocessing ensures we always return the first occurrence from wordlist
- Vowel patterns allow matching words with different vowels in the same positions
- Non-matching queries return empty strings
Solution Implementation
1class Solution:
2 def spellchecker(self, wordlist: List[str], queries: List[str]) -> List[str]:
3 def devowel(word: str) -> str:
4 """
5 Replace all vowels in a word with '*' to create a vowel-agnostic pattern.
6 """
7 pattern = []
8 for char in word:
9 if char in "aeiou":
10 pattern.append("*")
11 else:
12 pattern.append(char)
13 return "".join(pattern)
14
15 # Create a set for O(1) exact match lookups
16 word_set = set(wordlist)
17
18 # Dictionary to store first occurrence of each lowercase version
19 lowercase_dict = {}
20
21 # Dictionary to store first occurrence of each vowel pattern
22 vowel_pattern_dict = {}
23
24 # Build the dictionaries, keeping only the first occurrence of each pattern
25 for word in wordlist:
26 word_lower = word.lower()
27
28 # Store first occurrence of lowercase version
29 if word_lower not in lowercase_dict:
30 lowercase_dict[word_lower] = word
31
32 # Store first occurrence of vowel pattern
33 pattern = devowel(word_lower)
34 if pattern not in vowel_pattern_dict:
35 vowel_pattern_dict[pattern] = word
36
37 result = []
38
39 # Process each query
40 for query in queries:
41 # Priority 1: Exact match (case-sensitive)
42 if query in word_set:
43 result.append(query)
44 continue
45
46 # Priority 2: Case-insensitive match
47 query_lower = query.lower()
48 if query_lower in lowercase_dict:
49 result.append(lowercase_dict[query_lower])
50 continue
51
52 # Priority 3: Vowel error match (case-insensitive)
53 query_pattern = devowel(query_lower)
54 if query_pattern in vowel_pattern_dict:
55 result.append(vowel_pattern_dict[query_pattern])
56 continue
57
58 # No match found
59 result.append("")
60
61 return result
62
1class Solution {
2 /**
3 * Spellchecker that handles exact matches, case-insensitive matches, and vowel-error matches.
4 *
5 * @param wordlist Array of valid words
6 * @param queries Array of words to check
7 * @return Array of corrected words (or empty string if no match found)
8 */
9 public String[] spellchecker(String[] wordlist, String[] queries) {
10 // Set for exact match checking
11 Set<String> exactWords = new HashSet<>();
12
13 // Map for case-insensitive matches (lowercase -> first occurrence in wordlist)
14 Map<String, String> caseInsensitiveMap = new HashMap<>();
15
16 // Map for vowel-error matches (vowel pattern -> first occurrence in wordlist)
17 Map<String, String> vowelPatternMap = new HashMap<>();
18
19 // Build the data structures from wordlist
20 for (String word : wordlist) {
21 // Add to exact match set
22 exactWords.add(word);
23
24 // Add to case-insensitive map (only first occurrence)
25 String lowercaseWord = word.toLowerCase();
26 caseInsensitiveMap.putIfAbsent(lowercaseWord, word);
27
28 // Add to vowel pattern map (only first occurrence)
29 String vowelPattern = replaceVowelsWithWildcard(lowercaseWord);
30 vowelPatternMap.putIfAbsent(vowelPattern, word);
31 }
32
33 // Process each query
34 int queryCount = queries.length;
35 String[] results = new String[queryCount];
36
37 for (int i = 0; i < queryCount; i++) {
38 String query = queries[i];
39
40 // Priority 1: Check for exact match
41 if (exactWords.contains(query)) {
42 results[i] = query;
43 continue;
44 }
45
46 // Priority 2: Check for case-insensitive match
47 String lowercaseQuery = query.toLowerCase();
48 if (caseInsensitiveMap.containsKey(lowercaseQuery)) {
49 results[i] = caseInsensitiveMap.get(lowercaseQuery);
50 continue;
51 }
52
53 // Priority 3: Check for vowel-error match
54 String queryVowelPattern = replaceVowelsWithWildcard(lowercaseQuery);
55 if (vowelPatternMap.containsKey(queryVowelPattern)) {
56 results[i] = vowelPatternMap.get(queryVowelPattern);
57 continue;
58 }
59
60 // No match found
61 results[i] = "";
62 }
63
64 return results;
65 }
66
67 /**
68 * Replaces all vowels in the string with wildcard character '*'.
69 *
70 * @param word Input string (should be lowercase)
71 * @return String with vowels replaced by '*'
72 */
73 private String replaceVowelsWithWildcard(String word) {
74 char[] characters = word.toCharArray();
75
76 for (int i = 0; i < characters.length; i++) {
77 char currentChar = characters[i];
78 // Check if current character is a vowel
79 if (currentChar == 'a' || currentChar == 'e' || currentChar == 'i' ||
80 currentChar == 'o' || currentChar == 'u') {
81 characters[i] = '*';
82 }
83 }
84
85 return String.valueOf(characters);
86 }
87}
88
1class Solution {
2public:
3 vector<string> spellchecker(vector<string>& wordlist, vector<string>& queries) {
4 // Set for exact word matching (case-sensitive)
5 unordered_set<string> exactWords(wordlist.begin(), wordlist.end());
6
7 // Map for case-insensitive matching (stores first occurrence)
8 unordered_map<string, string> caseInsensitiveMap;
9
10 // Map for vowel error matching (stores first occurrence)
11 unordered_map<string, string> vowelPatternMap;
12
13 // Lambda function to replace vowels with '*' for pattern matching
14 auto replaceVowels = [](const string& word) {
15 string pattern;
16 for (char c : word) {
17 if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
18 pattern += '*';
19 } else {
20 pattern += c;
21 }
22 }
23 return pattern;
24 };
25
26 // Build the case-insensitive and vowel pattern maps from wordlist
27 for (const auto& word : wordlist) {
28 // Create lowercase version for case-insensitive matching
29 string lowercaseWord = word;
30 transform(lowercaseWord.begin(), lowercaseWord.end(),
31 lowercaseWord.begin(), ::tolower);
32
33 // Store first occurrence for case-insensitive match
34 if (!caseInsensitiveMap.count(lowercaseWord)) {
35 caseInsensitiveMap[lowercaseWord] = word;
36 }
37
38 // Create vowel pattern and store first occurrence
39 string vowelPattern = replaceVowels(lowercaseWord);
40 if (!vowelPatternMap.count(vowelPattern)) {
41 vowelPatternMap[vowelPattern] = word;
42 }
43 }
44
45 // Process queries and build result
46 vector<string> result;
47 for (auto& query : queries) {
48 // Priority 1: Check for exact match
49 if (exactWords.count(query)) {
50 result.emplace_back(query);
51 continue;
52 }
53
54 // Convert query to lowercase for further matching
55 string lowercaseQuery = query;
56 transform(lowercaseQuery.begin(), lowercaseQuery.end(),
57 lowercaseQuery.begin(), ::tolower);
58
59 // Priority 2: Check for case-insensitive match
60 if (caseInsensitiveMap.count(lowercaseQuery)) {
61 result.emplace_back(caseInsensitiveMap[lowercaseQuery]);
62 continue;
63 }
64
65 // Priority 3: Check for vowel error match
66 string queryPattern = replaceVowels(lowercaseQuery);
67 if (vowelPatternMap.count(queryPattern)) {
68 result.emplace_back(vowelPatternMap[queryPattern]);
69 continue;
70 }
71
72 // No match found
73 result.emplace_back("");
74 }
75
76 return result;
77 }
78};
79
1function spellchecker(wordlist: string[], queries: string[]): string[] {
2 // Set for exact word matching (case-sensitive)
3 const exactWords = new Set<string>(wordlist);
4
5 // Map for case-insensitive matching (stores first occurrence)
6 const caseInsensitiveMap = new Map<string, string>();
7
8 // Map for vowel error matching (stores first occurrence)
9 const vowelPatternMap = new Map<string, string>();
10
11 // Helper function to replace vowels with '*' for pattern matching
12 const replaceVowels = (word: string): string => {
13 let pattern = '';
14 for (const char of word) {
15 if ('aeiou'.includes(char)) {
16 pattern += '*';
17 } else {
18 pattern += char;
19 }
20 }
21 return pattern;
22 };
23
24 // Build the case-insensitive and vowel pattern maps from wordlist
25 for (const word of wordlist) {
26 // Create lowercase version for case-insensitive matching
27 const lowercaseWord = word.toLowerCase();
28
29 // Store first occurrence for case-insensitive match
30 if (!caseInsensitiveMap.has(lowercaseWord)) {
31 caseInsensitiveMap.set(lowercaseWord, word);
32 }
33
34 // Create vowel pattern and store first occurrence
35 const vowelPattern = replaceVowels(lowercaseWord);
36 if (!vowelPatternMap.has(vowelPattern)) {
37 vowelPatternMap.set(vowelPattern, word);
38 }
39 }
40
41 // Process queries and build result
42 const result: string[] = [];
43
44 for (const query of queries) {
45 // Priority 1: Check for exact match
46 if (exactWords.has(query)) {
47 result.push(query);
48 continue;
49 }
50
51 // Convert query to lowercase for further matching
52 const lowercaseQuery = query.toLowerCase();
53
54 // Priority 2: Check for case-insensitive match
55 if (caseInsensitiveMap.has(lowercaseQuery)) {
56 result.push(caseInsensitiveMap.get(lowercaseQuery)!);
57 continue;
58 }
59
60 // Priority 3: Check for vowel error match
61 const queryPattern = replaceVowels(lowercaseQuery);
62 if (vowelPatternMap.has(queryPattern)) {
63 result.push(vowelPatternMap.get(queryPattern)!);
64 continue;
65 }
66
67 // No match found
68 result.push("");
69 }
70
71 return result;
72}
73
Time and Space Complexity
Time Complexity: O(n × L + m × L)
where n
is the length of wordlist
, m
is the length of queries
, and L
is the average length of words.
- Building the set
s
fromwordlist
:O(n × L)
(hashing each word) - Processing each word in
wordlist
to buildlow
andpat
dictionaries:O(n × L)
- Converting to lowercase:
O(L)
per word - Creating pattern with
f()
function:O(L)
per word
- Converting to lowercase:
- Processing each query:
O(m × L)
- Set lookup:
O(L)
for hashing - Converting to lowercase:
O(L)
- Creating pattern with
f()
:O(L)
- Dictionary lookups:
O(L)
for hashing
- Set lookup:
Since L
is typically considered constant or small compared to n
and m
, this simplifies to O(n + m)
.
Space Complexity: O(n × L)
which simplifies to O(n)
when L
is considered constant.
- Set
s
stores all words fromwordlist
:O(n × L)
- Dictionary
low
stores at mostn
lowercase mappings:O(n × L)
- Dictionary
pat
stores at mostn
pattern mappings:O(n × L)
- List
ans
storesm
results:O(m × L)
The dominant space usage comes from storing the wordlist-related data structures, giving us O(n)
when word length is considered constant.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Preserving the First Occurrence for Duplicate Patterns
One of the most common mistakes is overwriting dictionary values when multiple words in the wordlist map to the same lowercase form or vowel pattern. The problem specifically requires returning the first matching word from the wordlist.
Incorrect Implementation:
# WRONG: This overwrites previous values for word in wordlist: word_lower = word.lower() lowercase_dict[word_lower] = word # Overwrites if key exists! pattern = devowel(word_lower) vowel_pattern_dict[pattern] = word # Overwrites if key exists!
Example where this fails:
wordlist = ["Yellow", "yellow"]
,query = "yeLLow"
- Incorrect code would return
"yellow"
(last occurrence) - Correct answer should be
"Yellow"
(first occurrence)
Solution:
Always check if the key exists before setting it, or use setdefault()
:
# Method 1: Check before setting if word_lower not in lowercase_dict: lowercase_dict[word_lower] = word # Method 2: Use setdefault lowercase_dict.setdefault(word_lower, word)
2. Case Sensitivity Issues in Vowel Detection
Another common mistake is forgetting to handle uppercase vowels when creating the vowel pattern.
Incorrect Implementation:
def devowel(word: str) -> str:
pattern = []
for char in word:
if char in "aeiou": # WRONG: Only checks lowercase vowels!
pattern.append("*")
else:
pattern.append(char)
return "".join(pattern)
Example where this fails:
wordlist = ["KiTe"]
,query = "kati"
- The pattern for "KiTe" would incorrectly be "KT" instead of "kt"
- This would cause a mismatch even though it should match
Solution: Always convert to lowercase before processing the vowel pattern:
# Process the lowercase version for pattern matching word_lower = word.lower() pattern = devowel(word_lower) # Pass lowercase version
3. Incorrect Priority Order
Some implementations check all conditions and then decide, rather than following the strict priority order with early returns.
Incorrect Implementation:
# WRONG: Checks all conditions then decides for query in queries: exact_match = query if query in word_set else None case_match = lowercase_dict.get(query.lower(), None) vowel_match = vowel_pattern_dict.get(devowel(query.lower()), None) # This doesn't guarantee priority order! result.append(exact_match or case_match or vowel_match or "")
Solution:
Use continue
statements to ensure each priority level is handled before moving to the next:
for query in queries: if query in word_set: result.append(query) continue # Stop here if exact match found query_lower = query.lower() if query_lower in lowercase_dict: result.append(lowercase_dict[query_lower]) continue # Stop here if case-insensitive match found # Only check vowel pattern if no higher priority match query_pattern = devowel(query_lower) if query_pattern in vowel_pattern_dict: result.append(vowel_pattern_dict[query_pattern]) continue result.append("")
4. Modifying the Original Query Variable
A subtle bug can occur when reusing the query variable name:
Incorrect Implementation:
for query in queries: if query in word_set: result.append(query) continue query = query.lower() # DANGER: Overwrites original query! if query in lowercase_dict: result.append(lowercase_dict[query]) continue query = devowel(query) # Now query is a pattern, not the original! # ...
Solution: Use different variable names to preserve the original query:
for query in queries: if query in word_set: result.append(query) continue query_lower = query.lower() # Use different variable name if query_lower in lowercase_dict: result.append(lowercase_dict[query_lower]) continue query_pattern = devowel(query_lower) # Use different variable name # ...
In a binary min heap, the minimum element can be found in:
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!