Facebook Pixel

1554. Strings Differ by One Character 🔒

MediumHash TableStringHash FunctionRolling Hash
Leetcode Link

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 return true
  • If dict = ["abc", "def", "ghi"], no two strings differ by exactly one character, so return false

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:

  1. Iterating through each word in the dictionary
  2. For each position in the word, creating a pattern with that position replaced by "*"
  3. Checking if this pattern has been seen before (stored in set s)
  4. If the pattern exists, it means we found two words that match everywhere except at the wildcard position - they differ by exactly one character
  5. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Initialize a set: Create an empty set s to store all the patterns we've encountered.

  2. Iterate through each word: For each word in the dictionary, we'll generate all possible patterns.

  3. 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
  4. Check for collision: Before adding the pattern to our set, check if it already exists:

    • If t in s returns True, 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
  5. Store the pattern: If no collision is found, add the pattern to the set with s.add(t) for future comparisons.

  6. 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! Return True

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 Evaluator

Example 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] and word[i + 1:] each take O(m) time in the worst case.
  • String concatenation to form the pattern t also takes O(m) time.
  • The hash set operations (in check and add) take O(m) time on average since we're hashing strings of length m.
  • 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 to n * m different patterns (each word can generate m 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 is O(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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More