题目描述
给定两个字符串 s 和 t ,判断它们是否是同构的。
如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。
每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。
示例 1:
//
//输入:s = “egg”, t = “add”
//输出:true
//示例 2:
//
//输入:s = “foo”, t = “bar”
//输出:false
解题思路
要判断两个字符串是否是同构的,我们需要确保:
从字符串 s 到 t 的字符映射在整个字符串中是一致的。
不能有两个不同的字符在 s 中映射到 t 中的同一个字符。
我们可以使用两个哈希表(或字典)来跟踪字符映射:
一个哈希表用来记录从 s 到 t 的映射关系。
另一个哈希表用来记录从 t 到 s 的映射关系。
解题步骤
1.首先检查 s 和 t 的长度是否相等。如果不相等,则它们不可能是同构的,直接返回 false。
2.同时遍历字符串 s 和 t 中的字符。
3.对于每对字符 s[i] 和 t[i]:
4.如果 s[i] 已经在哈希表中映射过,检查其映射的字符是否为 t[i]。如果不是,返回 false。
5.如果 t[i] 已经在哈希表中映射过,检查其映射的字符是否为 s[i]。如果不是,返回 false。
6.如果两个字符都没有被映射过,则在两个哈希表中分别添加对应的映射关系。
7.如果遍历完整个字符串没有发现不一致的映射关系,则返回 true。
代码实现
func isIsomorphic(s string, t string) bool {
if len(s) != len(t) {
return false
}
// 用两个map来记录
// 这里为什么用byte
mapS := make(map[byte]byte)
mapT := make(map[byte]byte)
// 同时遍历
for i := 0; i < len(s); i++ {
charS := s[i]
charT := t[i]
// 检查s到t的映射是否存在且一致
if mappendChar, ok := mapS[charS]; ok {
if mappendChar != charT {
return false
}
} else {
mapS[charS] = charT
}
// 检查t到s的映射是否存在且一致
if mappendChar, ok := mapT[charT]; ok {
if mappendChar != charS {
return false
}
} else {
mapT[charT] = charS
}
}
return true
}
代码测试
func main() {
// 测试用例
testCases := []struct {
s string
t string
expected bool
}{
{"egg", "add", true},
{"foo", "bar", false},
{"paper", "title", true},
{"ab", "aa", false},
{"a", "a", true},
}
for _, tc := range testCases {
result := isIsomorphic(tc.s, tc.t)
fmt.Printf("isIsomorphic(%q, %q) = %v, expected = %v\n", tc.s, tc.t, result, tc.expected)
}
}
测试结果
关于题目疑问
Q1 为什么map用byte类型来构建
- 字符串的底层表示:
在 Go 语言中,字符串实际上是一个字节序列([]byte),每个字符都是一个 byte(即 8 位无符号整数,范围是 0-255)。
符串索引访问: 当我们访问字符串的某个字符时,例如 s[i],返回的是一个 byte 类型的数据。这是因为字符串在 Go 中是以 UTF-8 编码存储的,每个字符(如果是 ASCII 字符)可以直接用 byte 表示。
- 字节类型的高效性:
使用 byte 作为键值有以下几个优势:
性能优化: byte 类型只占用 1 字节(8 位),而 int 或 rune 类型可能会占用更多的内存(例如 int 通常是 4 字节或 8 字节)。在处理大量字符映射时,使用 byte 可以减少内存占用,并提高查找和插入操作的效率。
避免多余的类型转换: 如果直接使用 s[i] 作为键,它已经是 byte 类型,避免了将其转换为 rune 或 int 的开销。
- 与 UTF-8 兼容:
Go 的字符串是以 UTF-8 编码的字节序列存储的,UTF-8 的每个字符占用 1 到 4 个字节。对 ASCII 字符(范围在 0 到 127)的操作,使用 byte 类型已经足够,因为 ASCII 字符只占用 1 个字节。对于更复杂的 Unicode 字符,这个算法假设输入的字符串由简单的单字节字符组成(如英文字母),因此使用 byte 是合适且高效的。
总结
在这个算法中使用 byte 作为 map 的键类型主要是因为:
字符串的每个字符在 Go 中可以直接被表示为 byte 类型(如果字符是 ASCII 字符)。
byte 类型的存储和处理更加高效,节省了内存和计算资源。
避免了不必要的类型转换,提高了代码的性能和可读性。
因此,使用 byte 作为键类型是一个符合 Go 语言设计理念的选择,特别是在处理字符串时。
Q2 为什么这样能判断同构?
同构字符串定义
两个字符串是同构的,如果 s 中的字符可以通过某种一对一的映射关系被替换为 t 中的字符,且所有字符的出现顺序必须保持一致。
例子分析
举例来说,abb 和 egg 是同构的,因为可以通过如下映射来匹配:
a -> e
b -> g
这意味着字符串 s 中的每个字符都可以找到一个对应的字符 t,并且这个映射在整个字符串中是一致的。
为什么这样能判断同构?
为了判断两个字符串是否同构,我们需要满足以下条件:
一致性:字符串 s 中的每个字符只能映射到 t 中的一个唯一字符。
唯一性:t 中的每个字符只能对应 s 中的一个唯一字符。
详细分析
以 abb 和 egg 为例:
遍历第一个字符:a 和 e。a 映射到 e,建立映射关系 a -> e 和 e -> a。
遍历第二个字符:b 和 g。b 映射到 g,建立映射关系 b -> g 和 g -> b。
遍历第三个字符:b 和 g。已经有映射 b -> g,检查是否一致,结果一致,因此通过。
因为在整个过程中,s 中的字符映射到 t 中的字符是一致的,同时没有冲突的映射关系,所以可以判断 abb 和 egg 是同构的。
如果我们换一个例子,比如 foo 和 bar:
f 映射到 b,建立映射关系 f -> b 和 b -> f。
o 映射到 a,建立映射关系 o -> a 和 a -> o。
遍历第三个字符时,o 已经有映射为 a,但是第三个字符 r 并不是 a,所以 foo 和 bar 不是同构的。
关键点
同构的关键在于字符的映射:
字符映射的唯一性(一个字符只能映射到一个字符)。
字符映射的一致性(同一个字符在不同位置的映射必须一致)。
如果两个字符串在这两个条件下都满足一致的映射关系,那么这两个字符串就是同构的。
通过这种方式,我们可以很容易地判断两个字符串是否同构。
测试不通过修改
isIsomorphic(“badc”, “baba”) 结果为true
func isIsomorphic(s string, t string) bool {
if len(s) != len(t) {
return false
}
// 两个哈希表分别记录从 s 到 t 和从 t 到 s 的映射
mapS := make(map[byte]byte)
mapT := make(map[byte]byte)
for i := 0; i < len(s); i++ {
charS := s[i]
charT := t[i]
// 检查是否存在从 s 到 t 的映射
if mappedChar, ok := mapS[charS]; ok {
// 如果存在映射,但不匹配,则返回 false
if mappedChar != charT {
return false
}
} else {
// 在设置映射之前,确保 t[i] 没有被映射到其他字符
if mappedChar, ok := mapT[charT]; ok && mappedChar != charS {
return false
}
// 建立 s 到 t 的映射
mapS[charS] = charT
}
// 检查是否存在从 t 到 s 的映射
if mappedChar, ok := mapT[charT]; ok {
// 如果存在映射,但不匹配,则返回 false
if mappedChar != charS {
return false
}
} else {
// 建立 t 到 s 的映射
mapT[charT] = charS
}
}
return true
}