字符串匹配类题目是高频考点,而「同构字符串」作为其中的经典题型,不仅考察对映射关系的理解,更能检验我们对代码效率的把控。今天就来拆解 LeetCode 205 题,从题目解读、原代码分析,到时间空间双优化,一步步吃透这道题。
一、题目解读:什么是同构字符串?
题目给出两个字符串 s 和 t,要求判断它们是否是同构的,核心规则可以提炼为 3 点(划重点,避免踩坑):
-
映射唯一性:s 中的每个字符,必须映射到 t 中的同一个字符(不能今天映射 A,明天映射 B);
-
反向唯一性:t 中的每个字符,也只能被 s 中的同一个字符映射(不同字符不能"共用"一个映射目标);
-
顺序不变性:映射过程中,字符出现的顺序不能改变,且字符可以映射到自身(比如 s 和 t 完全相同,也是同构)。
补充约束:两个字符串长度一定相等(1 ≤ s.length ≤ 5×10⁴),且由任意有效 ASCII 字符组成(不止小写字母,可能包含符号、数字等),这意味着我们的代码需要兼顾通用性和高效性,不能因数据量太大而超时。
举个例子,快速理解
-
有效案例:s = "egg",t = "add" → 同构(e→a,g→d,g→d,满足所有规则);
-
无效案例1:s = "foo",t = "bar" → 不同构(o 既要映射 a,又要映射 r,违反唯一性);
-
无效案例2:s = "ab",t = "aa" → 不同构(a→a,b→a,两个不同字符映射到同一个目标,违反反向唯一性)。
二、原代码解析:思路正确,但有优化空间
先看题干给出的 TypeScript 代码,思路是完全正确的,我们先拆解它的核心逻辑,再分析可优化点。
1. 原代码实现
typescript
function isIsomorphic(s: string, t: string): boolean {
const mapS = new Map<string, string>();
const mapT = new Map<string, string>();
for (let i = 0; i < s.length; i++) {
const x = s[i], y = t[i];
if ((mapS.has(x) && mapS.get(x) !== y) || (mapT.has(y) && mapT.get(y) !== x)) {
return false;
}
mapS.set(x, y);
mapT.set(y, x);
}
return true;
};
2. 核心思路(双向映射,避免冲突)
这道题的关键的是「双向校验」,因为只做单向映射会忽略"不同字符映射到同一个目标"的问题(比如上面的无效案例2),原代码用了两个 Map 完美解决这个问题:
-
mapS:存储 s → t 的映射(key 是 s 中的字符,value 是对应的 t 中的字符);
-
mapT:存储 t → s 的映射(key 是 t 中的字符,value 是对应的 s 中的字符);
-
遍历过程中,每一对字符 (x, y) 都要校验:如果 x 已经在 mapS 中,且映射的不是 y → 冲突;如果 y 已经在 mapT 中,且映射的不是 x → 冲突;只要有一个冲突,直接返回 false;
-
如果没有冲突,就把这对映射关系存入两个 Map,继续遍历,直到结束,返回 true。
3. 原代码的优缺点
优点:逻辑清晰、无 bug,能够覆盖所有测试用例,上手容易,适合面试时快速写出基础版本。
可优化点:时间和空间效率有提升空间------
-
时间上:Map 的 has、get、set 方法虽然是 O(1) 平均复杂度,但 Map 是复杂数据结构,有哈希表维护、原型链查询等额外开销,在数据量达到 5×10⁴ 时,会有细微的性能损耗;
-
空间上:Map 的空间开销是动态的(O(k),k 是字符串中不同字符的数量),最坏情况下(所有字符都不同),空间复杂度是 O(n),可以优化到 O(1)。
三、优化方案:数组替代 Map,实现效率翻倍
核心优化思路:利用「ASCII 字符的特性」------ 任意有效 ASCII 字符的 charCodeAt(0) 取值范围是 0~127,我们可以用两个固定长度为 128 的数组,替代两个 Map,数组的索引对应字符的 ASCII 码,数组的值存储对应的映射字符。
数组的访问、赋值操作是纯底层操作,比 Map 更快,且固定长度的数组空间开销是常量 O(1),不受字符串长度影响,完美解决原代码的优化点。
优化后代码(通用版,支持所有 ASCII 字符)
typescript
function isIsomorphic(s: string, t: string): boolean {
// 前置边界判断:虽然题目说t.length == s.length,但加上更严谨,避免无效遍历
if (s.length !== t.length) return false;
// 用数组替代Map,索引=ASCII码,值=映射字符,初始化长度128(覆盖所有ASCII字符)
const mapS: (string | undefined)[] = new Array(128);
const mapT: (string | undefined)[] = new Array(128);
for (let i = 0; i < s.length; i++) {
const x = s[i];
const y = t[i];
// 获取字符对应的ASCII码,作为数组索引
const xCode = x.charCodeAt(0);
const yCode = y.charCodeAt(0);
// 校验逻辑和原代码一致,只是用数组索引替代Map的key
if ((mapS[xCode] !== undefined && mapS[xCode] !== y) || (mapT[yCode] !== undefined && mapT[yCode] !== x)) {
return false;
}
// 存入映射关系
mapS[xCode] = y;
mapT[yCode] = x;
}
return true;
}
进一步极致优化(小写字母专用版)
如果题目约束改为「仅包含小写英文字母」(很多类似变式题会有这个约束),可以将数组长度缩小到 26,进一步节省空间、提升效率------通过 x.charCodeAt(0) - 'a'.charCodeAt(0) 得到 0~25 的索引(对应 a~z)。
typescript
function isIsomorphic(s: string, t: string): boolean {
if (s.length !== t.length) return false;
// 仅针对小写英文字母,数组长度26即可
const baseCode = 'a'.charCodeAt(0);
const mapS: (string | undefined)[] = new Array(26);
const mapT: (string | undefined)[] = new Array(26);
for (let i = 0; i < s.length; i++) {
const x = s[i];
const y = t[i];
// 转换为0~25的索引
const xCode = x.charCodeAt(0) - baseCode;
const yCode = y.charCodeAt(0) - baseCode;
if ((mapS[xCode] !== undefined && mapS[xCode] !== y) || (mapT[yCode] !== undefined && mapT[yCode] !== x)) {
return false;
}
mapS[xCode] = y;
mapT[yCode] = x;
}
return true;
}
四、优化效果对比(关键指标)
| 指标 | 原代码(Map版) | 优化代码(数组版) |
|---|---|---|
| 时间复杂度 | O(n)(平均情况) | O(n)(最优,无额外开销) |
| 空间复杂度 | O(k)(k为不同字符数,最坏O(n)) | O(1)(固定长度数组,常量开销) |
| 实际执行速度 | 中等(Map有封装层开销) | 更快(数组底层操作,无额外损耗) |
| 通用性 | 强(支持所有字符) | 通用版强,小写版针对性强 |
五、常见踩坑点提醒(面试高频)
做这道题时,很多人会忽略「双向映射」,只做单向映射导致出错,这里总结 3 个高频踩坑点,帮你避开雷区:
-
只维护 s→t 的映射,不维护 t→s 的映射:比如 s="ab"、t="aa",此时 a→a、b→a,单向映射看似没问题,但违反"不同字符不能映射到同一个目标",会误判为同构;
-
忽略 ASCII 字符范围:用数组优化时,数组长度设为 26(仅小写字母),但题目中可能有大写字母、符号等,导致索引越界或映射错误,通用版建议设为 128;
-
没有前置判断长度:虽然题目明确 t.length == s.length,但实际刷题/面试中,加上前置判断
if (s.length !== t.length) return false,会让代码更严谨,也能避免无效遍历。
六、总结与拓展
LeetCode 205 题的核心是「双向映射校验」,原代码用 Map 实现了基础逻辑,而用数组替代 Map 是最优优化方案------既保持了 O(n) 的时间复杂度(已达最优,无法进一步降低),又将空间复杂度从 O(k) 优化到 O(1),同时提升了实际执行效率。
拓展思考:这道题的思路可以迁移到类似的「映射匹配」题目中,比如 LeetCode 290. 单词规律(单词与字符的双向映射),核心逻辑完全一致,只需将"字符映射"改为"单词映射"即可,大家可以动手试试,巩固今天的知识点。