2451. Odd String Difference
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 ton-2
, computedifference[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.
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:
- We need to group strings by their difference arrays
- All strings except one will fall into the same group
- 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:
-
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. -
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 atd[t]
- Calculate the difference array using
-
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 EvaluatorExample 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, alln
words could have different difference patterns, resulting inn
different tuples, each of lengthm-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 lengthm
. - 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
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
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!