1554. Strings Differ by One Character 🔒
Problem Description
You are given a list of strings dict
where all strings have the same length. Your task is to determine if there exist any two strings in the list that differ by exactly one character at the same position.
For example:
- If
dict = ["abc", "abd", "xyz"]
, the strings "abc" and "abd" differ only at index 2 ('c' vs 'd'), so returntrue
- If
dict = ["abc", "def", "ghi"]
, no two strings differ by exactly one character, so returnfalse
The solution uses a pattern-matching approach. For each word, it generates all possible patterns by replacing each character position with a wildcard "*"
. For instance, the word "abc" would generate patterns: "*bc"
, "a*c"
, and "ab*"
.
The algorithm works by:
- Iterating through each word in the dictionary
- For each position in the word, creating a pattern with that position replaced by
"*"
- Checking if this pattern has been seen before (stored in set
s
) - If the pattern exists, it means we found two words that match everywhere except at the wildcard position - they differ by exactly one character
- If not found, adding the pattern to the set for future comparisons
The time complexity is O(n * m)
where n
is the number of words and m
is the length of each word, since we process each character of each word once.
Intuition
The key insight is that if two strings differ by exactly one character, they must be identical everywhere else. This means they share a common "pattern" or "signature" when we mask out the differing position.
Think about it this way: if we have two strings like "abc" and "adc", they differ only at position 1. If we replace position 1 with a wildcard in both strings, we get the same pattern: "a*c"
. This observation leads us to the solution strategy.
Instead of comparing every pair of strings directly (which would take O(n²)
comparisons), we can be clever. For each string, we generate all possible patterns by masking one position at a time. If two strings differ by exactly one character, they will inevitably produce the same pattern when we mask their differing position.
For example:
- "abc" generates:
"*bc"
,"a*c"
,"ab*"
- "adc" generates:
"*dc"
,"a*c"
,"ad*"
Notice that both strings generate "a*c"
- this is where they collide! This collision tells us these two strings differ by exactly one character.
By using a set to track all patterns we've seen so far, we can detect such collisions in constant time. As soon as we find a pattern that already exists in our set, we know we've found two strings that differ by exactly one character. This reduces our time complexity from O(n² * m)
for brute force to O(n * m)
with this pattern-matching approach.
Solution Approach
The implementation uses a hash set to efficiently track and detect pattern collisions:
-
Initialize a set: Create an empty set
s
to store all the patterns we've encountered. -
Iterate through each word: For each word in the dictionary, we'll generate all possible patterns.
-
Generate patterns for each word: For each position
i
in the current word:- Create a pattern by replacing the character at position
i
with a wildcard"*"
- The pattern is constructed as:
word[:i] + "*" + word[i + 1:]
- This creates a string that's identical to the original except for the wildcard at position
i
- Create a pattern by replacing the character at position
-
Check for collision: Before adding the pattern to our set, check if it already exists:
- If
t in s
returnsTrue
, we've found a collision - This means another word previously generated the same pattern
- These two words must differ by exactly one character (at the wildcard position)
- Return
True
immediately
- If
-
Store the pattern: If no collision is found, add the pattern to the set with
s.add(t)
for future comparisons. -
Return result: If we process all words and their patterns without finding any collision, return
False
.
Example walkthrough with dict = ["abc", "abd", "xyz"]
:
- Process "abc": Generate
"*bc"
,"a*c"
,"ab*"
→ Add all to set - Process "abd": Generate
"*bd"
,"a*d"
,"ab*"
→"ab*"
already exists! ReturnTrue
The algorithm efficiently finds the answer in a single pass through the dictionary, checking m
patterns per word where m
is the word length.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with dict = ["cat", "bat", "rat", "hat"]
:
Step 1: Process "cat"
- Generate pattern with position 0 masked:
"*at"
- Check if
"*at"
is in set → No - Add
"*at"
to set:s = {"*at"}
- Generate pattern with position 1 masked:
"c*t"
- Check if
"c*t"
is in set → No - Add
"c*t"
to set:s = {"*at", "c*t"}
- Generate pattern with position 2 masked:
"ca*"
- Check if
"ca*"
is in set → No - Add
"ca*"
to set:s = {"*at", "c*t", "ca*"}
Step 2: Process "bat"
- Generate pattern with position 0 masked:
"*at"
- Check if
"*at"
is in set → Yes! Found collision! - This means "bat" and "cat" differ only at position 0 ('b' vs 'c')
- Return True
The algorithm successfully identifies that "cat" and "bat" differ by exactly one character without needing to check the remaining words. The collision occurred when both words generated the same pattern "*at"
, indicating they're identical except at the first position.
If we had continued with a non-matching example like dict = ["abc", "def", "xyz"]
:
- "abc" would generate:
{"*bc", "a*c", "ab*"}
- "def" would generate:
{"*ef", "d*f", "de*"}
(no collisions with previous patterns) - "xyz" would generate:
{"*yz", "x*z", "xy*"}
(no collisions with previous patterns) - After processing all words with no collisions found → Return False
Solution Implementation
1class Solution:
2 def differByOne(self, dict: List[str]) -> bool:
3 """
4 Check if there exist two strings in the dictionary that differ by exactly one character.
5
6 Args:
7 dict: List of strings to check
8
9 Returns:
10 True if two strings differ by exactly one character, False otherwise
11 """
12 # Set to store patterns with one character replaced by wildcard
13 seen_patterns = set()
14
15 # Iterate through each word in the dictionary
16 for word in dict:
17 # Try replacing each character position with a wildcard
18 for i in range(len(word)):
19 # Create pattern by replacing character at position i with '*'
20 pattern = word[:i] + "*" + word[i + 1:]
21
22 # If this pattern was seen before, we found two words differing by one character
23 if pattern in seen_patterns:
24 return True
25
26 # Add the pattern to the set for future comparisons
27 seen_patterns.add(pattern)
28
29 # No two words differ by exactly one character
30 return False
31
1class Solution {
2 /**
3 * Determines if any two strings in the dictionary differ by exactly one character.
4 *
5 * @param dict Array of strings to check
6 * @return true if any two strings differ by exactly one character, false otherwise
7 */
8 public boolean differByOne(String[] dict) {
9 // Set to store patterns with one character replaced by wildcard
10 Set<String> patternSet = new HashSet<>();
11
12 // Iterate through each word in the dictionary
13 for (String word : dict) {
14 // For each position in the current word
15 for (int i = 0; i < word.length(); ++i) {
16 // Create a pattern by replacing character at position i with '*'
17 // This represents all possible words that differ from current word at position i
18 String pattern = word.substring(0, i) + "*" + word.substring(i + 1);
19
20 // If this pattern already exists, it means we found another word
21 // that differs from a previous word by exactly one character
22 if (patternSet.contains(pattern)) {
23 return true;
24 }
25
26 // Add the current pattern to the set for future comparisons
27 patternSet.add(pattern);
28 }
29 }
30
31 // No two words differ by exactly one character
32 return false;
33 }
34}
35
1class Solution {
2public:
3 bool differByOne(vector<string>& dict) {
4 // Set to store patterns with one character replaced by wildcard
5 unordered_set<string> patternSet;
6
7 // Iterate through each word in the dictionary
8 for (const auto& word : dict) {
9 // Try replacing each character position with a wildcard
10 for (int i = 0; i < word.size(); ++i) {
11 // Create a pattern by replacing character at position i with '*'
12 string pattern = word;
13 pattern[i] = '*';
14
15 // If this pattern already exists, we found two words that differ by exactly one character
16 if (patternSet.count(pattern)) {
17 return true;
18 }
19
20 // Add the current pattern to the set for future comparisons
21 patternSet.insert(pattern);
22 }
23 }
24
25 // No two words differ by exactly one character
26 return false;
27 }
28};
29
1function differByOne(dict: string[]): boolean {
2 // Set to store patterns with one character replaced by wildcard
3 const patternSet: Set<string> = new Set<string>();
4
5 // Iterate through each word in the dictionary
6 for (const word of dict) {
7 // Try replacing each character position with a wildcard
8 for (let i = 0; i < word.length; i++) {
9 // Create a pattern by replacing character at position i with '*'
10 const pattern: string = word.substring(0, i) + '*' + word.substring(i + 1);
11
12 // If this pattern already exists, we found two words that differ by exactly one character
13 if (patternSet.has(pattern)) {
14 return true;
15 }
16
17 // Add the current pattern to the set for future comparisons
18 patternSet.add(pattern);
19 }
20 }
21
22 // No two words differ by exactly one character
23 return false;
24}
25
Time and Space Complexity
Time Complexity: O(n * m²)
where n
is the number of words in the dictionary and m
is the length of each word.
- The outer loop iterates through all
n
words in the dictionary. - For each word, the inner loop runs
m
times (once for each character position). - Inside the inner loop, string slicing operations
word[:i]
andword[i + 1:]
each takeO(m)
time in the worst case. - String concatenation to form the pattern
t
also takesO(m)
time. - The hash set operations (
in
check andadd
) takeO(m)
time on average since we're hashing strings of lengthm
. - Therefore, the total time complexity is
O(n * m * m) = O(n * m²)
.
Space Complexity: O(n * m²)
in the worst case.
- The set
s
can store up ton * m
different patterns (each word can generatem
patterns with wildcards). - Each pattern string has length
m
(same as the original words). - Therefore, the total space used by the set is
O(n * m * m) = O(n * m²)
. - Additional space for temporary string
t
isO(m)
, which doesn't affect the overall complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Hash Collision with Large Datasets
When dealing with very large strings or massive dictionaries, storing string patterns directly in a set can lead to hash collisions or memory issues. The string concatenation approach (word[:i] + "*" + word[i + 1:]
) creates new string objects for every pattern, which can be memory-intensive.
Solution: Use tuple representation or rolling hash for more efficient storage:
def differByOne(self, dict: List[str]) -> bool:
seen_patterns = set()
for word in dict:
for i in range(len(word)):
# Use tuple instead of string concatenation
pattern = (word[:i], word[i + 1:])
if pattern in seen_patterns:
return True
seen_patterns.add(pattern)
return False
Pitfall 2: Not Handling Edge Cases
The code assumes all strings have the same length but doesn't validate this assumption. If strings have different lengths, the algorithm might produce incorrect results or miss valid matches.
Solution: Add validation or handle variable-length strings:
def differByOne(self, dict: List[str]) -> bool:
if not dict or len(dict) < 2:
return False
# Group words by length first
length_groups = {}
for word in dict:
length = len(word)
if length not in length_groups:
length_groups[length] = []
length_groups[length].append(word)
# Check within each length group
for words in length_groups.values():
if len(words) < 2:
continue
seen_patterns = set()
for word in words:
for i in range(len(word)):
pattern = word[:i] + "*" + word[i + 1:]
if pattern in seen_patterns:
return True
seen_patterns.add(pattern)
return False
Pitfall 3: Character Encoding Issues
When working with Unicode strings or special characters, the slicing approach might not handle multi-byte characters correctly, potentially causing incorrect pattern matching.
Solution: Convert to character list for safer manipulation:
def differByOne(self, dict: List[str]) -> bool:
seen_patterns = set()
for word in dict:
chars = list(word) # Convert to list for safer manipulation
for i in range(len(chars)):
original = chars[i]
chars[i] = '*'
pattern = ''.join(chars)
if pattern in seen_patterns:
return True
seen_patterns.add(pattern)
chars[i] = original # Restore original character
return False
You are given an array of intervals where intervals[i] = [start_i, end_i]
represent the start and end of the ith
interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
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!