1 问题
Implement regular expression matching with support for ‘.’ and ‘*’.
'.' Matches any single character.
'*' Matches zero or more of the preceding element.
The matching should cover the entire input string (not partial).
The function prototype should be:
bool isMatch(const char *s, const char *p)
Some examples:
isMatch("aa","a") → false
isMatch("aa","aa") → true
isMatch("aaa","aa") → false
isMatch("aa", "a*") → true
isMatch("aa", ".*") → true
isMatch("ab", ".*") → true
isMatch("aab", "c*a*b") → true
2 分析
2.1 该问题的递归性
如果不考虑"."
和"*"
, 检查两个字符串是否匹配的方法是逐位比较,如果这两个字符串有一个位置上的字符不相等,那么这两个字符串不匹配,如果两个字符串所有位置均对应相等,那么这两个字符串匹配。
接下来考虑存在"."
和"*"
的情况:
"."
匹配任意一个字符,如果待比较的两个字符串某一位置上的字符是"."
, 则认为这两个位置上的字符相等,继续下一个位置的比较。"*"
可以匹配0个或者多个紧邻的前一个字符。即"*"
的存在使该字符串变成了多个可能的字符串。因此,必须将"*"
能够表示的所有情况与另外一个字符串相比较,如果存在一种匹配的情况,那么这两个字符串匹配,反之,这两个字符串不匹配。
"*"
存在时,"*"
和其紧邻的前一个字符(设为m
)有如下三种可能的匹配情况:- 空字符串”“
- 单一字符
m
- 多个字符
mm...
在对"*"
表示的所有可能情况,进行搜索、确定是否匹配的过程中,如果一种情况匹配失败,就要回溯,选择下一种可能情况来进行操作。因此,该问题具有递归性。
2.2 递归解法
递归,回溯解决该问题的伪代码如下:
isMatch(String s, String p)
:
- base case:
if p == ""
: //对应情况:模式字符串匹配完毕
if s == "": return true
else: return false
if s == ""
: //对应情况:源字符串匹配完毕,模式字符串有剩余
if p的长度大于1,且p的第二个字符是*
:return isMatch(s,p[2:end])
else: return false
- 递归过程:
if p的长度大于1 且 p[1] == "*":
//对应情况:str*的匹配
if s[0] == p[0] or p[0] == ".":
if isMatch(s[1:end],p[2:end]): return true
//对应情况:将str*视为单个字符if isMatch(s[1:end], p): return true;
//对应情况:将str*视为多个字符
if isMatch(s,p[2, end]): return true;
//对应情况:将str*视为空return false
else if s[0] == p[0] or p[0] == ".":
return isMatch(s[1:end], p[1:end])
else: return false
匹配字符串str*
至少会存在3种子问题,因此, 算法的时间复杂度与模式字符串中"*"
的个数有关系,设其为
n
,则递归算法的时间复杂度是
2.3 使用动态规划优化代码
- 2.1-2.2节分析得出该问题具有递归性,可以分解为多个子问题求解。下面证明:该问题的子问题是重叠的。
以"a*b*c
为例:
上图中虚线方框内的子问题在递归过程中被重复求解,因此子问题是重叠的。 - 该问题具有最优子结构,如果
s
和p
整个字符串匹配,那么它们任何一个对应子字符串匹配。
通过上述分析,可以得出结论: 动态规划可以用于优化代码。
令dp[i][j]
表示s[0:i],p[0,j]
是否匹配。 dp[i][j]
可以通过2.2节中分析的子问题关系逐步计算出来,计算方法为:
If p.charAt(j) == s.charAt(i) : dp[i][j] = dp[i-1][j-1];
If p.charAt(j) == '.' : dp[i][j] = dp[i-1][j-1];
If p.charAt(j) == '*':
if p.charAt(j-1) != s.charAt(i) : dp[i][j] = dp[i][j-2]
if p.charAt(i-1) == s.charAt(i) or p.charAt(i-1) == '.':
dp[i][j] = dp[i-1][j]
or dp[i][j] = dp[i][j-1]
or dp[i][j] = dp[i][j-2]
经过动态规划优化后算法的时间复杂度为
O(mn)
, 其中
m,n
分别表示s,p
的长度。
3 代码
3.1 递归解法:
public class Solution {
public boolean isMatch(String s, String p) {
System.out.println("S: "+s + " P: " + p);
//base case
if(p.length() == 0){
if(s.length() == 0){
return true;
}else{
return false;
}
}
if(s.length() == 0){
if(p.length() > 1 && p.charAt(1) == '*'){
return isMatch(s, p.substring(2));
}else{
System.out.println(s+" "+p);
return false;
}
}
//recursive procedure
if(p.length() > 1 && p.charAt(1) == '*'){
if( s.charAt(0) == p.charAt(0) || p.charAt(0) == '.'){
if(isMatch(s.substring(1), p.substring(2))){
return true;
}
if(isMatch(s.substring(1), p)){
return true;
}
}
if(isMatch(s, p.substring(2))){
return true;
}
return false;
}else if(s.charAt(0) == p.charAt(0) || p.charAt(0) == '.'){
return isMatch(s.substring(1), p.substring(1));
}else{
return false;
}
}
}
3.2 dp解法
public class Solution {
public boolean isMatch(String s, String p) {
if (s == null || p == null) {
return false;
}
boolean[][] dp = new boolean[s.length()+1][p.length()+1];
dp[0][0] = true;
for (int i = 0; i < p.length(); i++) {
if (p.charAt(i) == '*' && dp[0][i-1]) {
dp[0][i+1] = true;
}
}
for (int i = 0 ; i < s.length(); i++) {
for (int j = 0; j < p.length(); j++) {
if (p.charAt(j) == '.') {
dp[i+1][j+1] = dp[i][j];
}
if (p.charAt(j) == s.charAt(i)) {
dp[i+1][j+1] = dp[i][j];
}
if (p.charAt(j) == '*') {
if (p.charAt(j-1) != s.charAt(i) && p.charAt(j-1) != '.') {
dp[i+1][j+1] = dp[i+1][j-1];
} else {
dp[i+1][j+1] = (dp[i+1][j] || dp[i][j+1] || dp[i+1][j-1]);
}
}
}
}
return dp[s.length()][p.length()];
}
[1] dp解法的代码来源于 https://discuss.leetcode.com/topic/40371/easy-dp-java-solution-with-detailed-explanation