Isomorphic strings

Walk both strings together, maintaining two maps: a → b and b → a. Every pair must agree with both. One map alone allows two characters to collapse onto one, so it wrongly accepts "badc" and "baba" — which is exactly the case interviewers test.

Overview

The problem

Two strings are isomorphic if the characters of one can be replaced by the characters of the other consistently, preserving order.

stIsomorphic?Why
"egg""add"Yese→a, g→d
"foo""bar"Noo would map to both a and r
"paper""title"Yesp→t, a→i, e→l, r→e
"badc""baba"Nod and c would both map to a
"ab""aa"NoSame reason

The critical requirement is that the mapping must be a bijection — one-to-one in both directions. The "badc" / "baba" case is the one that catches solutions checking only one direction.

StringsCoding problemMedium

Step through it

paper
p
a
p
e
r
title
t
i
t
l
e
a -> b
p : t
'p' maps to 't'.
pairs1at0
Step 1 of 6

What to watch

  • Each step checks the pair against both maps.
  • A character already mapped elsewhere fails immediately.
  • The two maps at the end are inverses of each other.

Say this out loud

"Two dictionaries, one each way, because the mapping has to be a bijection. With only the forward map you'd accept two letters mapping onto the same one."

Isomorphic strings

Do two strings have the same character pattern? 'egg' and 'add' do; 'foo' and 'bar' do not.

Run it

The two-map version and the single-map version run side by side, on the input that separates them, plus the canonical-form alternative reaching the same verdicts.

Two maps, not one

O(n) time, O(k) space where k is the alphabet size.

Checking only forward accepts "badc" / "baba": b→b, a→a, d→b... which conflicts, so that particular pair is caught. But "ab" / "aa" is not — a→a and b→a are both consistent forwards, and they map two characters to one.

The second map is what enforces injectivity, and stating why it is needed — with a counterexample — is the substance of a good answer.

The pattern-normalisation alternative

An elegant one-liner: two strings are isomorphic exactly when their occurrence patterns match.

"egg" gives [0, 1, 1] and "add" gives [0, 1, 1] — equal, so isomorphic. "foo" gives [0, 1, 1] and "bar" gives [0, 1, 2] — different.

This automatically handles both directions, because the pattern is a canonical form. The catch is that x.index(ch) is O(n), making the whole thing O(n²). Fixing it with a dictionary restores O(n):

Worth offering as the canonical-form view after the two-map version, because it is the same idea that solves the "group anagrams" family — normalise, then compare.

What isomorphic means here

There must be a one-to-one correspondence between the characters: replacing every character of the first string according to a fixed rule produces the second, and no two characters map to the same target. "Same shape, different letters".

A length check is a free first rejection, and the empty string is isomorphic to itself. The canonical-form view makes the whole thing one comparison: encode each string as the first-occurrence index of every character, so "paper" becomes [0, 1, 0, 2, 3] and "title" becomes [0, 1, 0, 2, 3] — identical, so isomorphic — while "foo" gives [0, 1, 1] and "bar" gives [0, 1, 2], which differ at the third position. Because the pattern is canonical it checks both directions at once, which is exactly what the two-map version needs a second map to do.

Why one map is not enough

Track only a → b and "badc" against "baba" passes: b→b, a→a, d→b, c→a — every forward rule is consistent, and two distinct characters have collapsed onto one target.

The reverse map catches it: b is already claimed by b, so d→b is rejected. Both directions are needed because a bijection is a constraint in both directions.

The canonical-form alternative

Normalise each string to the pattern of first appearances — "paper" and "title" both become [0,1,2,0,3] — and compare the patterns. One pass each, no paired bookkeeping, and it extends naturally to comparing many strings at once by using the pattern as a dictionary key.

That is the same "canonicalise, then compare" idea as grouping anagrams, which is worth pointing out.

Edge cases

CaseResult
Different lengthsFalse — check first, it is O(1)
Both emptyTrue
Single charactersTrue, whatever they are
Same stringTrue — the identity mapping
"aa" / "ab"False — a cannot map to two characters
"ab" / "aa"False — needs the backward map
All distinct in bothTrue, if lengths match
UnicodeWorks — dictionaries handle any hashable character

The two "ab"/"aa" rows are the pair to test: one direction catches the first, and only the second map catches the second. Mentioning both demonstrates that the bijection requirement is understood rather than copied.

Word pattern. The same problem with words instead of characters: does "dog cat cat dog" match the pattern "abba"? Identical structure over s.split().

Group isomorphic strings. Use the normalised pattern as a dictionary key, exactly as sorted characters group anagrams. The canonical-form technique again.

Isomorphic graphs. Superficially related and vastly harder — graph isomorphism has no known polynomial algorithm. Worth knowing the distinction if it comes up, because the name invites confusion.

Find and replace with a consistent mapping. The practical version: applying a cipher or a tokenisation consistently.

ProblemCanonical form
Isomorphic stringsFirst-occurrence index pattern
AnagramsSorted characters, or a count tuple
RotationsSmallest rotation, or b in a + a
Case-insensitive equalityCasefolded string

That table is the transferable lesson: when a problem asks whether two things are equivalent under some transformation, compute a canonical form and compare. It turns an equivalence test into a hash lookup, which is what makes grouping possible.

What it is testing

Do you require a bijection? The one-map solution is the trap, and the interviewer will have "ab" / "aa" ready.

Do you check lengths first? One line, O(1), and it avoids the whole loop.

Do you know it is order-sensitive? Isomorphism preserves position, unlike anagrams — conflating the two is a conceptual error.

Do you see the canonical-form view? Offering the pattern-normalisation solution shows you can reframe rather than only implement.

Is your index-based version O(n) or O(n²)? The elegant one-liner is quadratic, and noticing that is the difference between neat and correct.

Recap in one screen

  • Isomorphic means a consistent one-to-one character mapping preserving order.
  • Two dictionaries are required — forward and backward — or "ab" / "aa" passes incorrectly.
  • O(n) time, O(k) space; check lengths first.
  • Normalising each string to its first-occurrence index pattern gives a canonical form that handles both directions.
  • Use str.index inside a comprehension and the elegant version becomes O(n²); a dictionary restores O(n).

Check yourself

0 of …

Answer without scrolling back up.

  1. Why are two maps needed rather than one?

  2. The canonical-form approach compares:

  3. Why must the length check come before zip?

Cheat sheet

Isomorphic strings

Walk both strings together, maintaining two maps: a → b and b → a. Every pair must agree with both. One map alone allows two characters to collapse onto one, so it wrongly accepts "badc" and "baba" — which is exactly the case interviewers test.

INTERVIEW · AI.Cruz.in/interview/isomorphic-strings.html

About the author

AI.Cruz builds and maintains AI.Cruz. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.