Facebook Pixel

2451. Odd String Difference

EasyArrayHash TableString
Leetcode Link

Problem Description

You have an array of strings where all strings have the same length n. Your task is to find the one string that is different from all the others based on a specific pattern.

For each string, you need to calculate a difference integer array which captures the pattern of differences between consecutive characters. The difference array has length n-1 and is calculated as follows:

  • For each position j from 0 to n-2, compute difference[j] = words[i][j+1] - words[i][j]
  • The subtraction uses the alphabetical positions of characters (where 'a' = 0, 'b' = 1, ..., 'z' = 25)

For example, the string "acb" would produce:

  • 'c' - 'a' = 2 - 0 = 2
  • 'b' - 'c' = 1 - 2 = -1
  • Resulting in difference array [2, -1]

The key insight is that all strings in the input array will produce the same difference array, except for exactly one string which will have a different pattern. Your goal is to identify and return that unique string.

The solution uses a hash table to group strings by their difference arrays (stored as tuples). Since all strings except one share the same difference pattern, there will be one group with only a single string - that's the odd one out that needs to be returned.

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

Intuition

The key observation is that we're looking for an outlier - one string that doesn't follow the same pattern as all the others. When dealing with finding outliers in a collection, a natural approach is to group similar items together and identify which group is different.

Since we need to compare patterns (difference arrays) across all strings, we can think of this as a grouping problem. If we calculate the difference array for each string and use it as a "signature" or "fingerprint" for that string, then strings with the same pattern will have identical signatures.

The insight that leads to the hash table solution comes from recognizing that:

  1. We need to group strings by their difference arrays
  2. All strings except one will fall into the same group
  3. The odd string will be alone in its own group

By using a hash table where the key is the difference array (converted to a tuple for hashability) and the value is a list of strings with that pattern, we can efficiently organize all strings. After processing all strings, we'll have exactly two groups:

  • One large group containing all but one string (the majority pattern)
  • One group with just a single string (the outlier)

The string we're looking for is simply the one that ends up alone in its group. This approach elegantly handles the problem without needing to explicitly determine which pattern is the "correct" one - we just find the string that doesn't have any companions sharing its pattern.

Solution Approach

The implementation uses a hash table to group strings by their difference patterns. Here's the step-by-step breakdown:

  1. Initialize a Hash Table: Create a defaultdict(list) where each key will be a difference array (stored as a tuple) and the value will be a list of strings sharing that pattern.

  2. Process Each String: For each string s in the input array:

    • Calculate the difference array using pairwise(s) which gives consecutive character pairs (a, b)
    • For each pair, compute ord(b) - ord(a) to get the difference between character positions
    • Convert the resulting differences into a tuple t (since lists aren't hashable but tuples are)
    • Append the original string s to the list at d[t]
  3. Find the Outlier: After processing all strings, the hash table d will have exactly two entries:

    • One entry with multiple strings (the common pattern)
    • One entry with just a single string (the outlier)

    Use next(ss[0] for ss in d.values() if len(ss) == 1) to find and return the first (and only) string from the group that has exactly one element.

The elegance of this approach lies in its simplicity - we don't need to determine which pattern is "correct" or compare patterns directly. By grouping strings and looking for the singleton group, we automatically identify the odd string out.

Time complexity: O(m * n) where m is the number of strings and n is the length of each string. Space complexity: O(m * n) for storing the hash table entries.

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 a concrete example with the input: ["abc", "bcd", "xyz", "cde"]

Step 1: Initialize the hash table We create an empty defaultdict(list) to group strings by their difference patterns.

Step 2: Process each string

For "abc":

  • Calculate differences: 'b' - 'a' = 1, 'c' - 'b' = 1
  • Difference array: [1, 1]
  • Convert to tuple: (1, 1)
  • Add to hash table: d[(1, 1)] = ["abc"]

For "bcd":

  • Calculate differences: 'c' - 'b' = 1, 'd' - 'c' = 1
  • Difference array: [1, 1]
  • Convert to tuple: (1, 1)
  • Add to hash table: d[(1, 1)] = ["abc", "bcd"]

For "xyz":

  • Calculate differences: 'y' - 'x' = 1, 'z' - 'y' = 1
  • Difference array: [1, 1]
  • Convert to tuple: (1, 1)
  • Add to hash table: d[(1, 1)] = ["abc", "bcd", "xyz"]

For "cde":

  • Calculate differences: 'd' - 'c' = 1, 'e' - 'd' = 1
  • Difference array: [1, 1]
  • Convert to tuple: (1, 1)
  • Add to hash table: d[(1, 1)] = ["abc", "bcd", "xyz", "cde"]

Wait, this example has all strings with the same pattern! Let me use a better example: ["abc", "bcd", "ace", "xyz"]

Starting over with a better example:

For "abc":

  • 'b' - 'a' = 1, 'c' - 'b' = 1
  • Tuple: (1, 1)
  • d[(1, 1)] = ["abc"]

For "bcd":

  • 'c' - 'b' = 1, 'd' - 'c' = 1
  • Tuple: (1, 1)
  • d[(1, 1)] = ["abc", "bcd"]

For "ace":

  • 'c' - 'a' = 2, 'e' - 'c' = 2
  • Tuple: (2, 2)
  • d[(2, 2)] = ["ace"]

For "xyz":

  • 'y' - 'x' = 1, 'z' - 'y' = 1
  • Tuple: (1, 1)
  • d[(1, 1)] = ["abc", "bcd", "xyz"]

Step 3: Find the outlier Our hash table now contains:

  • (1, 1): ["abc", "bcd", "xyz"] (3 strings)
  • (2, 2): ["ace"] (1 string)

The group with only one string is ["ace"], so we return "ace" as the odd string out.

Solution Implementation

1from collections import defaultdict
2from itertools import pairwise
3from typing import List
4
5class Solution:
6    def oddString(self, words: List[str]) -> str:
7        # Dictionary to group words by their difference patterns
8        difference_groups = defaultdict(list)
9      
10        # Process each word to calculate its difference pattern
11        for word in words:
12            # Calculate differences between consecutive characters
13            # For example: "abc" -> (1, 1) since b-a=1, c-b=1
14            differences = tuple(ord(char2) - ord(char1) 
15                              for char1, char2 in pairwise(word))
16          
17            # Group words with the same difference pattern
18            difference_groups[differences].append(word)
19      
20        # Find and return the word that has a unique difference pattern
21        # (appears only once in its group)
22        for word_group in difference_groups.values():
23            if len(word_group) == 1:
24                return word_group[0]
25
1class Solution {
2    public String oddString(String[] words) {
3        // HashMap to store difference patterns as keys and corresponding words as values
4        HashMap<String, List<String>> differenceMap = new HashMap<>();
5      
6        // Process each word to calculate its difference pattern
7        for (String word : words) {
8            int wordLength = word.length();
9          
10            // Array to store character differences between consecutive characters
11            char[] differences = new char[wordLength - 1];
12          
13            // Calculate differences between consecutive characters
14            for (int i = 0; i < wordLength - 1; i++) {
15                differences[i] = (char) (word.charAt(i + 1) - word.charAt(i));
16            }
17          
18            // Convert difference array to string for use as map key
19            String differencePattern = String.valueOf(differences);
20          
21            // Add word to the list corresponding to its difference pattern
22            differenceMap.putIfAbsent(differencePattern, new ArrayList<>());
23            differenceMap.get(differencePattern).add(word);
24        }
25      
26        // Find and return the word with unique difference pattern
27        for (List<String> wordList : differenceMap.values()) {
28            // The odd string will be alone in its list
29            if (wordList.size() == 1) {
30                return wordList.get(0);
31            }
32        }
33      
34        // Return empty string if no odd string found (shouldn't happen with valid input)
35        return "";
36    }
37}
38
1class Solution {
2public:
3    string oddString(vector<string>& words) {
4        // Map to store difference patterns as keys and corresponding words as values
5        unordered_map<string, vector<string>> patternToWords;
6      
7        // Process each word to calculate its difference pattern
8        for (auto& word : words) {
9            string differencePattern;
10          
11            // Calculate differences between consecutive characters
12            for (int i = 0; i < word.size() - 1; ++i) {
13                // Calculate the difference between adjacent characters
14                int difference = word[i + 1] - word[i];
15              
16                // Append the difference to the pattern string
17                // Cast to char to store as a character representation
18                differencePattern += (char) difference;
19              
20                // Add separator for clarity between differences
21                differencePattern += ',';
22            }
23          
24            // Add the word to the map with its difference pattern as key
25            patternToWords[differencePattern].emplace_back(word);
26        }
27      
28        // Find and return the word with a unique difference pattern
29        for (auto& [pattern, wordList] : patternToWords) {
30            // The odd string will be the only one with its pattern
31            if (wordList.size() == 1) {
32                return wordList[0];
33            }
34        }
35      
36        // Return empty string if no odd string found (should not happen per problem constraints)
37        return "";
38    }
39};
40
1/**
2 * Finds the odd string in an array of strings based on character difference patterns.
3 * The odd string is the one whose consecutive character differences differ from all others.
4 * 
5 * @param words - Array of strings to analyze
6 * @returns The string with a unique character difference pattern
7 */
8function oddString(words: string[]): string {
9    // Map to store difference patterns as keys and corresponding strings as values
10    const differencePatternMap: Map<string, string[]> = new Map();
11  
12    // Process each word to calculate its character difference pattern
13    for (const word of words) {
14        // Array to store differences between consecutive characters
15        const characterDifferences: number[] = [];
16      
17        // Calculate differences between each pair of consecutive characters
18        for (let i = 0; i < word.length - 1; i++) {
19            const difference = word.charCodeAt(i + 1) - word.charCodeAt(i);
20            characterDifferences.push(difference);
21        }
22      
23        // Convert the differences array to a string key for map storage
24        const patternKey = characterDifferences.join(',');
25      
26        // Initialize the array for this pattern if it doesn't exist
27        if (!differencePatternMap.has(patternKey)) {
28            differencePatternMap.set(patternKey, []);
29        }
30      
31        // Add the current word to its corresponding pattern group
32        differencePatternMap.get(patternKey)!.push(word);
33    }
34  
35    // Find and return the string that appears alone (odd one out)
36    for (const [pattern, stringGroup] of differencePatternMap) {
37        if (stringGroup.length === 1) {
38            return stringGroup[0];
39        }
40    }
41  
42    // Return empty string if no odd string is found (shouldn't happen with valid input)
43    return '';
44}
45

Time and Space Complexity

Time Complexity: O(m × n)

The algorithm iterates through all n words in the list. For each word, it computes the difference array using pairwise(s), which generates m-1 pairs where m is the length of each string. Computing the differences between consecutive characters takes O(m) time per word. Therefore, the total time complexity is O(m × n).

Space Complexity: O(m × n)

The space complexity consists of:

  • The dictionary d which stores tuples as keys and lists of strings as values. In the worst case, all n words could have different difference patterns, resulting in n different tuples, each of length m-1.
  • Each tuple key has size O(m).
  • The values store the original strings, which in total can be up to O(n) strings, each of length m.
  • Therefore, the total space complexity is O(m × n).

Note: The reference answer states space complexity as O(m + n), but this appears to be incorrect for this implementation. The actual space complexity is O(m × n) because we're storing all the strings in the dictionary values and the tuple keys, which together require space proportional to the product of m and n.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Character Position Calculation

A common mistake is using the character itself instead of its position when calculating differences. For example:

# WRONG: Using characters directly
differences = tuple(char2 - char1 for char1, char2 in pairwise(word))

This will cause a TypeError since you cannot subtract strings directly. The correct approach uses ord() to get ASCII values:

# CORRECT: Converting to ASCII values first
differences = tuple(ord(char2) - ord(char1) for char1, char2 in pairwise(word))

2. Using List as Dictionary Key

Attempting to use a list as the dictionary key will fail since lists are mutable and unhashable:

# WRONG: Lists are not hashable
differences = [ord(char2) - ord(char1) for char1, char2 in pairwise(word)]
difference_groups[differences].append(word)  # KeyError!

The solution is to convert the list to a tuple:

# CORRECT: Tuples are immutable and hashable
differences = tuple(ord(char2) - ord(char1) for char1, char2 in pairwise(word))
difference_groups[differences].append(word)

3. Manual Index-Based Iteration

While not incorrect, using manual indexing is more error-prone and less readable:

# Error-prone approach
differences = []
for i in range(len(word) - 1):
    differences.append(ord(word[i+1]) - ord(word[i]))

The pairwise() function from itertools provides a cleaner, more Pythonic solution that avoids off-by-one errors.

4. Assuming Fixed Number of Groups

Don't assume there will always be exactly two groups. While the problem guarantees one outlier, it's better to write defensive code:

# RISKY: Assumes exactly 2 groups exist
groups = list(difference_groups.values())
return groups[0][0] if len(groups[0]) == 1 else groups[1][0]

The provided solution correctly handles any number of groups by iterating through all of them.

5. Forgetting Edge Cases

While the problem likely guarantees valid input, production code should handle edge cases:

  • Empty word list
  • Words with length 1 (no differences to calculate)
  • All words being identical (no outlier exists)

A more defensive version might include:

if not words or len(words[0]) < 2:
    return None  # or handle appropriately
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using the following tree as input?

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings

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

Load More