LeetCode 290简单哈希
单词规律 图解题解
这道题到底在问什么
给定规律 pattern 和字符串 s,判断 s 是否遵循相同的规律。这里「遵循」指完全匹配:pattern 里的每个字母与 s 里按空格分出的每个单词之间,存在双向一一对应。
- pattern
- "abba"
- s
- "dog cat cat dog"
- 输出
- true
- 解释
- a↔dog, b↔cat,前后一致
最优解:一步一步想明白
- 3不够! 比如 pattern=ab、s=dog dog:只看「字母→单词」,a→dog、b→dog 都没矛盾,会误判成 true。但 dog 同时被 a 和 b 占了,违反一一对应。所以必须两个方向都记。
- 4也能做:把相同字母的下标聚成组、相同单词的下标聚成组,再看两套分组是否一模一样。但要建两套分组结构、再逐组比对,写起来啰嗦。其实一遍扫就够了。
- 5核心:一边扫一边记两份对应。c2w 管「字母指向哪个单词」,w2c 管「单词指向哪个字母」。两张表互相把关,任何一边和历史记录打架就立刻 false。
- 6对每一对字母-单词 (c, w):① c 之前记过且记的不是 w → 矛盾 false;② w 之前记过且记的不是 c → 矛盾 false;都过关就把 c→w、w→c 各登记一次。扫完没翻车就是 true。
- 7pattern 4 字母 · s 切出 4 个单词把 s 按空格切成单词表 [dog, cat, cat, dog],正好 4 个,和 pattern 的 4 个字母一一对齐(格子下方小数字是下标)。下面会逐对处理,并在底下长出 c2w / w2c 两张哈希表。
- 8len(pattern)=4 == 单词数=4第一道关:字母个数必须等于单词个数。这里都是 4,过关(全亮)。如果一句话单词比字母多或少,根本对不上,直接 false。
- 9a↔dog, b↔cat提前剧透结果:扫完后字母 a(下标0、3)全程对 dog、b(下标1、2)全程对 cat,反过来也成立,没有任何冲突。下面看两张表怎么一对一对建起来。
- 10c2w 查 a → 空第一对:字母 a(下标0) 配单词 dog。先查正向表 c2w:里头没有 a,说明 a 还没被绑定,先放行。
- 11w2c 查 dog → 空再查反向表 w2c(切到单词→字母这张):dog 也没被任何字母占用。两个方向都查过、都是空的,这一对可以放心登记。
- 12c2w[a]=dog · w2c[dog]=a都没冲突,两个方向各登记一次:c2w 加「a → dog」(绿色高亮),w2c 同步记「dog → a」。下标0 锁定,a 和 dog 绑死。
- 13c2w 查 b → 空第二对:字母 b(下标1) 配 cat。查正向 c2w,只有 a→dog 那行,没有 b,b 还是新的。
- 14w2c 查 cat → 空切到反向表 w2c:现在它有 dog→a 一行,查 cat 没有,说明 cat 还没被占用。两边都查空,这对可登记。
- 15c2w[b]=cat · w2c[cat]=b登记第二组:c2w 新增「b → cat」(高亮),w2c 同步记「cat → b」。下标0、1 都锁定,a、b 各有归属。
- 16c2w[b]=cat == cat第三对又是 b(下标2) 配 cat。查正向 c2w:b 已经在(蓝色高亮),记的是 cat,正好等于当前单词——正向一致。
- 17w2c[cat]=b == b再查反向 w2c:cat 这行(高亮)记的是 b,也正好等于当前字母。两个方向都对得上,记录已存在且一致,无需新增。
- 18已存在且一致 · 继续因为 b、cat 早绑好了,这对只是复查通过,两张表不动,下标2 确认对齐(已点亮 0、1、2)。这正是「同字母必须对同单词」。
- 19c2w[a]=dog == dog最后一对 a(下标3) 配 dog。查正向 c2w:a 那行(高亮)记的是 dog,等于当前单词——正向一致。
- 20w2c[dog]=a == a查反向 w2c:dog 这行(高亮)记的是 a,等于当前字母。两方向都对得上,最后一对也复查通过。
- 21返回 true四对都没翻车(下标 0、1、2、3 全亮),两张表最终是 a↔dog、b↔cat 的干净双向映射。返回 true,和示例答案完全对上。
- 22把最后一个单词换成 fish 看看会怎样。前三对和刚才完全一样(a→dog, b→cat, b→cat 复查通过),问题出在最后一对。
- 23a→dog, b→cat 已建反例 s="dog cat cat fish":头两对 (a,dog)、(b,cat) 跟正例一模一样,两张表照常建起 a→dog、b→cat(下标0、1 点亮)。问题还没暴露。
- 24和正例一致第三对还是 (b, cat),和正例一样复查通过(下标2 点亮)。三对都没事——坑在最后一对,因为最后一个单词被换成了 fish。
- 25c2w[a]=dog ≠ fish最后一对是 a 配 fish。查 c2w 发现 a 早就绑死 dog(高亮行),可现在要它对 fish——同一个字母不能对两个不同单词,直接返回 false。这就是正向表把关的作用。
- 26前面那个 fish 冲突,单靠正向表就能抓到。但有一种情况只有反向表才拦得住——开头提的 ab/dog dog。下面演给你看。
- 27c2w[a]=dog · w2c[dog]=apattern=ab、s="dog dog"。第一对 (a, dog) 正常登记:c2w 记 a→dog,反向 w2c 记下 dog→a(关键就在这条反向记录)。
- 28w2c[dog]=a ≠ b第二对 (b, dog):正向查 c2w,b 是新的(只看正向会放过!)。但切到反向表 w2c 一查——dog 这行(高亮)早记了 a,dog 已被 a 占用,b 抢不走 → false。这就是必须有反向表的原因。
- 31一一对应是双向约束:字母不能对多个单词(正向表管),单词也不能被多个字母占(反向表管)。只记一张会漏掉「不同字母指向同一单词」这种(如 ab→dog dog),所以两张缺一不可。
- 33ab 配 "dog dog" 误判如果只建正向 c2w:a→dog、b→dog 各自都没冲突,程序高高兴兴返回 true。可 dog 被两个字母占了,根本不是一一对应。补上反向表 w2c,第二对就会因 dog 已属 a 而正确返回 false。
⚠️ 容易写错的地方
✗ 错:只建「字母→单词」一张表
✓ 对:正反两张表都建
漏掉「两字母共用一单词」,如 ab 配 dog dog 会误判 true
✗ 错:忘了先比长度
✓ 对:先判 len 相等
字母数与单词数不等根本不可能一一对应,先挡掉
✗ 错:Java 用 == 比较单词字符串
✓ 对:用 .equals 比较
== 比的是引用,内容相同也可能 false
完整代码(Python / C++ / Java)
Python
class Solution:
def wordPattern(self, pattern, s):
words = s.split()
if len(pattern) != len(words):
return False
c2w, w2c = {}, {} # 两张哈希表
for c, w in zip(pattern, words):
if c in c2w and c2w[c] != w:
return False
if w in w2c and w2c[w] != c:
return False
c2w[c] = w
w2c[w] = c
return TrueC++
class Solution {
public:
bool wordPattern(string pattern, string s) {
vector<string> words;
stringstream ss(s);
string w;
while (ss >> w) words.push_back(w);
if (pattern.size() != words.size()) return false;
unordered_map<char,string> c2w;
unordered_map<string,char> w2c;
for (int i = 0; i < (int)pattern.size(); i++) {
char c = pattern[i]; string ww = words[i];
if (c2w.count(c) && c2w[c] != ww) return false;
if (w2c.count(ww) && w2c[ww] != c) return false;
c2w[c] = ww; w2c[ww] = c;
}
return true;
}
};Java
class Solution {
public boolean wordPattern(String pattern, String s) {
String[] words = s.split(" ");
if (pattern.length() != words.length) return false;
Map<Character,String> c2w = new HashMap<>();
Map<String,Character> w2c = new HashMap<>();
for (int i = 0; i < pattern.length(); i++) {
char c = pattern.charAt(i);
String w = words[i];
if (c2w.containsKey(c) && !c2w.get(c).equals(w))
return false;
if (w2c.containsKey(w) && w2c.get(w) != c)
return false;
c2w.put(c, w);
w2c.put(w, c);
}
return true;
}
}复杂度
时间复杂度
O(n)
n 为 pattern 长度(=单词数),每对做常数次哈希查与写
空间复杂度
O(n)
两张哈希表最多各存 n 条映射
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 单词规律 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
和 LC205 同构字符串有什么关系?+
本质完全相同,都是「两个序列元素间的双向一一映射」。LC205 映射的是字符↔字符,本题映射的是字符↔单词。解法都是开两张哈希表、逐对双向校验,O(n)。
只用一张表,能不能靠某种技巧搞定?+
可以用一个巧技:把字母和单词都映射到「首次出现的下标」,两序列同位置的下标序列若一致即匹配。但开两张表更直观、更不易错,面试推荐两表法。
如果单词里可能有多个连续空格怎么办?+
用 split 时要注意:Python 的 s.split()(无参)会自动合并连续空格,但 Java 的 split(" ") 会切出空串。按题意单词由单个空格分隔,需要时改用更严格的切分或正则。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 单词规律 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。