剑指offer-52:正则表达式匹配
题目描述
请实现一个函数用来匹配包括 ' . ' 和 ' * ' 的正则表达式。模式中的字符 ' . ' 表示任意一个字符,而 ' * ' 表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 " aaa " 与模式 " a.a " 和 " abaca " 匹配,但是与 " aa.a " 和 " ab*a " 均不匹配
解题思路
做这题的时候,我花了不少时间。一开始我用两个indexes循环来做题,但遇到 ' * ' pattern 的时候总是无法满足所有情况。
最后发现,有两种比较方便的方法来做这题,递归和动态规划。
方法一:递归
递归方法比动态规划更好理解。通过不断的递归,缩小 str 和 pattern 需要判断的长度,来判断最终是否匹配。
根据我的解题思路,递归有两个base case:
- str 和 pattern 同时匹配完,即 strIdx == str.length && patIdx == pattern.length。此时返回 true。
- pattern 先匹配完,但 str 还有剩余。此时返回 false。
(当 str 先匹配完的时候,如果剩余 pattern 全是 ' _* ' 这种形式,仍然返回 true,否则false)
其余情况,让当前所指的 str 与 pattern 对比:
- str[strIdx] == pattern[patIdx] 或 pattern[patIdx] == ' . ' :
- 如果 pattern[patIdx + 1] 不等于 ' * ',则 strIdx 和 patIdx 同时加一,后移一项。
- 如果等于,又会有三种情况:
- ab | a*ab,patIdx 后移一位,strIdx 保持不变
- aab | a*b,strIdx 后移一位,patIdx 保持不变
- ab | a*b, strIdx 和 patIdx 同时后移一位
(第一种和第二种情况在递归的时候会包含第三种情况,因此不需要单独把这种情况写入代码中)
- str[strIdx] != pattern[patIdx]:
- 如果 pattern[patIdx + 1] 不等于 ' * ', 返回 false。
- 如果 pattern[patIdx + 1] 等于 ' * ',则从 ' * ' 的后一项接着对比,strIdx 不改变。
public class Solution {
public boolean match(char[] str, char[] pattern) {
if (str.length == 0 && pattern.length == 0) return true;
if (pattern.length == 0) return false;
return match(str, pattern, 0, 0);
}
public boolean match(char[] str, char[] pattern, int strIdx, int patIdx) {
// base case 1
if (patIdx == pattern.length && strIdx == str.length) {
return true;
}
// base case 2
if (patIdx == pattern.length) {
return false;
}
if (strIdx == str.length) {
if (patIdx + 1 < pattern.length && pattern[patIdx + 1] == '*') {
return match(str, pattern, strIdx, patIdx + 2);
}
return false;
}
if (str[strIdx] == pattern[patIdx] || pattern[patIdx] == '.') {
if (patIdx + 1 < pattern.length && pattern[patIdx + 1] == '*') {
return match(str, pattern, strIdx + 1, patIdx) ||
match(str, pattern, strIdx, patIdx + 2);
} else {
return match(str, pattern, strIdx + 1, patIdx + 1);
}
} else {
if (patIdx + 1 < pattern.length && pattern[patIdx + 1] == '*') {
return match(str, pattern, strIdx, patIdx + 2);
} else {
return false;
}
}
}
}
方法二:动态规划
动态规划的问题主要是对问题状态的定义和状态转移方程的定义。
这题我参考以下链接,并根据自己的思路,整理了一份从后往前推导的方法。
首先生成一个二维状态表,长宽都比 str 和 pattern 多一。设置默认状态 dp[str.length][pattern.length] = true
这里最难处理的问题还是有 ' * ' 的情况。从后往前循环,每一个状态都是当前状态和后一个状态来决定的,最后只需判断 dp[0][0] 的状态即可。整体情况如下:
-
(图片只显示了主要的过程,省略的部分细节)
-
当前 pattern 为 ' * '时,跳过,只判断非 ' * ' 的 pattern
-
当 pattern[patIdx + 1] != ' * ' ,并且 str[i] == pattern[j] 或 pattern[j] == '.':
- 此时情况很简单,当前 dp[i][j] 的状态由dp[i + 1][j + 1] 或者 dp[i + 1][j + 2n +1] (n = 0, 1, 2, ...)决定,如图所示
- 此时情况很简单,当前 dp[i][j] 的状态由dp[i + 1][j + 1] 或者 dp[i + 1][j + 2n +1] (n = 0, 1, 2, ...)决定,如图所示
-
当 pattern[patIdx + 1] == ' * ' ,有两种情况:
-
str[i] == pattern[j] || pattern[j] == '.'
这种情况最为复杂,dp[i][j] 状态由 dp[i + 1][j + 2] || dp[i][j + 2] || dp[i + 1][j] 决定,如图所示
-
str[i] 与 pattern[j]不相等
此时 dp[i][j] 的状态由 dp[i][j + 2]确定, 如图所示
-
public class Solution {
public boolean match(char[] str, char[] pattern) {
if (str.length == 0 && pattern.length == 0) return true;
if (pattern.length == 0) return false;
if (str.length == 0) {
for (int i = 0; i < pattern.length; i += 2) {
if (!(i + 1 < pattern.length && pattern[i + 1] == '*')) {
return false;
}
}
return true;
}
boolean[][] dp = new boolean[str.length + 1][pattern.length + 1];
dp[str.length][pattern.length] = true;
for (int i = str.length - 1; i >= 0; i--) {
for (int j = pattern.length - 1; j >= 0; j--) {
if (pattern[j] == '*') {
continue;
}
if (j + 1 < pattern.length && pattern[j + 1] == '*') {
if (str[i] == pattern[j] || pattern[j] == '.') {
dp[i][j] = dp[i + 1][j + 2] || dp[i][j + 2] || dp[i + 1][j];
} else {
dp[i][j] = dp[i][j + 2];
}
} else {
if (str[i] == pattern[j] || pattern[j] == '.') {
int j_tmp = j;
while (j_tmp + 2 < pattern.length && pattern[j_tmp + 2] == '*') {
j_tmp += 2;
}
dp[i][j] = dp[i + 1][j_tmp + 1] || dp[i + 1][j + 1];
}
}
}
}
return dp[0][0];
}
}