Facebook Pixel

205. Isomorphic Strings

EasyHash TableString
Leetcode Link

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 in t
  • Each character in t must be mapped to by exactly one character in s (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.

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

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 in s try to map to the same character in t
  • If we only track t→s mappings, we might miss cases where one character in s tries to map to multiple characters in t

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 in s and b in t, we check: Has a already been mapped to something other than b? Has b already been mapped from something other than a?
  • 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 from s to t
  • d2: Dictionary mapping characters from t to s

Algorithm Steps:

  1. Initialize two empty dictionaries d1 and d2 to store the character mappings.

  2. Iterate through both strings simultaneously using zip(s, t), which pairs up characters at the same positions:

    • For each pair (a, b) where a is from s and b is from t
  3. Check for mapping conflicts:

    • If a already exists in d1 but maps to a different character than b: (a in d1 and d1[a] != b)
    • If b already exists in d2 but maps to a different character than a: (b in d2 and d2[b] != a)
    • If either condition is true, return False immediately
  4. Update the mappings:

    • Set d1[a] = b to record that character a maps to character b
    • Set d2[b] = a to record that character b is mapped from character a
  5. Return the result:

    • If we complete the iteration without finding any conflicts, return True

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 Evaluator

Example 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 and d map to b, 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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

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

Load More