Hard | LeetCode 10 | 剑指 Offer19. 正则表达式匹配 | 递归 | 动态规划
请实现一个函数用来匹配包含'. '
和'*'
的正则表达式。模式中的字符'.'
表示任意一个字符,而'*'
表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"
与模式"a.a"
和"ab*ac*a"
匹配,但与"aa.a"
和"ab*a"
均不匹配。
示例 1:
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:
s = "aa"
p = "a*"
输出: true
解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:
输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4:
输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
示例 5:
输入:
s = "mississippi"
p = "mis*is*p*."
输出: false
s
可能为空,且只包含从a-z
的小写字母。p
可能为空,且只包含从a-z
的小写字母以及字符.
和*
,无连续的'*'
。
方法一:递归
维持两个指针sIndex, pIndex 分别指向主串和模式串, 从左往右比较。
-
如果pIndex指向的元素的下一个元素不是
'*'
, 则说明当前的模式串字符必须参与匹配, 且匹配一次, 然后继续匹配后面未匹配的字符。用代码语言表达为if (pIndex == p.length() - 1 || p.charAt(pIndex+1) != '*') { // 下一个字符不是'*' if (sIndex < s.length() &&(p.charAt(pIndex) == s.charAt(sIndex) || p.charAt(pIndex) =='.')) { // 匹配主串和模式串后面的字符 return matchCore(s, sIndex + 1, p, pIndex + 1); } }
-
如果pIndex指向的元素的下一个元素是
'*'
, 则说明当前的pIndex元素可以参与匹配0次 或者参与匹配多次。2.1 如果当前的pIndex元素参与匹配0次 , 那么当前的主串字符在下一次递归还是要参与匹配的, 而且对于模式串, 下一次递归里, 当前字符和下一个
'*'
都不参与匹配。用代码语言表达为return matchCore(s, sIndex, p, pIndex + 2);
2.2 如果当前的pIndex元素参与匹配, 可能1, 2,3,4... 多次。则当前主串字符在下一次递归就无需再匹配了, 因为当前的pIndex元素已经匹配了。如果是只匹配一次, 当前的模式串字符和下一个
'*'
号在下一次递归可以不匹配了, 但如果是2, 3, 4...等多次, 那么还是要在下一次递归继续使用。综合来讲, 可以写为matchCore(s, sIndex + 1, p, pIndex)
, 因为即使下一个'*'
号只代表当前字符串出现1次, 把这个'*'
号保留到下一次递归当中, 下一次递归如果不需要此字符,'*'
号是可以代表当前当前字符出现0次的。所以将'*'
代表前一个字符出现1, 2,3..等多次写成matchCore(s, sIndex + 1, p, pIndex)
也是没有问题的。 -
前面1,2两点是递归的具体逻辑。而递归出口就比较容易想到了。
3.1 如果主串和模式串刚好全部匹配完, 自然返回true
3.2 如果模式串已经匹配完, 主串还没有匹配完, 那么返回false。
3.3 如果是主串已经全部匹配完, 模式串还没有匹配完, 那么这个时候是否能直接返回false呢?答案是不可以。反例就是:主串
""
, 模式串a*
所以即找到了递归的关系(前面第1, 2点), 也找到了递归出口(前面第3点), 下面的代码自然就水到渠成了。
public boolean isMatch(String s, String p) {
if (s == null || p == null) {
}
return matchCore(s, 0, p, 0);
}
public boolean matchCore(String s, int sIndex, String p, int pIndex) {
if (sIndex == s.length() && pIndex == p.length()) {
return true;
}
// 模式串全部匹配完就会立即判断是否能够匹配
if ((sIndex < s.length() && pIndex == p.length())) {
return false;
}
// 模式串的下一个不是 * , 或者模式串已经匹配到最后一个字符 的情况
if (pIndex == p.length() - 1 || p.charAt(pIndex+1) != '*') {
if (sIndex < s.length() && // 确保主串指针不能越界, 因为即使主串已经全部匹配完, 如果模式串还没有匹配, 还是要继续匹配的
(p.charAt(pIndex) == s.charAt(sIndex) || p.charAt(pIndex) == '.')) {
return matchCore(s, sIndex + 1, p, pIndex + 1);
}
} else {
// 模式串下一个是 * 的情况
// 当前模式串的字符, 能够匹配当前字符串字符
if (sIndex < s.length() && (p.charAt(pIndex) == s.charAt(sIndex) || p.charAt(pIndex) == '.')) {
// 可以选择 * 匹配当前字符串字符1次或多次, 当前模式串字符还要继续匹配后面的字符串
return matchCore(s, sIndex + 1, p, pIndex) ||
// * 匹配当前字符串字符 0 次
matchCore(s, sIndex, p, pIndex + 2);
} else {
// 当前模式串字符 无法匹配 当前字符串字符 那么* 只能出现代表前面字符 0 次
return matchCore(s, sIndex, p, pIndex + 2);
}
}
return false;
}
方法二: 动态规划
既然是动态规划, 我首先给一个二维表格boolean[][] match = new boolean[s.length() + 1][p.length() + 1];
其中match[i][j]代表从 主串i以及左边的字符串 与 模式串里j以及左边的字符串 能够匹配上。所以动态规划的递归关系就分为以下两种情况
-
如果当前字符不是
'*'
, 那么当前match值取决于 (1) 当前的主串和模式串的字符(单个) 是否能够匹配; (2) 如果能够匹配了, 还要看他们前面的那些字符是否能够匹配上, 使用代码语言表达为if (p.charAt(j-1) != '*') { match[i][j] = matches(s, i, p, j) && match[i-1][j-1]; }
其中判断某两个字符是否能够匹配的方法如下 , 注意, 在进行遍历的时候, 第一个字符的下标是从1开始里, 这里要相应的处理。
public boolean matches(String s, int i, String p, int j) { if (i == 0) { return false; } return s.charAt(i-1) == p.charAt(j-1) || p.charAt(j-1) == '.'; }
-
如果当前字符是
'*'
。这种情况就要分两种情况讨论了。
2.1 当前'*'
字符的前一个字符出现次数为0, 那么当前的match[i][j]值, 完全等价于match[i][j-2], 这个应该很容易理解
2.2 当前'*'
字符的前一个字符出现次数不为0, 出现1, 2,3....等多次, 那么当前的match[i][j]值即取决于:(1) 当前主串的s[i]的值与模式串*号前一个值p[j-1]的值是否能够匹配; (2) 主串前面的值 与 模式串前面的值(包含当前的'*'
) 是否能够匹配。(至于为什么即使*表示前一个字符只出现1次, 比较前面的字符串 也要包含在内 ? 原因和第一种递归里的方法一样, 在前一次匹配当中, 这里的*同样是可以代表前一个字符出现次数为0的);这两种情况用代码语言表达为:
match[i][j] = match[i][j-2] || // * 的前一个字符匹配0次 (matches(s, i, p, j-1) && match[i-1][j]); // * 的前一个字符匹配1次或者多次
-
动态规划的初始边界。
3.1 match[0][0] = true, 表示当主串和模式串都没有字符时, 为true。
3.2 循环遍历起始指针如下, 主串是从下标0开始的, 原因是即使主串没有字符, 模式串有字符, 比如主串:
""
, 模式串"a*"
, 这个时候match[0][1]和match[0][2] 都是true, 也是需要进行计算的。for(int i = 0; i <= s.length(); i++) { for (int j = 1; j <= p.length(); j++) { // TODO 动态规划递推关系 } }
依据以上的递推关系和起始边界, 动态规则算法的代码如下:
public boolean isMatch(String s, String p) {
boolean[][] match = new boolean[s.length() + 1][p.length() + 1];
match[0][0] = true;
for(int i = 0; i <= s.length(); i++) {
for (int j = 1; j <= p.length(); j++) {
if (p.charAt(j-1) != '*') {
match[i][j] = matches(s, i, p, j) && match[i-1][j-1];
} else {
// 当前字符为 *
match[i][j] = match[i][j-2] || // * 的前一个字符匹配0次
(matches(s, i, p, j-1) && match[i-1][j]); // * 的前一个字符匹配1次或者多次
}
}
}
return match[s.length()][p.length()];
}
public boolean matches(String s, int i, String p, int j) {
if (i == 0) {
return false;
}
// 匹配的条件: (1) 主串和模式串字符相等 (2) 模式串字符为'.', 可以匹配任意字符
return s.charAt(i-1) == p.charAt(j-1) || p.charAt(j-1) == '.';
}