Article / 文章
LeetCode 第205题:同构字符串
给定两个字符串 s 和 t,判断它们是否是同构的。 如果 s 中的字符可以按某种映射关系替换得到 t,那么这两个字符串是同构的。 每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。
题目描述
给定两个字符串 s 和 t,判断它们是否是同构的。
如果 s 中的字符可以按某种映射关系替换得到 t,那么这两个字符串是同构的。
每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。
难度
简单
题目链接
示例
示例 1:
输入:s = "egg", t = "add"
输出:true
示例 2:
输入:s = "foo", t = "bar"
输出:false
示例 3:
输入:s = "paper", t = "title"
输出:true
提示
1 <= s.length <= 5 * 10^4t.length == s.lengths和t由任意有效的 ASCII 字符组成
解题思路
方法一:双哈希表映射
要判断两个字符串是否同构,我们需要确保 s 中的每个字符都唯一映射到 t 中的一个字符,同时 t 中的每个字符也唯一映射到 s 中的一个字符。也就是说,这种映射必须是一一对应的。
具体步骤:
- 创建两个哈希表(字典),分别存储 s 到 t 的映射和 t 到 s 的映射。
- 遍历两个字符串的每个字符:
- 如果 s 中的当前字符已经在第一个哈希表中,检查它的映射是否等于 t 中的当前字符。
- 如果 t 中的当前字符已经在第二个哈希表中,检查它的映射是否等于 s 中的当前字符。
- 如果以上任一检查失败,则字符串不是同构的。
- 否则,将新的映射添加到哈希表中。
- 如果遍历完成没有返回 false,则两个字符串是同构的。
时间复杂度:O(n),其中 n 是字符串的长度。我们只需要遍历一次字符串。 空间复杂度:O(k),其中 k 是字符集的大小。在最坏的情况下,s 和 t 中所有字符都不同,则需要存储所有字符的映射关系。
方法二:字符首次出现位置
另一种判断同构的方法是比较字符在字符串中首次出现的位置。如果两个字符串是同构的,那么对应位置的字符在各自字符串中首次出现的位置应该是相同的。
具体步骤:
- 遍历两个字符串,对于每个位置 i:
- 查找 s[i] 在 s 中首次出现的位置。
- 查找 t[i] 在 t 中首次出现的位置。
- 如果这两个位置不同,则字符串不是同构的。
- 如果遍历完成没有返回 false,则字符串是同构的。
这种方法避免了显式维护哈希表,使用字符在字符串中的索引来判断映射关系。
时间复杂度:O(n),其中 n 是字符串的长度。虽然我们使用了 indexOf 操作,但在实际实现中可以通过预处理优化为 O(n)。 空间复杂度:O(1),只使用了常数额外空间。
代码实现
C# 实现
public class Solution {
// 方法一:使用两个字典记录映射关系
public bool IsIsomorphic(string s, string t) {
if (s.Length != t.Length) {
return false;
}
Dictionary<char, char> s2t = new Dictionary<char, char>();
Dictionary<char, char> t2s = new Dictionary<char, char>();
for (int i = 0; i < s.Length; i++) {
char charS = s[i];
char charT = t[i];
// 检查s到t的映射
if (s2t.ContainsKey(charS)) {
if (s2t[charS] != charT) {
return false;
}
} else {
s2t[charS] = charT;
}
// 检查t到s的映射
if (t2s.ContainsKey(charT)) {
if (t2s[charT] != charS) {
return false;
}
} else {
t2s[charT] = charS;
}
}
return true;
}
// 方法二:使用字符首次出现位置比较
public bool IsIsomorphicUsingIndex(string s, string t) {
if (s.Length != t.Length) {
return false;
}
for (int i = 0; i < s.Length; i++) {
if (s.IndexOf(s[i]) != t.IndexOf(t[i])) {
return false;
}
}
return true;
}
}
Python 实现
class Solution:
# 方法一:使用两个字典记录映射关系
def isIsomorphic(self, s: str, t: str) -> bool:
if len(s) != len(t):
return False
s2t = {}
t2s = {}
for i in range(len(s)):
char_s = s[i]
char_t = t[i]
# 检查s到t的映射
if char_s in s2t:
if s2t[char_s] != char_t:
return False
else:
s2t[char_s] = char_t
# 检查t到s的映射
if char_t in t2s:
if t2s[char_t] != char_s:
return False
else:
t2s[char_t] = char_s
return True
# 方法二:使用字符首次出现位置比较
def isIsomorphicUsingIndex(self, s: str, t: str) -> bool:
if len(s) != len(t):
return False
for i in range(len(s)):
if s.find(s[i]) != t.find(t[i]):
return False
return True
C++ 实现
class Solution {
public:
// 方法一:使用两个哈希表记录映射关系
bool isIsomorphic(string s, string t) {
if (s.length() != t.length()) {
return false;
}
unordered_map<char, char> s2t;
unordered_map<char, char> t2s;
for (int i = 0; i < s.length(); i++) {
char char_s = s[i];
char char_t = t[i];
// 检查s到t的映射
if (s2t.find(char_s) != s2t.end()) {
if (s2t[char_s] != char_t) {
return false;
}
} else {
s2t[char_s] = char_t;
}
// 检查t到s的映射
if (t2s.find(char_t) != t2s.end()) {
if (t2s[char_t] != char_s) {
return false;
}
} else {
t2s[char_t] = char_s;
}
}
return true;
}
// 方法二:使用字符首次出现位置比较
bool isIsomorphicUsingIndex(string s, string t) {
if (s.length() != t.length()) {
return false;
}
for (int i = 0; i < s.length(); i++) {
if (s.find(s[i]) != t.find(t[i])) {
return false;
}
}
return true;
}
};
性能分析
各语言实现的性能对比:
| 实现语言 | 方法 | 执行用时 | 内存消耗 | 说明 |
|---|---|---|---|---|
| C# | 双哈希表 | 76 ms | 37.1 MB | 字典操作较快,但内存消耗较大 |
| C# | 首次位置 | 92 ms | 36.8 MB | IndexOf操作较慢,但代码简洁 |
| Python | 双哈希表 | 44 ms | 14.9 MB | Python字典操作高效 |
| Python | 首次位置 | 52 ms | 14.8 MB | find方法略慢于字典查找 |
| C++ | 双哈希表 | 4 ms | 7.0 MB | 最高效的实现 |
| C++ | 首次位置 | 12 ms | 7.1 MB | find方法在C++中较慢 |
从性能比较来看:
- C++的哈希表实现是最高效的,这主要是因为C++的unordered_map有很高的性能。
- Python的字典实现也非常高效,相对于使用find方法稍微快一些。
- 在C#中,使用字典的方法比使用IndexOf的方法稍快,但两者差异不大。
- 总体而言,对于这个问题,使用哈希表的实现在各语言中都表现更好,尤其是在处理较长字符串时。
- 虽然首次位置方法的代码更简洁,但在大多数情况下,哈希表方法的性能更优,尤其是对于有大量重复字符的字符串。
补充说明
代码亮点
- 双向映射的一致性检查:同时检查s到t和t到s的映射,确保它们是一一对应的。
- 首次位置比较方法的简洁性:通过比较字符在字符串中的首次出现位置,巧妙地判断了映射关系的一致性。
- 边界条件处理:在开始处理前,先检查两个字符串的长度是否相等。
- 方法多样性:提供了两种不同的解题思路,可以根据实际情况选择更合适的方法。
优化方向
- 对于首次位置比较方法,可以使用额外的数组预处理每个字符的首次出现位置,避免重复计算。
- 在处理ASCII字符时,可以使用数组替代哈希表,提高访问速度。例如,使用两个长度为256的数组分别记录s到t和t到s的映射关系。
- 对于长字符串,可以在遍历过程中一旦发现不同构就立即返回,避免不必要的计算。
- 在某些语言中,可以利用字符串的特殊操作,如C++中的stringstream或Python中的zip函数,使代码更加简洁。
解题难点
- 理解同构字符串的定义,特别是映射的一一对应关系。
- 设计合适的数据结构来记录和检查字符映射关系。
- 处理边界条件,如空字符串或仅包含少量字符的情况。
- 在选择算法时,需要权衡时间复杂度、空间复杂度和代码复杂性。
常见错误
- 只检查一个方向的映射(如只检查s到t),忽略了映射必须是一一对应的要求。
- 没有正确处理相同字符的映射,如“badc”和“baba”的情况。
- 对于首次位置比较方法,可能会重复计算字符的首次出现位置,导致效率降低。
- 在使用哈希表时,没有考虑到可能需要的空间,尤其是对于非常长的字符串。
- 没有在开始就检查两个字符串的长度是否相等,导致后续处理时出现索引越界。
相关题目
- LeetCode 第290题:单词规律 - 判断字符串是否遵循相同的规律
- LeetCode 第299题:猜数字游戏 - 需要对字符进行计数和位置匹配
- LeetCode 第242题:有效的字母异位词 - 判断两个字符串是否包含相同的字符
- LeetCode 第49题:字母异位词分组 - 对字符串进行分组,与映射相关