LeetCode100天Day10-单词规律与同构字符串:双向映射与字符映射
摘要:本文详细解析了LeetCode中两道经典题目------"单词规律"和"同构字符串"。通过双向映射解决模式匹配问题,以及使用HashMap实现字符映射,帮助读者掌握一一对应关系的处理技巧。
目录
文章目录
- LeetCode100天Day10-单词规律与同构字符串:双向映射与字符映射
-
- 目录
- [1. 单词规律(Word Pattern)](#1. 单词规律(Word Pattern))
-
- [1.1 题目描述](#1.1 题目描述)
- [1.2 解题思路](#1.2 解题思路)
- [1.3 代码实现](#1.3 代码实现)
- [1.4 代码逐行解释](#1.4 代码逐行解释)
- [1.5 执行流程详解](#1.5 执行流程详解)
- [1.6 算法图解](#1.6 算法图解)
- [1.7 复杂度分析](#1.7 复杂度分析)
- [1.8 边界情况](#1.8 边界情况)
- [2. 同构字符串(Isomorphic Strings)](#2. 同构字符串(Isomorphic Strings))
-
- [2.1 题目描述](#2.1 题目描述)
- [2.2 解题思路](#2.2 解题思路)
- [2.3 代码实现](#2.3 代码实现)
- [2.4 代码逐行解释](#2.4 代码逐行解释)
- [2.5 执行流程详解](#2.5 执行流程详解)
- [2.6 算法图解](#2.6 算法图解)
- [2.7 复杂度分析](#2.7 复杂度分析)
- [2.8 边界情况](#2.8 边界情况)
- [3. 两题对比与总结](#3. 两题对比与总结)
-
- [3.1 算法对比](#3.1 算法对比)
- [3.2 双向映射模板](#3.2 双向映射模板)
- [3.3 为什么需要双向映射](#3.3 为什么需要双向映射)
- [3.4 HashMap的containsKey与get](#3.4 HashMap的containsKey与get)
- [3.5 String比较的坑](#3.5 String比较的坑)
- [4. 总结](#4. 总结)
- 参考资源
- 文章标签
1. 单词规律(Word Pattern)
1.1 题目描述
给定一种规律 pattern 和一个字符串 s,判断 s 是否遵循相同的规律。
这里的 遵循 指完全匹配,例如,pattern 里的每个字母和字符串 s 中的每个非空单词之间存在着双向连接的对应规律。具体来说:
pattern中的每个字母都 恰好 映射到s中的一个唯一单词。s中的每个唯一单词都 恰好 映射到pattern中的一个字母。- 没有两个字母映射到同一个单词,也没有两个单词映射到同一个字母。
示例 1:
输入: pattern = "abba", s = "dog cat cat dog"
输出: true
解释: "a"对应"dog","b"对应"cat",满足双向映射
示例 2:
输入: pattern = "abba", s = "dog cat cat fish"
输出: false
解释: "a"不能同时对应"dog"和"fish"
示例 3:
输入: pattern = "aaaa", s = "dog cat cat dog"
输出: false
解释: 不同字母不能映射到同一个单词
1.2 解题思路
这道题的核心是维护双向映射关系:
- 使用两个HashMap,一个记录字母到单词的映射,一个记录单词到字母的映射
- 检查每个字母-单词对是否满足双向一致性
- 如果映射冲突,返回false
解题步骤:
- 将字符串s按空格分割成单词数组
- 检查pattern长度与单词数组长度是否相等
- 遍历pattern,检查每个字符对应单词的映射关系
- 如果出现冲突,返回false
- 遍历结束,返回true
1.3 代码实现
java
class Solution {
public boolean wordPattern(String s, String t) {
String[] t2 = t.split(" ");
if(t2.length != s.length()){
return false;
}
Map<Character,String>s2t = new HashMap<>();
Map<String,Character>t2s = new HashMap<>();
for(int i = 0;i < s.length(); i++){
char a = s.charAt(i);
String b = t2[i];
if((s2t.containsKey(a) && !s2t.get(a).equals(b)) ||
(t2s.containsKey(b) && t2s.get(b) != a)){
return false;
}
s2t.put(a,b);
t2s.put(b,a);
}
return true;
}
}
1.4 代码逐行解释
第一部分:分割字符串与长度检查
java
String[] t2 = t.split(" ");
if(t2.length != s.length()){
return false;
}
功能说明:
| 代码 | 作用 |
|---|---|
t.split(" ") |
按空格分割字符串 |
t2.length |
单词数量 |
s.length() |
pattern字符数量 |
示例:
t = "dog cat cat dog"
t.split(" ") → ["dog", "cat", "cat", "dog"]
t2.length = 4
pattern = "abba"
s.length() = 4
长度相等,继续检查
第二部分:创建双向映射HashMap
java
Map<Character,String>s2t = new HashMap<>();
Map<String,Character>t2s = new HashMap<>();
两个HashMap的作用:
| HashMap | 键类型 | 值类型 | 作用 |
|---|---|---|---|
s2t |
Character | String | 字符 → 单词 |
t2s |
String | Character | 单词 → 字符 |
为什么需要两个HashMap:
只有一个HashMap的情况:
pattern = "abba"
s = "dog dog dog dog"
s2t: {'a' → "dog", 'b' → "dog"}
问题:无法检测到"dog"被两个字符映射!
使用两个HashMap:
pattern = "abba"
s = "dog dog dog dog"
i=0: s2t={'a':"dog"}, t2s={"dog":'a'}
i=1: 'b'映射到"dog"
但t2s中"dog"已映射到'a'
冲突!返回false
第三部分:遍历检查映射
java
for(int i = 0;i < s.length(); i++){
char a = s.charAt(i);
String b = t2[i];
if((s2t.containsKey(a) && !s2t.get(a).equals(b)) ||
(t2s.containsKey(b) && t2s.get(b) != a)){
return false;
}
s2t.put(a,b);
t2s.put(b,a);
}
条件判断详解:
java
s2t.containsKey(a) && !s2t.get(a).equals(b)
| 条件 | 含义 |
|---|---|
s2t.containsKey(a) |
字符a已经映射过 |
!s2t.get(a).equals(b) |
但映射的不是单词b |
java
t2s.containsKey(b) && t2s.get(b) != a
| 条件 | 含义 |
|---|---|
t2s.containsKey(b) |
单词b已经映射过 |
t2s.get(b) != a |
但映射的不是字符a |
1.5 执行流程详解
示例1 :pattern = "abba", s = "dog cat cat dog"
初始状态:
s2t = {}
t2s = {}
t2 = ["dog", "cat", "cat", "dog"]
i=0, a='a', b="dog":
s2t不包含'a'
t2s不包含"dog"
s2t = {'a': "dog"}
t2s = {"dog": 'a'}
i=1, a='b', b="cat":
s2t不包含'b'
t2s不包含"cat"
s2t = {'a': "dog", 'b': "cat"}
t2s = {"dog": 'a', "cat": 'b'}
i=2, a='b', b="cat":
s2t包含'b',s2t.get('b') = "cat"
"cat".equals("cat")? 是,不冲突
t2s包含"cat",t2s.get("cat") = 'b'
'b' == 'b'? 是,不冲突
s2t = {'a': "dog", 'b': "cat"}
t2s = {"dog": 'a', "cat": 'b'}
i=3, a='a', b="dog":
s2t包含'a',s2t.get('a') = "dog"
"dog".equals("dog")? 是,不冲突
t2s包含"dog",t2s.get("dog") = 'a'
'a' == 'a'? 是,不冲突
s2t = {'a': "dog", 'b': "cat"}
t2s = {"dog": 'a', "cat": 'b'}
循环结束,返回 true
示例2 :pattern = "abba", s = "dog cat cat fish"
初始状态:
s2t = {}
t2s = {}
t2 = ["dog", "cat", "cat", "fish"]
i=0, a='a', b="dog":
s2t = {'a': "dog"}
t2s = {"dog": 'a'}
i=1, a='b', b="cat":
s2t = {'a': "dog", 'b': "cat"}
t2s = {"dog": 'a', "cat": 'b'}
i=2, a='b', b="cat":
不冲突,继续
s2t = {'a': "dog", 'b': "cat"}
t2s = {"dog": 'a', "cat": 'b'}
i=3, a='a', b="fish":
s2t包含'a',s2t.get('a') = "dog"
"dog".equals("fish")? 否,冲突!
返回 false
输出: false
1.6 算法图解
pattern = "abba"
s = "dog cat cat dog"
双向映射关系:
字符 单词
'a' ←─────→ "dog"
'b' ←─────→ "cat"
映射检查:
i=0: 'a' → "dog" ✓
i=1: 'b' → "cat" ✓
i=2: 'b' → "cat" ✓
i=3: 'a' → "dog" ✓
结果:true
pattern = "abba"
s = "dog dog dog dog"
双向映射关系:
i=0: 'a' → "dog"
s2t = {'a': "dog"}
t2s = {"dog": 'a'}
i=1: 'b' → "dog"
检查:t2s中"dog"已映射到'a'
但现在要映射到'b'
冲突!
s2t = {'a': "dog", 'b': "dog"}
t2s = {"dog": 'a'}
问题:一个单词对应两个字符!
结果:false
1.7 复杂度分析
| 分析维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | n是pattern长度 |
| 空间复杂度 | O(n) | 存储映射关系 |
1.8 边界情况
| pattern | s | 说明 | 输出 |
|---|---|---|---|
"a" |
"dog" |
单字符 | true |
"ab" |
"dog" |
长度不等 | false |
"ab" |
"dog dog" |
一对多 | false |
"aa" |
"dog cat" |
多对一 | false |
2. 同构字符串(Isomorphic Strings)
2.1 题目描述
给定两个字符串 s 和 t,判断它们是否是同构的。
如果 s 中的字符可以按某种映射关系替换得到 t,那么这两个字符串是同构的。
每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。
示例 1:
输入:s = "egg", t = "add"
输出:true
解释:'e'→'a', 'g'→'d'
示例 2:
输入:s = "foo", t = "bar"
输出:false
解释:'o'不能同时映射到'a'和'r'
示例 3:
输入:s = "paper", t = "title"
输出:true
解释:'p'→'t', 'a'→'i', 'e'→'l', 'r'→'e'
2.2 解题思路
这道题与单词规律类似,也是维护双向映射:
- 使用两个HashMap记录字符映射关系
- 检查每个位置的两个字符是否满足双向一致性
- 如果映射冲突,返回false
解题步骤:
- 创建两个HashMap,分别记录s→t和t→s的映射
- 遍历字符串,检查每个字符对
- 如果映射冲突,返回false
- 遍历结束,返回true
2.3 代码实现
java
class Solution {
public boolean isIsomorphic(String s, String t) {
Map<Character,Character>s2t = new HashMap<>();
Map<Character,Character>t2s = new HashMap<>();
for(int i = 0;i < s.length(); i++){
char a = s.charAt(i);
char b = t.charAt(i);
if((s2t.containsKey(a) && s2t.get(a) != b) ||
(t2s.containsKey(b) && t2s.get(b) != a)){
return false;
}
s2t.put(a,b);
t2s.put(b,a);
}
return true;
}
}
2.4 代码逐行解释
第一部分:创建双向映射
java
Map<Character,Character>s2t = new HashMap<>();
Map<Character,Character>t2s = new HashMap<>();
| HashMap | 键类型 | 值类型 | 作用 |
|---|---|---|---|
s2t |
Character | Character | s的字符 → t的字符 |
t2s |
Character | Character | t的字符 → s的字符 |
第二部分:遍历检查
java
for(int i = 0;i < s.length(); i++){
char a = s.charAt(i);
char b = t.charAt(i);
if((s2t.containsKey(a) && s2t.get(a) != b) ||
(t2s.containsKey(b) && t2s.get(b) != a)){
return false;
}
s2t.put(a,b);
t2s.put(b,a);
}
条件详解:
java
s2t.containsKey(a) && s2t.get(a) != b
| 条件 | 含义 |
|---|---|
s2t.containsKey(a) |
s中的字符a已经映射过 |
s2t.get(a) != b |
但映射的不是t中的字符b |
java
t2s.containsKey(b) && t2s.get(b) != a
| 条件 | 含义 |
|---|---|
t2s.containsKey(b) |
t中的字符b已经映射过 |
t2s.get(b) != a |
但映射的不是s中的字符a |
2.5 执行流程详解
示例1 :s = "egg", t = "add"
初始状态:
s2t = {}
t2s = {}
i=0, a='e', b='a':
s2t不包含'e'
t2s不包含'a'
s2t = {'e': 'a'}
t2s = {'a': 'e'}
i=1, a='g', b='d':
s2t不包含'g'
t2s不包含'd'
s2t = {'e': 'a', 'g': 'd'}
t2s = {'a': 'e', 'd': 'g'}
i=2, a='g', b='d':
s2t包含'g',s2t.get('g') = 'd'
'd' == 'd'? 是,不冲突
t2s包含'd',t2s.get('d') = 'g'
'g' == 'g'? 是,不冲突
s2t = {'e': 'a', 'g': 'd'}
t2s = {'a': 'e', 'd': 'g'}
循环结束,返回 true
输出: true
示例2 :s = "foo", t = "bar"
初始状态:
s2t = {}
t2s = {}
i=0, a='f', b='b':
s2t = {'f': 'b'}
t2s = {'b': 'f'}
i=1, a='o', b='a':
s2t = {'f': 'b', 'o': 'a'}
t2s = {'b': 'f', 'a': 'o'}
i=2, a='o', b='r':
s2t包含'o',s2t.get('o') = 'a'
'a' != 'r'? 是,冲突!
返回 false
输出: false
示例3 :s = "paper", t = "title"
i=0, a='p', b='t':
s2t = {'p': 't'}
t2s = {'t': 'p'}
i=1, a='a', b='i':
s2t = {'p': 't', 'a': 'i'}
t2s = {'t': 'p', 'i': 'a'}
i=2, a='p', b='t':
s2t包含'p',s2t.get('p') = 't'
't' == 't'? 是,不冲突
t2s包含't',t2s.get('t') = 'p'
'p' == 'p'? 是,不冲突
映射保持不变
i=3, a='e', b='l':
s2t = {'p': 't', 'a': 'i', 'e': 'l'}
t2s = {'t': 'p', 'i': 'a', 'l': 'e'}
i=4, a='r', b='e':
s2t = {'p': 't', 'a': 'i', 'e': 'l', 'r': 'e'}
t2s = {'t': 'p', 'i': 'a', 'l': 'e', 'e': 'r'}
循环结束,返回 true
输出: true
2.6 算法图解
s = "egg"
t = "add"
字符对应关系:
位置: 0 1 2
s: e g g
t: a d d
↓ ↓ ↓
映射: e→a g→d g→d
检查:
位置0: e→a, a→e ✓
位置1: g→d, d→g ✓
位置2: g→d, d→g ✓ (与位置1一致)
结果:true
s = "foo"
t = "bar"
字符对应关系:
位置: 0 1 2
s: f o o
t: b a r
↓ ↓ ↓
映射: f→b o→a o→r?
检查:
位置0: f→b, b→f ✓
位置1: o→a, a→o ✓
位置2: o→r, 但o已映射到'a'
'a' != 'r',冲突!
结果:false
2.7 复杂度分析
| 分析维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | n是字符串长度 |
| 空间复杂度 | O(1) | 字符集有限(ASCII) |
2.8 边界情况
| s | t | 说明 | 输出 |
|---|---|---|---|
"a" |
"a" |
单字符 | true |
"ab" |
"aa" |
多对一 | false |
"aa" |
"ab" |
一对多 | false |
"" |
"" |
空字符串 | true |
3. 两题对比与总结
3.1 算法对比
| 对比项 | 单词规律 | 同构字符串 |
|---|---|---|
| 核心算法 | 双向映射 | 双向映射 |
| 数据结构 | 两个HashMap | 两个HashMap |
| 映射类型 | 字符→字符串 | 字符→字符 |
| 时间复杂度 | O(n) | O(n) |
| 空间复杂度 | O(n) | O(1) |
3.2 双向映射模板
java
// 双向映射标准模板
Map<TypeA, TypeB> a2b = new HashMap<>();
Map<TypeB, TypeA> b2a = new HashMap<>();
for(int i = 0; i < length; i++){
TypeA a = getA(i);
TypeB b = getB(i);
// 检查a→b的映射
if(a2b.containsKey(a) && !a2b.get(a).equals(b)){
return false;
}
// 检查b→a的映射
if(b2a.containsKey(b) && !b2a.get(b).equals(a)){
return false;
}
// 建立双向映射
a2b.put(a, b);
b2a.put(b, a);
}
return true;
3.3 为什么需要双向映射
单向映射的问题:
只有s→t的映射:
s = "foo"
t = "bar"
i=0: f→b
s2t = {'f': 'b'}
i=1: o→a
s2t = {'f': 'b', 'o': 'a'}
i=2: o→r
s2t包含'o',映射到'a'
'a' != 'r',检测到冲突 ✓
但在某些情况下:
s = "ab"
t = "aa"
i=0: a→a
s2t = {'a': 'a'}
i=1: b→a
s2t不包含'b',可以映射
s2t = {'a': 'a', 'b': 'a'}
问题:'a'被两个字符映射!
需要反向检查才能发现
双向映射的必要性:
| 检查项 | s→t映射 | t→s映射 |
|---|---|---|
| 一对多 | 无法检测 | 可以检测 |
| 多对一 | 可以检测 | 无法检测 |
| 完全检测 | 需要两者 | 需要两者 |
3.4 HashMap的containsKey与get
java
// 检查映射是否冲突
if(map.containsKey(key) && !map.get(key).equals(value)){
// key已存在,但映射到不同的value
}
// 等价写法
if(map.containsKey(key)){
if(!map.get(key).equals(value)){
// 冲突
}
}
为什么不能只用get:
java
// 错误写法
if(map.get(key) != value){
// 如果key不存在,get返回null
// null != value会误判为冲突
}
3.5 String比较的坑
java
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
// == 比较引用
s1 == s2; // true (字符串常量池)
s1 == s3; // false (不同对象)
// equals比较内容
s1.equals(s2); // true
s1.equals(s3); // true
// 所以必须用equals
if(!s2t.get(a).equals(b)){
// 正确
}
4. 总结
今天我们学习了两道映射匹配题目:
- 单词规律:掌握双向映射处理模式匹配,理解字符串与字符的映射关系
- 同构字符串:掌握字符映射检查,理解双向一致性验证
核心收获:
- 双向映射是解决一一对应问题的关键
- HashMap可以高效建立和检查映射关系
- containsKey配合get可以检测映射冲突
- String比较必须使用equals,而非==
练习建议:
- 尝试用数组代替HashMap(假设字符集是ASCII)
- 思考如果允许一对多映射,应该如何修改
- 尝试找出所有满足条件的映射方案
参考资源
文章标签
#LeetCode #算法 #Java #HashMap #字符串
喜欢这篇文章吗?别忘了点赞、收藏和分享!你的支持是我创作的最大动力!