Solving tips
- Recognize this as building a bijection in one pass: walk s and t in lockstep and commit each character's mapping on its first appearance.
- There are two independent failure modes, so keep two maps: s->t catches one source mapping to two targets, and t->s catches two sources colliding on one target (injectivity).
- Target O(n) time and O(k) space for the alphabet; a terse alternative is len(set(zip(s,t))) == len(set(s)) == len(set(t)).
- Pitfall: checking only one direction wrongly accepts cases like 'badc'/'baba'; the reverse map (or the distinct-pair count) is mandatory.
Problem
Two strings s and t of equal length are isomorphic if there is a one-to-one substitution of characters that turns s into t: every occurrence of a character in s must map to the same character in t, and no two different characters of s may map to the same character of t. A character may map to itself.
Given s and t, return True if they are isomorphic and False otherwise.
Examples
s = "egg", t = "add" β True β map e β a, g β d; the mapping is consistent and injective.
s = "foo", t = "bar" β False β o would have to map to both a and r.
s = "badc", t = "baba" β False β b β b and d β b would send two different characters to b, which the one-to-one rule forbids.
Constraints
1 <= len(s) <= 5 * 10^4
t has the same length as s
- The strings may contain any valid ASCII characters.
Length 5 * 10^4 makes comparing all pairs of positions (O(n^2)) too slow; a single hashed pass is expected.
Think about it first
Hint 1
Walk both strings in lockstep and try to build the substitution as you go. What are the two distinct ways a new pair (s[i], t[i]) can contradict what you've already committed to?
Hint 2
One dictionary from s-characters to t-characters catches "same source, two targets". What extra structure catches "two sources, same target"?
Hint 3
Keep two hash maps β sβt and tβs. For each position, if either map already binds the character to something different, fail; otherwise record both directions. Consistency in both maps at the end means isomorphic.
TL;DR
Two hash maps (one per direction) in a single pass β O(n) time, O(k) space for the alphabet.
Approach 1 β Brute force (pairwise consistency)
Isomorphism is equivalent to: for every pair of positions i, j, the characters of s match at those positions exactly when the characters of t do. Check all pairs.
class Solution:
def isIsomorphic(self, s: str, t: str) -> bool:
n = len(s)
for i in range(n):
for j in range(i + 1, n):
if (s[i] == s[j]) != (t[i] == t[j]):
return False
return True
Complexity: O(n^2) time, O(1) space. At n = 5 * 10^4 that is ~1.25 * 10^9 comparisons β the constraints kill it outright.
Approach 2 β Two hash maps
The insight: the substitution can be built greedily in one pass, because the first time a character appears it must commit to its mapping forever. Two failure modes exist and each needs its own map: an s-character re-mapping to a new target (checked via s β t), and two s-characters colliding on one target (checked via t β s β this is what makes the mapping injective).
class Solution:
def isIsomorphic(self, s: str, t: str) -> bool:
s_to_t: dict[str, str] = {}
t_to_s: dict[str, str] = {}
for a, b in zip(s, t):
if a in s_to_t and s_to_t[a] != b:
return False
if b in t_to_s and t_to_s[b] != a:
return False
s_to_t[a] = b
t_to_s[b] = a
return True
Walkthrough on s = "badc", t = "baba":
| a | b | s_to_t check | t_to_s check | maps after |
|---|
| b | b | new | new | bβb / bβb |
| a | a | new | new | +aβa / aβa |
| d | b | new | b already maps back to b, not d β return False | β |
And on s = "egg", t = "add": eβa and gβd are recorded at their first occurrences; the second g/d pair agrees with both maps β True.
Complexity: O(n) time, O(k) space where k is the number of distinct characters (bounded by the alphabet, so effectively O(1)).
Approach 3 β First-occurrence signatures (zip-and-count)
The insight: s and t are isomorphic exactly when the pairing zip(s, t) never puts one character in two different pairs β i.e. the number of distinct pairs equals the number of distinct characters on each side. Three set sizes settle it.
class Solution:
def isIsomorphic(self, s: str, t: str) -> bool:
return len(set(zip(s, t))) == len(set(s)) == len(set(t))
Walkthrough on s = "foo", t = "bar":
set(zip(s, t)) = {('f','b'), ('o','a'), ('o','r')} β size 3 (the character o shows up in two different pairs, inflating the pair count).
set(s) = {'f','o'} β size 2; set(t) = {'b','a','r'} β size 3.
3 == 2 is false β False. For "egg"/"add" the sizes are 2, 2, 2 β True.
Complexity: O(n) time, O(n) space in the worst case for the pair set. Same asymptotics as Approach 2 β terser, though it always scans everything and canβt fail fast mid-string.
Common pitfalls
- Checking only one direction: a single
s β t map accepts s = "badc", t = "baba" even though two sources collide on b β the injectivity check (t β s, or the set(t) size) is mandatory.
- The tempting-but-wrong shortcut
set(s) vs set(t) sizes alone: "ab" and "aa" have sizes 2 and 1 and fail correctly, but "ab" vs "ca" has sizes 2 and 2 while being isomorphic β equal alphabet sizes are necessary, not sufficient; you must also count the distinct pairs.
- Overwriting a map entry without checking it first β the first binding of a character is a permanent commitment; later occurrences may only confirm it.
- Assuming lowercase letters: the problem allows any ASCII characters, so fixed 26-slot arrays need to be sized to the full character range (hash maps sidestep this).
Pattern takeaway
βConsistent relabelingβ problems reduce to building a bijection incrementally with a pair of hash maps β one per direction β where each elementβs first appearance fixes its image and every later appearance is verified against it. Whenever a constraint is symmetric (βno two X share a Yβ and βno two Y share an Xβ), expect to maintain both directions explicitly; checking one and hoping is the classic bug. The same double-map skeleton solves Word Pattern and friends.