LeetCode 205. 同构字符串:解题思路+代码优化全解析

字符串匹配类题目是高频考点,而「同构字符串」作为其中的经典题型,不仅考察对映射关系的理解,更能检验我们对代码效率的把控。今天就来拆解 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 个高频踩坑点,帮你避开雷区:

  1. 只维护 s→t 的映射,不维护 t→s 的映射:比如 s="ab"、t="aa",此时 a→a、b→a,单向映射看似没问题,但违反"不同字符不能映射到同一个目标",会误判为同构;

  2. 忽略 ASCII 字符范围:用数组优化时,数组长度设为 26(仅小写字母),但题目中可能有大写字母、符号等,导致索引越界或映射错误,通用版建议设为 128;

  3. 没有前置判断长度:虽然题目明确 t.length == s.length,但实际刷题/面试中,加上前置判断 if (s.length !== t.length) return false,会让代码更严谨,也能避免无效遍历。

六、总结与拓展

LeetCode 205 题的核心是「双向映射校验」,原代码用 Map 实现了基础逻辑,而用数组替代 Map 是最优优化方案------既保持了 O(n) 的时间复杂度(已达最优,无法进一步降低),又将空间复杂度从 O(k) 优化到 O(1),同时提升了实际执行效率。

拓展思考:这道题的思路可以迁移到类似的「映射匹配」题目中,比如 LeetCode 290. 单词规律(单词与字符的双向映射),核心逻辑完全一致,只需将"字符映射"改为"单词映射"即可,大家可以动手试试,巩固今天的知识点。

相关推荐
renhongxia12 小时前
AI算法实战:逻辑回归在风控场景中的应用
人工智能·深度学习·算法·机器学习·信息可视化·语言模型·逻辑回归
CoderCodingNo2 小时前
【GESP】C++四级/五级练习题 luogu-P1223 排队接水
开发语言·c++·算法
民乐团扒谱机2 小时前
【AI笔记】精密光时频传递技术核心内容总结
人工智能·算法·光学频率梳
2301_812731412 小时前
CSS3笔记
前端·笔记·css3
ziblog2 小时前
CSS3白云飘动动画特效
前端·css·css3
越努力越幸运5082 小时前
CSS3学习之网格布局grid
前端·学习·css3
CoderCodingNo2 小时前
【GESP】C++五级/四级练习题 luogu-P1413 坚果保龄球
开发语言·c++·算法
半斤鸡胗2 小时前
css3基础
前端·css
ziblog2 小时前
CSS3创意精美页面过渡动画效果
前端·css·css3