题目来源于leetcodeRegular Expression Matching:
Given an input string s and a pattern p, implement regular expression matching with support for '.' and '*' where:
.
Matches any single character.*
Matches zero or more of the preceding element. The matching should cover the entire input string (not partial).
翻译:
给定一个字符串s和正则表达式p,实现正则表达式中的
.
和*
匹配功能:
.
匹配任意单个字符。*
匹配0个或者多个该字符前面的字符。 匹配应该是正确匹配输入的完整字符串而非字符串的一部分。
例子: 1.
输入: s = "aa", p = "a"
输出: false
解释: "a"不匹配"aa"。
输入: s = "aa", p = "a*"
输出: true
解释: '*'表示将前一个字符重复0次或者多次. 因此, 将前面的'a'重复1次, 匹配"aa"。
输入: s = "ab", p = ".*"
输出: true
解释: ".*"重复任意字符0次或者多次。
输入: s = "aab", p = "c*a*b"
输出: true
解释: 重复c0次,重复a一次,p匹配"aab"
如果没有*
,只需要从头到尾遍历即可得出答案,但是因为*
的存在,题目的难度成倍增加。
用isMatch(s, p)
表示字符串s
与p
是否匹配。
将复杂的情况细化讨论:
s
和p
都为空时,结果为true
。s
不为空时,p
为空时,结果为false
。s
为空,p
长度大于等于2,且p[1]
为*
时,结果等价于判断s
是否与p.slice(2)
匹配。s
不为空,p
长度大于等于2,且p[1]
为*
时,若s
与p
首字母匹配(即(!(s.length === 0) && (s[0] === p[0] || p[0] === "."))
),结果等价于判断isMatch(s, p.slice(2)) || isMatch(s.slice(1), p)
。s
不为空,p
长度大于等于2,且p[1]
为*
时,若s
与p
首字母不匹配,即判断isMatch(s, p.slice(2))
。p
的长度小于2,或者p[1]
不等于*
时,若s
与p
首字母匹配即判断isMatch(s.slice(1), p.slice(1))
。p
的长度小于2,或者p[1]
不等于*
时,若s
与p
首字母匹配,结果为false
。通过上述分析,很容易写出以下递归答案:
var isMatch = function (s, p) {
if (p.length === 0) {
return s.length === 0;
}
var firstMatch = (!(s.length === 0) && (s[0] === p[0] || p[0] === "."));
if (p.length >= 2 && p[1] === "*") {
return (isMatch(s, p.slice(2)) || (firstMatch && isMatch(s.slice(1), p)));
} else {
return firstMatch && isMatch(s.slice(1), p.slice(1));
}
};
在上面的递归例子中,其实有大量的计算式重复的。不妨使用动态规划,将已经计算的结果存储起来。
算法思路如下:
dp[s.length() + 1][p.length() + 1]
,dp[n][m]
表示s.slice(0, n)
与p.slice(0, m)
是否匹配,dp[0][0]
表示两个空字符串匹配。dp[0][0]
为true,注意这里数组的长度为字符串长度加1,dp[0][0]
表示s
和p
为空。s = "aab"
,p = "c*a*b"
,创建一个dp值表:c | * | a | * | b | |||
---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | ||
0 | TRUE | FALSE | TRUE | FALSE | TRUE | FALSE | |
a | 1 | FALSE | FALSE | FALSE | TRUE | TRUE | FALSE |
a | 2 | FALSE | FALSE | FALSE | FALSE | TRUE | FALSE |
b | 3 | FALSE | FALSE | FALSE | FALSE | FALSE | TRUE |
p
为空,只有当s
也为空时,结果才为true
,所以dp[0][0]
为true,dp[i][0]
(i > 0)为false。s
为空时,其中dp[0][2]
为true,因为p[2]
即p的第二个字符为*
,它可以表示重复前面的字符0次,所以dp[0][2]
与dp[0][0]
相等,所以为true
。s[i - 1] == p[j - 1]
,这表示第i-1个字符与第j-1个字符相同,也就等价于判断剩余的字符串是否匹配,如果剩余的字符串匹配,那么当前的子字符串也就匹配,否则不匹配,dp[i][j] = dp[i - 1][j-1]
(注意dp[0][0]
表示空字符串,所以1
才是字符串的第一个字符)。p[j - 1] == "."
,表示任意单个字符都可以匹配,所以我们需要检查剩余字符是否匹配,即dp[i][j] = dp[i - 1][j - 1]
。p[j - 1] == "*"
,此时有两种情况,第一种情况是重复0次的情况时dp[i][j] == dp[i][j - 2]
。第二种情况s[i - 1] == p[j -2] || p[j - 2] == "."
,此时dp[i][j] == dp[i - 1][j]
通过以上分析,得到以下解法。 JavaScript:
var isMatch = function (s, p) {
const rows = s.length;
const columns = p.length;
/// Base conditions
if (rows == 0 && columns == 0) {
return true;
}
if (columns == 0) {
return false;
}
// 创建数组,首列都为false(匹配模式为空)
const dp = Array.from({ length: s.length + 1 }, () => [false]);
// 都为空的情况
dp[0][0] = true;
// 处理*
for (let i = 1; i < columns + 1; i++) {
if (p[i - 1] === '*') {
dp[0][i] = dp[0][i - 2];
}
else {
dp[0][i] = false;
};
}
// 剩余字符串
for (let i = 1; i < rows + 1; i++) {
for (let j = 1; j < columns + 1; j++) {
if (p[j - 1] === '*') {
if (p[j - 2] === s[i - 1] || p[j - 2] === '.') {
dp[i][j] = dp[i][j - 2] || dp[i - 1][j];
} else {
dp[i][j] = dp[i][j - 2];
}
} else if (p[j - 1] === s[i - 1] || p[j - 1] === '.') {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = false;
}
}
}
return dp[rows][columns];
};
时间复杂度:O(m x n),m表示s
的长度,n表示字p
的长度。 空间复杂度:O(m × n),m表示s
的长度,n表示字p
的长度。
(完)