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

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.

1Python
Output
2Python
Output
3Python
Output

Two maps, not one

4Python
Output

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.

5Python
Output

"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):

6Python
Output

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).

How the code works

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.

How the code works

  1. forward.setdefault(x, y) != yInsert on first sight, compare on every later sight, in one expression. If x already maps somewhere else, this is the check that catches it.
  2. backward.setdefault(y, x) != xThe direction people omit. Without it two distinct characters can map onto the same target, and "badc"/"baba" passes.
  3. zip(a, b)Walks both together and stops at the shorter, which is why the length check has to come first — otherwise "ab" and "a" would compare equal on their overlap.
  4. seen.setdefault(ch, len(seen))The canonical form: each new character gets the next number, so the tuple describes the shape rather than the letters. Same idea as the anagram key.

Change one thing

  • Add ("abcd", "aabb"). Two characters collapsing onto one is exactly what the reverse map exists to reject.
  • Use the pattern as a dictionary key to group many strings by shape in one pass — the same move as grouping anagrams.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 3

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 · vizlearn.in/interview/isomorphic-strings.html

About the author

Ashish Jangra builds and maintains VizLearn. 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.