力扣解题-290. 单词规律
给定一种规律 pattern 和一个字符串 s ,判断 s 是否遵循相同的规律。
这里的 遵循 指完全匹配,例如,pattern 里的每个字母和字符串 s 中的每个非空单词之间存在着双向的一一对应关系。
示例 1:
输入: pattern = "abba", s = "dog cat cat dog"
输出: true
示例 2:
输入: pattern = "abba", s = "dog cat cat fish"
输出: false
示例 3:
输入: pattern = "aaaa", s = "dog cat cat dog"
输出: false
提示:
1 <= pattern.length <= 300
pattern 只包含小写英文字母
1 <= s.length <= 3000
s 只包含小写英文字母和空格 ' '
s 不包含 前导或尾随空格
s 中每个单词都由单个空格分隔
Related Topics
哈希表、字符串
第一次解答(双向哈希表法)
解题思路
核心方法:双向哈希表映射法,通过两个HashMap分别维护pattern字符→s单词和s单词→pattern字符的双向唯一映射关系,确保“相同字符映射唯一单词”且“不同字符不映射到同一单词”,逻辑严谨且能完全满足“一一对应”的核心约束。
核心逻辑拆解
判断单词规律的核心是“字符与单词的双向唯一绑定”:
- 前置校验:将
s按空格分割为单词数组,若数组长度与pattern长度不一致,直接返回false(字符数和单词数不匹配,无法一一对应); - 正向约束:
pattern中的同一个字符,必须始终映射到s中的同一个单词(比如pattern的'a'不能既映射到'dog'又映射到'cat'); - 反向约束:
s中的同一个单词,只能被pattern中的一个字符映射(比如pattern的'a'和'b'不能同时映射到'dog'); - 双Map维护映射:
map:key为pattern的字符,value为s的单词(字符→单词);map2:key为s的单词,value为pattern的字符(单词→字符);
- 遍历验证:
- 若字符已在
map中,检查映射的单词是否与当前单词一致,不一致则返回false; - 若字符不在
map中,检查当前单词是否已被其他字符映射(在map2中),若已映射则返回false; - 若都满足,将字符→单词、单词→字符分别存入两个Map;
- 若字符已在
- 遍历完成后返回true。
性能说明
- 时间复杂度:O(n)(n为
pattern长度,分割字符串O(m) + 遍历O(n),m为s长度,整体仍为线性),耗时1ms击败99.29%用户; - 空间复杂度:O(k)(k为不同字符/单词的数量,最多等于
pattern长度); - 优势:逻辑直观,双向Map能清晰体现“一一对应”的约束,新手易理解;
- 内存损耗点:两个HashMap的存储开销 + 字符串分割生成的单词数组,导致内存消耗42.3MB仅击败10.71%用户。
public boolean wordPattern(String pattern, String s) {
String [] sArray=s.split(" ");
if(pattern.length()!=sArray.length){
return false;
}
boolean result=true;
Map<Character,String> map = new HashMap<>();
Map<String,Character> map2 = new HashMap<>();
for(int i=0;i<pattern.length();i++){
char a=pattern.charAt(i);
if(map.containsKey(a)){
if(!map.get(a).equals(sArray[i])){
result=false;
break;
}
}else {
if(map2.containsKey(sArray[i])){
result=false;
break;
}else {
map.put(a,sArray[i]);
map2.put(sArray[i],a);
}
}
}
return result;
}
第二次解答(首次位置映射法)
解题思路
核心方法:首次出现位置统一映射法,将“字符→单词”的双向映射转化为“字符/单词的首次出现位置”比对,用数组记录字符的首次位置、HashMap记录单词的首次位置,通过“位置是否一致”判断是否满足规律,减少映射维护的开销,内存效率大幅提升。
核心逻辑拆解
规律匹配的本质是“字符和单词的出现模式完全一致”,比如:
pattern="abba"的首次出现位置:a(1)、b(2)、b(2)、a(1);s="dog cat cat dog"的首次出现位置:dog(1)、cat(2)、cat(2)、dog(1);- 位置序列完全一致则满足规律。
具体步骤:
- 前置校验:分割
s为单词数组,长度不一致直接返回false; - 位置记录容器:
sPos数组(长度256):记录pattern中每个字符的首次出现位置(字符ASCII码为下标,初始值0);mapPosHashMap:记录s中每个单词的首次出现位置(初始无记录时返回0);
- 遍历比对:
- 取当前
pattern字符pc和s单词sc; - 若
pc的首次位置(sPos[pc])与sc的首次位置(mapPos.getOrDefault(sc,0))不一致,返回false; - 若一致,更新两者的首次位置为
i+1(避免与初始值0冲突);
- 取当前
- 遍历完成返回true。
性能优化点
- 时间复杂度:仍为O(n),但减少了HashMap的“存值/取值”比对操作,仅需比对位置数值;
- 空间复杂度:O(k)(k为不同单词数),但数组替代了一个HashMap,内存开销降低;
- 内存表现:42.02MB击败70.57%用户,相比双Map法内存效率显著提升;
- 核心优势:无需维护双向映射关系,仅通过“位置一致性”判断,逻辑更简洁,减少了Map的equals比对开销。
public boolean wordPattern(String pattern, String s) {
String [] sArray=s.split(" ");
if(pattern.length()!=sArray.length){
return false;
}
int n = sArray.length;
// 记录字符首次出现的位置(初始为0)
int[] sPos = new int[256];
Map<String,Integer> mapPos=new HashMap<>();
for (int i = 0; i < n; i++) {
char pc = pattern.charAt(i);
String sc = sArray[i];
// 首次出现位置不同则不同构
if (sPos[pc] != mapPos.getOrDefault(sc,0)) {
return false;
}
// 更新首次出现位置为i+1(避免与初始值0冲突)
sPos[pc] = i + 1;
mapPos.put(sc,i+1);
}
return true;
}
示例解答
解题思路
解法1:纯数组优化版(极致性能)
核心方法:单词→数字编码 + 数组映射,先将s的单词映射为数字(如第一个单词→1,第二个新单词→2),再用两个数组分别记录pattern字符和s单词的编码,通过编码一致性判断规律,完全避免HashMap的使用,性能达到最优。
核心逻辑
- 分割
s为单词数组,长度不一致返回false; - 用
wordToCodeHashMap将单词映射为唯一数字编码(仅用于临时编码); - 初始化两个数组
patternCode(长度300)、wordCode(长度300); - 遍历过程:
- 对每个单词,若未编码则分配递增编码(从1开始);
- 将
pattern字符转为编码(如'a'→0, 'b'→1); - 记录
pattern字符编码和单词编码到对应数组; - 若两者编码不一致,返回false;
- 遍历完成返回true。
代码实现
public boolean wordPattern(String pattern, String s) {
String[] words = s.split(" ");
if (pattern.length() != words.length) {
return false;
}
int n = pattern.length();
// 单词→数字编码(避免重复单词)
Map<String, Integer> wordToCode = new HashMap<>();
int[] patternCode = new int[n];
int[] wordCodeArr = new int[n];
int code = 1;
for (int i = 0; i < n; i++) {
// pattern字符编码(a→0, b→1...)
patternCode[i] = pattern.charAt(i) - 'a';
// 单词编码(首次出现分配新编码)
String word = words[i];
if (!wordToCode.containsKey(word)) {
wordToCode.put(word, code++);
}
wordCodeArr[i] = wordToCode.get(word);
// 编码不一致则不满足规律
if (patternCode[i] != wordCodeArr[i] - 1) { // 对齐编码起始值
return false;
}
}
return true;
}
优势说明
- 时间复杂度:O(n),HashMap仅用于单词编码,且仅做“是否存在”判断,开销极小;
- 空间复杂度:O(k)(k为不同单词数),数组访问速度远超HashMap;
- 极致性能:完全基于数组比对,无字符串equals操作,是大数据量下的最优选择。
解法2:无分割优化版(减少内存开销)
核心方法:遍历字符串不分割,通过双指针直接遍历s的单词(避免分割生成单词数组),进一步降低内存开销,适合s长度极大的场景。
代码实现
public boolean wordPattern(String pattern, String s) {
int pLen = pattern.length();
int sLen = s.length();
int pIdx = 0, sIdx = 0;
Map<Character, String> charToWord = new HashMap<>();
Map<String, Character> wordToChar = new HashMap<>();
while (pIdx < pLen && sIdx < sLen) {
// 取pattern当前字符
char c = pattern.charAt(pIdx);
// 双指针找当前单词的结束位置
int end = sIdx;
while (end < sLen && s.charAt(end) != ' ') {
end++;
}
String word = s.substring(sIdx, end);
sIdx = end + 1; // 移动到下一个单词起始位置
// 双向映射校验
if (charToWord.containsKey(c)) {
if (!charToWord.get(c).equals(word)) {
return false;
}
} else {
if (wordToChar.containsKey(word)) {
return false;
}
charToWord.put(c, word);
wordToChar.put(word, c);
}
pIdx++;
}
// 校验是否都遍历完成(避免pattern遍历完但s还有单词,或反之)
return pIdx == pLen && sIdx > sLen;
}
优势说明
- 内存优化:无需生成单词数组,直接遍历字符串截取单词,节省数组存储的内存;
- 提前终止:遍历过程中若发现不匹配可立即终止,无需分割整个字符串;
- 适用场景:
s长度极大(如接近3000)时,避免分割生成大数组,内存效率更高。
总结
- 双向哈希表法:逻辑直观,清晰体现“一一对应”约束,适合新手理解核心原理;
- 首次位置映射法:减少Map比对开销,内存效率更高,是平衡性能和可读性的优选;
- 纯数组优化版:极致性能,无字符串equals操作,适合大数据量场景;
- 无分割优化版:减少内存开销,避免生成单词数组,适合超长字符串场景;
- 关键技巧:
- 前置校验(长度一致)是必做步骤,可快速过滤无效用例;
- 字符类映射优先用数组(ASCII范围)替代HashMap,提升性能;
- 避免不必要的字符串操作(如分割、equals),可大幅降低内存/时间开销。