205. Isomorphic Strings
Problem Description
You need to determine if two strings are isomorphic. Two strings s
and t
are considered isomorphic when you can replace characters in s
to transform it into t
.
The replacement must follow these rules:
- Each character in
s
must map to exactly one character int
- Each character in
t
must be mapped to by exactly one character ins
(one-to-one mapping) - A character can map to itself
- The mapping must be consistent - if character 'a' maps to 'b', then every occurrence of 'a' must map to 'b'
- The order of characters must be preserved
For example:
"egg"
and"add"
are isomorphic because we can map:e→a
,g→d
"foo"
and"bar"
are NOT isomorphic because 'o' would need to map to both 'a' and 'r'"paper"
and"title"
are isomorphic with mapping:p→t
,a→i
,e→l
,r→e
The solution uses two dictionaries to track the bidirectional mapping between characters. d1
maps characters from s
to t
, while d2
maps characters from t
to s
. As we iterate through both strings simultaneously, we check if any character already has a different mapping than what we're trying to establish. If we find a conflict (a character trying to map to multiple different characters), we return False
. If we successfully process all characters without conflicts, the strings are isomorphic.
Intuition
The key insight is recognizing that isomorphic strings require a bijective mapping (one-to-one correspondence) between characters. Think of it like creating a cipher where each letter consistently translates to exactly one other letter.
When we see a character in string s
, it must always map to the same character in string t
. Similarly, when we see a character in string t
, it must always come from the same character in string s
. This bidirectional constraint is crucial - it's not enough to just check one direction.
Why do we need two dictionaries? Consider what could go wrong with just one:
- If we only track
s→t
mappings, we might miss cases where two different characters ins
try to map to the same character int
- If we only track
t→s
mappings, we might miss cases where one character ins
tries to map to multiple characters int
By maintaining both d1
(mapping from s
to t
) and d2
(mapping from t
to s
), we can catch violations in both directions:
- When we see character
a
ins
andb
int
, we check: Hasa
already been mapped to something other thanb
? Hasb
already been mapped from something other thana
? - If either check fails, the strings cannot be isomorphic
The solution naturally emerges from this understanding: traverse both strings together, build the mapping relationships, and immediately return False
if we detect any inconsistency. If we complete the traversal without conflicts, the bijective mapping exists, confirming the strings are isomorphic.
Solution Approach
We implement the solution using two hash tables (dictionaries in Python) to maintain the bidirectional character mapping between strings s
and t
.
Data Structures:
d1
: Dictionary mapping characters froms
tot
d2
: Dictionary mapping characters fromt
tos
Algorithm Steps:
-
Initialize two empty dictionaries
d1
andd2
to store the character mappings. -
Iterate through both strings simultaneously using
zip(s, t)
, which pairs up characters at the same positions:- For each pair
(a, b)
wherea
is froms
andb
is fromt
- For each pair
-
Check for mapping conflicts:
- If
a
already exists ind1
but maps to a different character thanb
:(a in d1 and d1[a] != b)
- If
b
already exists ind2
but maps to a different character thana
:(b in d2 and d2[b] != a)
- If either condition is true, return
False
immediately
- If
-
Update the mappings:
- Set
d1[a] = b
to record that charactera
maps to characterb
- Set
d2[b] = a
to record that characterb
is mapped from charactera
- Set
-
Return the result:
- If we complete the iteration without finding any conflicts, return
True
- If we complete the iteration without finding any conflicts, return
Time Complexity: O(n)
where n
is the length of the strings, as we traverse both strings once.
Space Complexity: O(k)
where k
is the number of unique characters in the strings. In the worst case, this could be O(n)
if all characters are unique.
The solution elegantly handles all the requirements: it ensures one-to-one mapping, preserves order (by iterating sequentially), and allows self-mapping (no special case needed).
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with the example: s = "paper"
and t = "title"
Initial state:
d1 = {}
(maps s→t)d2 = {}
(maps t→s)
Iteration 1: a = 'p'
, b = 't'
- Check: Is 'p' in d1? No. Is 't' in d2? No.
- No conflicts, update mappings:
d1 = {'p': 't'}
d2 = {'t': 'p'}
Iteration 2: a = 'a'
, b = 'i'
- Check: Is 'a' in d1? No. Is 'i' in d2? No.
- No conflicts, update mappings:
d1 = {'p': 't', 'a': 'i'}
d2 = {'t': 'p', 'i': 'a'}
Iteration 3: a = 'p'
, b = 't'
- Check: Is 'p' in d1? Yes, it maps to 't'. Does it equal 't'? Yes. ✓
- Check: Is 't' in d2? Yes, it maps to 'p'. Does it equal 'p'? Yes. ✓
- No conflicts, mappings remain unchanged (already correct)
Iteration 4: a = 'e'
, b = 'l'
- Check: Is 'e' in d1? No. Is 'l' in d2? No.
- No conflicts, update mappings:
d1 = {'p': 't', 'a': 'i', 'e': 'l'}
d2 = {'t': 'p', 'i': 'a', 'l': 'e'}
Iteration 5: a = 'r'
, b = 'e'
- Check: Is 'r' in d1? No. Is 'e' in d2? No.
- No conflicts, update mappings:
d1 = {'p': 't', 'a': 'i', 'e': 'l', 'r': 'e'}
d2 = {'t': 'p', 'i': 'a', 'l': 'e', 'e': 'r'}
Result: No conflicts found during the entire traversal, return True
.
Now let's see a failing case with s = "foo"
and t = "bar"
:
Iteration 1: a = 'f'
, b = 'b'
d1 = {'f': 'b'}
,d2 = {'b': 'f'}
Iteration 2: a = 'o'
, b = 'a'
d1 = {'f': 'b', 'o': 'a'}
,d2 = {'b': 'f', 'a': 'o'}
Iteration 3: a = 'o'
, b = 'r'
- Check: Is 'o' in d1? Yes, it maps to 'a'. Does it equal 'r'? No! ✗
- Conflict detected: 'o' cannot map to both 'a' and 'r'
- Return
False
immediately
Solution Implementation
1class Solution:
2 def isIsomorphic(self, s: str, t: str) -> bool:
3 # Dictionary to map characters from s to t
4 s_to_t_mapping = {}
5 # Dictionary to map characters from t to s (ensures bijection)
6 t_to_s_mapping = {}
7
8 # Iterate through both strings simultaneously
9 for char_s, char_t in zip(s, t):
10 # Check if char_s already has a different mapping
11 if char_s in s_to_t_mapping and s_to_t_mapping[char_s] != char_t:
12 return False
13
14 # Check if char_t already has a different mapping (ensures one-to-one)
15 if char_t in t_to_s_mapping and t_to_s_mapping[char_t] != char_s:
16 return False
17
18 # Create bidirectional mapping
19 s_to_t_mapping[char_s] = char_t
20 t_to_s_mapping[char_t] = char_s
21
22 # All characters follow isomorphic mapping rules
23 return True
24
1class Solution {
2 public boolean isIsomorphic(String s, String t) {
3 // Map to store character mappings from string s to string t
4 Map<Character, Character> sToTMap = new HashMap<>();
5 // Map to store character mappings from string t to string s
6 Map<Character, Character> tToSMap = new HashMap<>();
7
8 int length = s.length();
9
10 // Iterate through both strings simultaneously
11 for (int i = 0; i < length; i++) {
12 char charFromS = s.charAt(i);
13 char charFromT = t.charAt(i);
14
15 // Check if charFromS already has a mapping
16 // If it exists and maps to a different character, strings are not isomorphic
17 if (sToTMap.containsKey(charFromS) && sToTMap.get(charFromS) != charFromT) {
18 return false;
19 }
20
21 // Check if charFromT already has a mapping
22 // If it exists and maps to a different character, strings are not isomorphic
23 if (tToSMap.containsKey(charFromT) && tToSMap.get(charFromT) != charFromS) {
24 return false;
25 }
26
27 // Create or update the bidirectional mapping
28 sToTMap.put(charFromS, charFromT);
29 tToSMap.put(charFromT, charFromS);
30 }
31
32 // All characters satisfy the isomorphic property
33 return true;
34 }
35}
36
1class Solution {
2public:
3 bool isIsomorphic(string s, string t) {
4 // Create mapping arrays for both strings
5 // Index represents ASCII character, value represents last seen position
6 int charToPositionS[256] = {0}; // Mapping for string s
7 int charToPositionT[256] = {0}; // Mapping for string t
8
9 int length = s.size();
10
11 // Iterate through both strings simultaneously
12 for (int i = 0; i < length; ++i) {
13 char charFromS = s[i];
14 char charFromT = t[i];
15
16 // Check if the characters have been mapped to different positions previously
17 // If the last seen positions don't match, the strings are not isomorphic
18 if (charToPositionS[charFromS] != charToPositionT[charFromT]) {
19 return false;
20 }
21
22 // Update the last seen position for both characters
23 // Using (i + 1) to distinguish from initial zero values
24 charToPositionS[charFromS] = i + 1;
25 charToPositionT[charFromT] = i + 1;
26 }
27
28 return true;
29 }
30};
31
1/**
2 * Determines if two strings are isomorphic.
3 * Two strings are isomorphic if characters in string s can be replaced to get string t.
4 * Each character must map to exactly one character (bijection).
5 *
6 * @param s - The first string to compare
7 * @param t - The second string to compare
8 * @returns true if the strings are isomorphic, false otherwise
9 */
10function isIsomorphic(s: string, t: string): boolean {
11 // Array to store the last seen position of each character in string s
12 // Using 256 to cover all extended ASCII characters
13 const lastPositionInS: number[] = new Array(256).fill(0);
14
15 // Array to store the last seen position of each character in string t
16 const lastPositionInT: number[] = new Array(256).fill(0);
17
18 // Iterate through both strings simultaneously
19 for (let i = 0; i < s.length; ++i) {
20 // Get ASCII code of current character in string s
21 const charCodeS = s.charCodeAt(i);
22
23 // Get ASCII code of current character in string t
24 const charCodeT = t.charCodeAt(i);
25
26 // Check if the mapping is consistent
27 // If characters have different last seen positions, they don't map correctly
28 if (lastPositionInS[charCodeS] !== lastPositionInT[charCodeT]) {
29 return false;
30 }
31
32 // Update last seen position for both characters
33 // Using i + 1 to distinguish from initial value 0
34 lastPositionInS[charCodeS] = i + 1;
35 lastPositionInT[charCodeT] = i + 1;
36 }
37
38 // All characters map consistently
39 return true;
40}
41
Time and Space Complexity
The time complexity is O(n)
where n
is the length of the input strings s
and t
. The algorithm iterates through both strings once using the zip
function, and for each character pair, it performs constant-time dictionary operations (lookup and insertion).
The space complexity is O(C)
where C
is the size of the character set. In the worst case, each unique character in the strings gets mapped in the dictionaries d1
and d2
. Since the problem deals with ASCII characters, C = 256
represents the maximum possible unique characters that could be stored in each dictionary. Therefore, the space used by both dictionaries is bounded by 2 * C = O(C)
.
Common Pitfalls
Pitfall 1: Using Only One Dictionary (Unidirectional Mapping)
A frequent mistake is attempting to solve this problem using only a single dictionary to map from s
to t
, without checking the reverse mapping. This fails to ensure the one-to-one (bijective) relationship required for isomorphism.
Incorrect Approach:
def isIsomorphic(self, s: str, t: str) -> bool:
mapping = {}
for char_s, char_t in zip(s, t):
if char_s in mapping and mapping[char_s] != char_t:
return False
mapping[char_s] = char_t
return True
Why it fails: Consider s = "badc"
and t = "baba"
. Using only one dictionary:
b → b
,a → a
,d → b
,c → a
- This passes the single dictionary check, but it's incorrect because both
b
andd
map tob
, violating the one-to-one mapping rule.
Solution: Always use two dictionaries to track both forward and reverse mappings, ensuring each character in t
is mapped to by exactly one character in s
.
Pitfall 2: Forgetting to Check Existing Mappings Before Assignment
Another mistake is updating the dictionaries without first checking if a conflicting mapping already exists, leading to overwriting previous mappings.
Incorrect Approach:
def isIsomorphic(self, s: str, t: str) -> bool:
s_to_t = {}
t_to_s = {}
for char_s, char_t in zip(s, t):
s_to_t[char_s] = char_t # Assigns without checking
t_to_s[char_t] = char_s
if s_to_t[char_s] != char_t or t_to_s[char_t] != char_s:
return False
return True
Why it fails: The check happens after assignment, so it will always pass since we just set those exact values. The original mapping gets overwritten before we can detect the conflict.
Solution: Always check for existing mappings before updating the dictionaries. The condition should verify if a key exists AND has a different value than what we're trying to map.
Pitfall 3: Assuming Equal String Lengths
While most implementations assume strings have equal length (and many problem statements guarantee this), it's good practice to handle edge cases.
Defensive Programming:
def isIsomorphic(self, s: str, t: str) -> bool:
if len(s) != len(t):
return False
# ... rest of the solution
Solution: Add a length check at the beginning if the problem doesn't guarantee equal-length strings. This prevents potential issues with zip()
silently truncating to the shorter string's length.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
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!