Swift 数据结构与算法(45) + Leetcode205. 同构字符串(双向映射)

109 阅读7分钟

Swift 数据结构与算法( ) + Leetcode 掘金 #日新计划更文活动

题目

205. 同构字符串

给定两个字符串 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 * 104
  • t.length == s.length
  • s 和 t 由任意有效的 ASCII 字符组成

解题思路🙋🏻‍ ♀️

1. 分析题目

题目要求:

给定两个字符串 st,我们需要判断这两个字符串是否是同构的。同构的定义是:如果字符串 s 可以通过一个字符到字符的映射被转换为字符串 t,则它们是同构的。

函数返回值:

函数需要返回一个布尔值:true 表示两个字符串是同构的,false 表示它们不是。

题目类型:

这是一个哈希映射问题。在 LeetCode 上,这种问题通常使用哈希表来解决,因为哈希表可以帮助我们轻松地存储和查找字符之间的映射关系。

2. 解题思路

为了确定两个字符串是否是同构的,我们需要确保:

  1. 对于字符串 s 中的每个字符,它都映射到字符串 t 中的一个特定字符。
  2. 对于字符串 t 中的每个字符,它也映射到字符串 s 中的一个特定字符。

我们可以使用两个哈希表来跟踪这两种映射。

示例:

考虑 s = "paper"t = "title"

[ \begin{align*} s & : \text{"paper"} \ t & : \text{"title"} \end{align*} ]

步骤:

  1. 我们先看字符串 s 的第一个字符 'p'。在哈希表中,我们没有为 'p' 设置映射,所以我们将 'p' 映射到 't'。
  2. 接下来,我们看 'a'。我们再次没有为 'a' 设置映射,所以我们将 'a' 映射到 'i'。
  3. 之后是 'p'。我们已经为 'p' 设置了映射,所以我们检查它是否映射到 't'。因为它确实是,所以我们继续。
  4. 对于 'e',我们将其映射到 'l',对于 'r',我们将其映射到 'e'。

我们还需要确保反过来也是这样。即,'t' 被映射到 'p','i' 被映射到 'a',等等。

由于在整个过程中,我们没有遇到任何不一致的映射,所以这两个字符串是同构的。

哈希映射图:

s -> t
-------------
p -> t
a -> i
p -> t
e -> l
r -> e

t -> s
-------------
t -> p
i -> a
t -> p
l -> e
e -> r

边界思考🤔

代码

func isIsomorphic(_ s: String, _ t: String) -> Bool {
    // 使用两个字典来分别存储从 s 到 t 和从 t 到 s 的字符映射
    var mapS = [Character: Character]()
    var mapT = [Character: Character]()
    
    // 将字符串转为字符数组方便遍历
    let sChars = Array(s)
    let tChars = Array(t)
    
    // 遍历字符数组
    for i in 0..<sChars.count {
        let charS = sChars[i]
        let charT = tChars[i]
        
        // 检查从 s 到 t 的映射是否存在
        if let mappedChar = mapS[charS] {
            if mappedChar != charT { // 如果映射已存在但不匹配当前的字符,则返回 false
                return false
            }
        } else {
            mapS[charS] = charT
        }
        
        // 检查从 t 到 s 的映射是否存在
        if let mappedChar = mapT[charT] {
            if mappedChar != charS { // 如果映射已存在但不匹配当前的字符,则返回 false
                return false
            }
        } else {
            mapT[charT] = charS
        }
    }
    
    // 如果所有字符都匹配,则返回 true
    return true
}

时空复杂度分析

错误与反思

代码中,我们使用了一个单循环,并在每次迭代中处理 st 中的相应字符。例如,当我们在循环的第 i 次迭代时,我们会处理 s[i]t[i]

这样做的好处是,我们可以在单次遍历中完成所有工作,而不需要两次单独的遍历。这不仅使代码更简洁,而且效率更高。

让我给你一个更详细的步骤说明:

  1. 初始化两个映射 mapSmapT

  2. 开始遍历字符串 s

  3. 对于每个字符 charSscharTt

    • 检查 charS 是否已经在 mapS 中有映射。如果有,确保它映射到的是 charT。如果不是,返回 false
    • 如果 charS 还没有映射,将其映射到 charT
    • 使用相同的逻辑处理从 ts 的映射。
  4. 如果循环完成并且没有返回 false,则返回 true

通过这种方式,我们可以确保每对字符都被检查,而无需进行两次独立的遍历。

2.

if let mappedChar = mapS[charS]{ if mappedChar != charT { return false } } else { mapS[charS] = charT } if let mappedChar = mapT[charT]{ if mappedChar != charS { return false } } else { mapT[charT] = charS }

这两段代码是在一个大的循环中,它们的目的是确保双向映射的完整性和唯一性。也就是说,我们不仅要确保从字符串 s 到字符串 t 的映射是唯一的,而且还要确保从字符串 t 到字符串 s 的映射也是唯一的。

  1. 从 s 到 t 的映射:

    • 如果字符 charS 已经在 mapS 中有一个映射,那么我们需要确保这个映射指向的字符与当前遍历到的字符 charT 是相同的。如果不是,则两个字符串不是同构的,直接返回 false
    • 如果字符 charS 还没有映射,我们就在 mapS 中为它创建一个映射,指向字符 charT
  2. 从 t 到 s 的映射:

    • 同理,如果字符 charT 已经在 mapT 中有一个映射,我们需要确保这个映射指向的字符与当前遍历到的字符 charS 是相同的。如果不是,则两个字符串不是同构的,直接返回 false
    • 如果字符 charT 还没有映射,我们就在 mapT 中为它创建一个映射,指向字符 charS

考虑下面的示例来说明为什么需要两个映射:

假设 s = "ab"t = "aa"

仅使用从 st 的映射时,我们得到:

  • a 映射到 a
  • b 映射到 a

此时,我们可能会错误地认为这两个字符串是同构的,因为我们没有检查从 ts 的映射。但是,当我们检查从 ts 的映射时,我们发现:

  • a 既可以映射到 a 又可以映射到 b

这就违反了同构的定义,所以我们需要返回 false

这就是为什么我们需要两次遍历来确保双向映射的唯一性。

概念

可以使用“语言翻译”作为比喻来解释这个方法。

假设正在学习两种全新的外语:语言A和语言B。为了帮助你学习,你决定创建一本小词典,将语言A的词汇映射到语言B的词汇。

当你遇到一个新单词时,你会查看词典以确定是否已经有对应的翻译。如果有,你会确保这个翻译是正确的。但如果没有,你会添加一个新的条目。

这听起来很简单,但有一个附加的挑战:确保翻译是双向一致的。也就是说,当你从语言A翻译到语言B时,反向翻译也应该是正确的。为了确保这一点,你需要另一本词典,将语言B的词汇映射到语言A的词汇。

现在,将这个情境与我们的问题相比较:

  • 字符串s代表语言A,字符串t代表语言B。
  • mapS是从语言A到语言B的词典,mapT是从语言B到语言A的词典。
  • 当我们遍历字符串并检查映射时,这就像在两本词典中查找单词,确保翻译是一致的。

因此,这个方法就像是在创建和检查一个双向词典,确保两种语言之间的每个单词都有一个一致的翻译。这种双向的核实确保了同构的特性。

使用场景与应用

1. 需要学习的概念及其应用到实际场景:

核心概念: 双向映射验证

这一题的核心概念是建立两个字符串之间的映射关系,并确保这种映射在两个方向上都是一致的。只有当两个映射都满足条件时,两个字符串才被认为是同构的。

实际应用场景:

  • 数据同步:当在两个不同的系统或数据库之间同步数据时,你需要确保数据在两边都是一致的。例如,在主数据库和备份数据库之间,或在云服务器和本地服务器之间。

    技术点: 使用双向哈希表或字典来存储和验证数据的映射关系。

  • 双向认证:在安全领域,双方可能需要验证彼此的身份。例如,客户端和服务器之间的SSL握手。

    技术点: 证书和公钥基础设施 (PKI)。

  • 语言翻译软件:在翻译软件中,你可能需要确保从语言A翻译到语言B的结果可以被准确地翻译回语言A。

    技术点: 使用双向字典和机器学习算法来优化翻译。

2. iOS app 开发的实际使用场景:

  • 用户设置同步:当用户在iOS设备上更改设置,并希望这些设置在其他Apple设备上也得到同步时(如iPad,Mac等)。

    技术应用: 使用iCloud来存储用户的设置,并确保在每个设备上的设置都是一致的。可以使用双向映射验证来确保数据的一致性。

  • 双向数据绑定:在某些iOS应用中,用户界面元素的状态可能需要与后台数据模型保持同步。例如,一个开关的状态可能与一个布尔变量绑定。

    技术应用: 使用Swift中的数据绑定技术,如Combine框架或Observable对象。这种双向数据绑定确保了UI和数据模型之间的一致性。

  • 社交应用中的消息同步:当用户在一个设备上发送消息,并希望在其他设备上也能看到这条消息。

    技术应用: 使用WebSocket或其他实时通信技术来同步消息。双向映射验证可以确保消息在所有设备上都是一致的。