力扣解题-205. 同构字符串
给定两个字符串 s 和 t ,判断它们是否是同构的。
如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。
每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。
示例 1:
输入:s = "egg", t = "add"
输出:true
解释:
字符串 s 和 t 可以通过以下方式变得相同:
将 'e' 映射为 'a'。
将 'g' 映射为 'd'。
示例 2:
输入:s = "f11", t = "b23"
输出:false
解释:
字符串 s 和 t 无法变得相同,因为 '1' 需要同时映射到 '2' 和 '3'。
示例 3:
输入:s = "paper", t = "title"
输出:true
提示:
1 <= s.length <= 5 * 104
t.length == s.length
s 和 t 由任意有效的 ASCII 字符组成
Related Topics
哈希表、字符串
第一次解答
解题思路
核心方法:双向哈希表映射法,通过两个HashMap分别维护s→t和t→s的字符映射关系,确保“相同字符映射唯一”且“不同字符不映射到同一字符”,逻辑严谨且能完全满足题目约束,时间复杂度O(n)、空间复杂度O(k)(k为不同字符数)。
核心逻辑拆解
判断同构字符串的核心是“双向唯一映射”:
- 正向约束:s中的同一个字符,必须始终映射到t中的同一个字符(比如s的'e'不能既映射到'a'又映射到'b');
- 反向约束:t中的同一个字符,只能被s中的一个字符映射(比如s的'e'和'g'不能同时映射到t的'a');
- 用两个HashMap分别记录:
map:key为s的字符,value为t的字符(正向映射);map2:key为t的字符,value为s的字符(反向映射);
- 遍历每个字符位置i:
- 若s[i]已在
map中,检查map.get(s[i])是否等于t[i],不等则返回false; - 若s[i]不在
map中,检查t[i]是否已在map2中(避免不同s字符映射到同一t字符),若存在则返回false; - 若都满足,将s[i]→t[i]存入
map,t[i]→s[i]存入map2;
- 若s[i]已在
- 遍历完成后返回true。
具体步骤(以示例1 s="egg",t="add"为例)
- i=0(s='e',t='a'):
map无'e',map2无'a' → 存入map(e→a)、map2(a→e);
- i=1(s='g',t='d'):
map无'g',map2无'd' → 存入map(g→d)、map2(d→g);
- i=2(s='g',t='d'):
map有'g',且map.get(g)=d == t[i] → 验证通过;
- 遍历完成,返回true。
性能说明
- 时间复杂度:O(n)(仅遍历字符串一次,HashMap的增删查操作均为O(1));
- 空间复杂度:O(k)(k为字符串中不同字符的数量,最多为256个ASCII字符);
- 优势:逻辑直观,双向映射能完全覆盖题目所有约束(相同字符唯一映射、不同字符不重复映射);
- 潜在优化点:HashMap的自动装箱/拆箱、哈希计算有轻微开销,可通过数组优化进一步提升性能。
public boolean isIsomorphic(String s, String t) {
boolean result=true;
Map<Character,Character> map = new HashMap<>();
Map<Character,Character> map2 = new HashMap<>();
for(int i=0;i<s.length();i++){
char a=s.charAt(i);
if(map.containsKey(a)){
if(map.get(a)!=t.charAt(i)){
result=false;
break;
}
}else {
if(map2.containsKey(t.charAt(i))){
result=false;
break;
}else {
map.put(a,t.charAt(i));
map2.put(t.charAt(i),a);
}
}
}
return result;
}
示例解答
解题思路
解法1:数组映射法(性能最优解)
核心方法:ASCII数组替代哈希表,利用ASCII字符的取值范围(0~255),用两个长度为256的数组分别记录s→t和t→s的映射关系,避免HashMap的哈希计算和装箱开销,时间复杂度O(n)、空间复杂度O(1)(数组长度固定为256),是本题的性能最优解。
核心逻辑拆解
ASCII字符的取值范围是0~255,因此可以用数组下标直接映射字符,数组值记录映射的字符编码:
- 初始化两个数组
s2t和t2s,长度均为256,初始值为0(表示未映射); - 遍历每个字符位置i:
- 取s[i]的编码
sc = s.charAt(i),t[i]的编码tc = t.charAt(i); - 若
s2t[sc] != 0(已映射),检查s2t[sc]是否等于tc,不等则返回false; - 若
t2s[tc] != 0(反向已映射),检查t2s[tc]是否等于sc,不等则返回false; - 若都未映射,设置
s2t[sc] = tc、t2s[tc] = sc;
- 取s[i]的编码
- 遍历完成返回true。
代码实现
public boolean isIsomorphic(String s, String t) {
// 初始化ASCII映射数组,0表示未映射
int[] s2t = new int[256];
int[] t2s = new int[256];
// 填充初始值为-1(避免与字符编码0冲突,比如ASCII 0是空字符)
Arrays.fill(s2t, -1);
Arrays.fill(t2s, -1);
int n = s.length();
for (int i = 0; i < n; i++) {
char sc = s.charAt(i);
char tc = t.charAt(i);
// 正向映射已存在但不匹配
if (s2t[sc] != -1 && s2t[sc] != tc) {
return false;
}
// 反向映射已存在但不匹配
if (t2s[tc] != -1 && t2s[tc] != sc) {
return false;
}
// 建立双向映射
s2t[sc] = tc;
t2s[tc] = sc;
}
return true;
}
性能优势
- 时间复杂度:O(n)(遍历一次字符串,数组访问为O(1)且无额外开销);
- 空间复杂度:O(1)(数组长度固定为256,与输入规模无关);
- 核心优化点:
- 数组替代HashMap:避免哈希冲突、自动装箱/拆箱、方法调用等开销,访问速度是HashMap的数倍;
- 直接操作字符编码:无需Character对象,减少内存占用;
- 适用场景:大数据量(如n=5×10⁴)下,性能远超哈希表解法。
解法2:字符首次出现位置映射法(拓展思路)
核心方法:统一字符的首次出现位置,将两个字符串转换为“字符首次出现位置”的序列,若序列完全一致则为同构字符串,无需维护双向映射,逻辑更简洁。
代码实现
public boolean isIsomorphic(String s, String t) {
int n = s.length();
// 记录字符首次出现的位置(初始为0)
int[] sPos = new int[256];
int[] tPos = new int[256];
for (int i = 0; i < n; i++) {
char sc = s.charAt(i);
char tc = t.charAt(i);
// 首次出现位置不同则不同构
if (sPos[sc] != tPos[tc]) {
return false;
}
// 更新首次出现位置为i+1(避免与初始值0冲突)
sPos[sc] = i + 1;
tPos[tc] = i + 1;
}
return true;
}
逻辑说明
- 核心原理:同构字符串的字符“出现规律”完全一致,比如:
- s="egg"的首次出现序列:e(1)、g(2)、g(2);
- t="add"的首次出现序列:a(1)、d(2)、d(2);
- 序列一致则同构;
- 优势:仅需两个数组,无需维护双向映射,代码更简洁;
- 注意点:位置需从1开始(避免初始值0与第一个位置0冲突)。
总结
- 双向哈希表法(第一次解答):逻辑直观,能清晰体现“双向唯一映射”的核心约束,适合新手理解;
- 数组映射法(最优解):性能最优,利用ASCII字符特性避免哈希表开销,空间复杂度O(1),工程首选;
- 首次位置映射法:思路巧妙,代码简洁,无需维护双向映射,适合拓展解题思路;
- 关键技巧:
- 字符类映射问题,若字符范围已知(如ASCII),优先用数组替代HashMap提升性能;
- 同构字符串的核心是“双向唯一映射”,需同时满足“正向唯一”和“反向唯一”;
- 初始值避免冲突:用-1或i+1替代0,防止空字符/第一个位置的误判。