Words Within Two Edits of Dictionary
MedPrompt
You are given two string arrays, queries and dictionary. All words in each array comprise of lowercase English letters and have the same length.
In one edit you can take a word from queries, and change any letter in it to any other letter. Find all words from queries that, after a maximum of two edits, equal some word from dictionary.
Return a list of all words from queries, that match with some word from dictionary after a maximum of two edits. Return the words in the same order they appear in queries.
Example 1:
Input: queries = ["word","note","ants","wood"], dictionary = ["wood","joke","moat"]
Output: ["word","note","wood"]
Explanation:
- Changing the 'r' in "word" to 'o' allows it to equal the dictionary word "wood".
- Changing the 'n' to 'j' and the 't' to 'k' in "note" changes it to "joke".
- It would take more than 2 edits for "ants" to equal a dictionary word.
- "wood" can remain unchanged (0 edits) and match the corresponding dictionary word.
Thus, we return ["word","note","wood"].Example 2:
Input: queries = ["yes"], dictionary = ["not"]
Output: []
Explanation:
Applying any two edits to "yes" cannot make it equal to "not". Thus, we return an empty array.
Constraints:
1 <= queries.length, dictionary.length <= 100n == queries[i].length == dictionary[j].length1 <= n <= 100- All
queries[i]anddictionary[j]are composed of lowercase English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a straightforward, nested loop structure to solve the problem. We iterate through each word in the queries array. For each query word, we then iterate through every single word in the dictionary array. In the innermost loop, we compare the query word and the dictionary word character by character to count the number of differences. If this count is two or less, we've found a potential match. To ensure we return the results in the original order and without duplicates, we use a HashSet to store all valid queries and then filter the original queries list based on the content of the set.
Algorithm
- Initialize a
HashSet<String>to store the queries that have a match in the dictionary. - Iterate through each
querystring in thequeriesarray. - For each
query, iterate through eachdictWordstring in thedictionaryarray. - Inside the inner loop, compare the
queryanddictWordcharacter by character to count the number of differing characters (the Hamming distance). - If the difference count is less than or equal to 2, add the current
querystring to theHashSet. Note that this version continues checking the query against other dictionary words even after a match is found. - After the loops complete, initialize a new empty
List<String>to store the final result. - Iterate through the original
queriesarray again. For eachquery, check if it is present in theHashSet. - If it is, add it to the final result list. This step is necessary to return the words in their original order.
- Return the final result list.
Walkthrough
The core idea is to exhaustively check every query against every dictionary word. To manage the results, we first identify all query words that have at least one valid match and store them in a temporary data structure, like a HashSet, which automatically handles duplicates.
Here's the breakdown of the algorithm:
- Initialize a
HashSet<String>calledmatchingQueries. - For each
queryin thequeriesarray: - For each
dictWordin thedictionaryarray: - Compare
queryanddictWordcharacter by character and count the differences. - If the difference count is
_ <= 2, add thequeryto thematchingQueriesset. - After checking all pairs, we construct the final list in the original order.
- Initialize an empty
List<String>calledresult. - Iterate through the original
queriesarray one more time. - For each
query, if it exists in ourmatchingQueriesset, add it to theresultlist. - Return the
resultlist.
This method is correct but slightly inefficient because it continues to check a query against the rest of the dictionary even after a match has been found, and it requires extra space and an extra pass to restore the order.
import java.util.ArrayList;import java.util.HashSet;import java.util.List;import java.util.Set; class Solution { public List<String> twoEditWords(String[] queries, String[] dictionary) { Set<String> matchingQueries = new HashSet<>(); for (String query : queries) { for (String dictWord : dictionary) { int diff = 0; for (int i = 0; i < query.length(); i++) { if (query.charAt(i) != dictWord.charAt(i)) { diff++; } } if (diff <= 2) { matchingQueries.add(query); // This version doesn't break, so it might do extra work. } } } List<String> result = new ArrayList<>(); for (String query : queries) { if (matchingQueries.contains(query)) { result.add(query); } } return result; }}Complexity
Time
O(Q * D * L), where `Q` is the number of queries, `D` is the number of dictionary words, and `L` is the length of the words. The nested loops dominate the runtime. There is an additional `O(Q * L)` cost for the final pass, but it's absorbed by the main term.
Space
O(Q * L), where `Q` is the number of queries and `L` is the length of each word. This is for the `HashSet` which, in the worst case, could store all the query words.
Trade-offs
Pros
The logic is simple and easy to understand.
It correctly solves the problem by ensuring all possible pairs are checked.
Cons
It performs unnecessary comparisons by not stopping after finding the first match for a query.
It requires extra space for the
HashSetto store matching queries.It needs a final pass over the
queriesarray to reconstruct the output in the correct order, adding a bit of overhead.
This approach improves upon the naive brute-force method by adding a simple but effective optimization. The core logic remains the same: we iterate through each query and compare it against dictionary words. However, as soon as we find a dictionary word that is within two edits of the current query word, we know this query is valid. There is no need to check it against the rest of the dictionary. We can immediately add the query to our result list and move on to the next one.
Algorithm
- Initialize an empty
List<String>to store the result. - Iterate through each
querystring in thequeriesarray. - For each
query, start an inner loop to iterate through eachdictWordin thedictionaryarray. - Create a helper function,
isMatch(query, dictWord), that compares the two strings. - Inside the helper function, count the character differences. For a small optimization, if the difference count ever exceeds 2, the function can immediately return
false. - If the helper function returns
true(meaning the difference is 0, 1, or 2), add the currentqueryto the result list. - Crucially, after adding the query,
breakthe inner loop to stop checking this query against the rest of the dictionary words. - Proceed to the next
queryin the outer loop. - After iterating through all queries, return the result list.
Walkthrough
By adding an early exit, we can significantly improve the average-case performance and reduce space usage. The moment a match is found for a given query, we add it to our results and break from the inner loop that iterates over the dictionary. This avoids many unnecessary comparisons.
The algorithm is as follows:
- Initialize an empty
List<String>calledresult. - For each
queryin thequeriesarray: - For each
dictWordin thedictionaryarray: - Check if the number of differing characters between
queryanddictWordis at most 2. - If it is, add the
queryto theresultlist. - Then,
breakfrom the inner loop to proceed to the next query. - Return the
resultlist.
This approach is more efficient in terms of both time (on average) and space. It naturally preserves the order of the queries because we add them to the result list as we encounter them in the original queries array, so no extra data structures or passes are needed.
import java.util.ArrayList;import java.util.List; class Solution { public List<String> twoEditWords(String[] queries, String[] dictionary) { List<String> result = new ArrayList<>(); for (String query : queries) { for (String dictWord : dictionary) { if (isMatch(query, dictWord)) { result.add(query); break; // Optimization: Found a match, move to the next query. } } } return result; } private boolean isMatch(String s1, String s2) { int diff = 0; for (int i = 0; i < s1.length(); i++) { if (s1.charAt(i) != s2.charAt(i)) { diff++; } // Further optimization: if diff is already > 2, no need to check further. if (diff > 2) { return false; } } return true; // If loop finishes, diff must be <= 2. }}Complexity
Time
O(Q * D * L) in the worst case. `Q` is the number of queries, `D` is the number of dictionary words, and `L` is the word length. However, the average-case performance is much better because of the early exit from the inner loop.
Space
O(1), excluding the space required for the output list. This is an improvement over the previous approach that required a `HashSet`.
Trade-offs
Pros
More efficient on average than the naive brute-force approach due to the early exit.
Requires no extra space (besides the output list), making it more memory-efficient.
Preserves the original order of queries naturally, simplifying the logic.
Cons
The worst-case time complexity remains
O(Q * D * L), which occurs if no matches are found or if matches are always the last words in the dictionary.
Solutions
Solution
public class Solution { public IList < string > TwoEditWords(string[] queries, string[] dictionary) { var ans = new List < string > (); foreach(var s in queries) { foreach(var t in dictionary) { int cnt = 0; for (int i = 0; i < s.Length; i++) { if (s[i] != t[i]) { cnt++; } } if (cnt < 3) { ans.Add(s); break; } } } return ans; }}class Solution {public List<String> twoEditWords(String[] queries, String[] dictionary) { List<String> ans = new ArrayList<>(); int n = queries[0].length(); for (var s : queries) { for (var t : dictionary) { int cnt = 0; for (int i = 0; i < n; ++i) { if (s.charAt(i) != t.charAt(i)) { ++cnt; } } if (cnt < 3) { ans.add(s); break; } } } return ans; }}class Solution {public: vector<string> twoEditWords(vector<string> &queries, vector<string> &dictionary) { vector<string> ans; for (auto &s : queries) { for (auto &t : dictionary) { int cnt = 0; for (int i = 0; i < s.size(); ++i) cnt += s[i] != t[i]; if (cnt < 3) { ans.emplace_back(s); break; } } } return ans; }};class Solution: def twoEditWords(self, queries: List[str], dictionary: List[str]) -> List[str]: ans = [] for s in queries: for t in dictionary: if sum(a != b for a, b in zip(s, t)) < 3: ans . append(s) break return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.