原题如下:
给定一个字符串 ( s) 和一个字符模式 ( p) ,实现一个支持 ‘?’ 和 ‘*’ 的通配符匹配。
'?' 可以匹配任何单个字符。
'*' 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。
链接:https://leetcode-cn.com/problems/wildcard-matching
思路:
分治法
看到这题,我最开始想到的是lc第10题正则表达式匹配,链接leetcode 10。第十题的官方解法可以简单的说成 “每次只看一位的情况” 的一种分治法。应用到这道题中,算法如下:
- 当p以’?'开头时,isMatch(s,p) = isMatch(s.substr(1), p.substr(1));
- 当p以字母开头时,isMatch(s,p) = ifMatch(s的开头, p的开头) && isMatch(s.substr(1), p.substr(1));
- 当p以‘*’开头时,isMatch(s,p) = isMatch(s, p.substr(1)) || isMatch(s.substr(1), p);
实现代码如下:
public:
// p是否完全由*构成
bool isTotal(string p){
char charArrP[p.size() + 1];
strcpy(charArrP, p.c_str());
for(int i = 0; i < p.size(); i++){
if(charArrP[i] != '*') return false;
}
return true;
}
bool isMatch(string s, string p) {
if(s.size() == 0) return isTotal(p);
if(p.size() == 0) return false;
// s转换为char数组
char charS[s.size() + 1];
strcpy(charS, s.c_str());
// p转换为char数组
char charP[p.size() + 1];
strcpy(charP, p.c_str());
if(charP[0] == '?') return isMatch(s.substr(1), p.substr(1));
else if(charP[0] != '*') return (charP[0] == charS[0]) && isMatch(s.substr(1), p.substr(1));
else return isMatch(s.substr(1), p) || isMatch(s, p.substr(1));
}
题目给的测试用例都能通过,但是提交后出现超时问题。
分治法超时原因分析
由于在面对p中出现‘*’时,我们的代码为:
return isMatch(s.substr(1), p) || isMatch(s, p.substr(1));
导致大量无用的子问题被计算,且子问题还存在重复被计算的情况。这自然就想到了使用动态规划来解决问题。
动态规划
按我的理解,动规重点在于找到合适的数据结构存储子问题和找到合适的方法遍历数据结构,至于递推公式是重要但不困难的。
子问题和数据结构:
从上面的超时分析中,我们可以明显得到子问题是isMatch(s.substr(i), p.substr(j))
,需要遍历的为s的不同长度子串和p的不同长度子串。所以存储子问题的数据结构为矩阵bool isMatchArr[p.size()][s.size()]
,矩阵中的元素isMatchArr[i][j]
记录s.substr(i)
和p.substr(j)
能否匹配。答案即为isMatch(0, 0)
。
递推公式:
- 当p以’?'开头时,isMatchArr[i][j] = isMatchArr[i + 1][j + 1];
- 当p以字母开头时,isMatchArr[i][j] = ifMatch(s的开头, p的开头) && isMatchArr[i + 1][j + 1];
- 当p以‘*’开头时,isMatchArr[i][j] = isMatchArr[i][j + 1] || isMatchArr[i + 1][j];
遍历数据结构方法:
从上面的递推公式可以看出,公式等号右边的值在左边值的右下方,所以我们从下到上,从右到左遍历数据结构。
数据结构初始化:
由于我们从下到上,从右到左遍历数据结构,所以要先初始化数组最下方的行和最右边的列,即p.substr(p.size() - 1)
和s.substr(s.size() - 1)
的情况。
考虑p.substr(p.size() - 1)
的情况,即最下方的行。这是用一位通配符去匹配多位的字符。当字符长度超过1时,只有通配符为‘*’
才能匹配成功。当字符长度为1时,字母相等,通配符‘?’
和通配符‘*’
都可认定成功,代码如下:
if(p.substr(p.size() - 1) == "*"){
for(int i = 0; i < s.size(); i++) isMatchArr[p.size() - 1][i] = true;
}else {
for(int i = 0; i < s.size() - 1; i++) isMatchArr[p.size() - 1][i] = false;
isMatchArr[p.size() - 1][s.size() - 1] = p.substr(p.size() - 1) == "?" || p.substr(p.size() - 1) == s.substr(s.size() - 1);
}
考虑s.substr(s.size() - 1)
的情况,即最右边的列。这是用多位通配符去匹配一位字符。在已知isMatchArr[p.size() - 1][s.size() - 1]
的情况下,分析如下:
-
当
isMatchArr[i + 1][s.size() - 1] == false
时,出现了多余的‘?’
或字母。它的后续isMatchArr[i][s.size() - 1]
必定也为false -
当
isMatchArr[i + 1][s.size() - 1] == true
时,若p.charAt(i) == '*'
则填真,p.charAt(i) == '?'
则只有后面都为‘*’
才为真,若p.charAt(i) 为字母
则只有字母匹配且后面都为‘*’
才为真。
代码如下:
for(int i = p.size() - 2; i >= 0; i--){
if(isMatchArr[i + 1][s.size() - 1] == false) isMatchArr[i][s.size() - 1] = false;
else if(charP[i] == '*') isMatchArr[i][s.size() - 1] = true;
else if(charP[i] == '?') isMatchArr[i][s.size() - 1] = isTotal(p.substr(i + 1));
else isMatchArr[i][s.size() - 1] = charP[i] == charS[s.size() - 1] && isTotal(p.substr(i + 1));
}
动规最终代码
class Solution {
public:
bool isTotal(string p){
char charArrP[p.size() + 1];
strcpy(charArrP, p.c_str());
for(int i = 0; i < p.size(); i++){
if(charArrP[i] != '*') return false;
}
return true;
}
bool isMatch(string s, string p) {
// if(s.size() == 0) return isTotal(p);
// if(p.size() == 0) return false;
// s转换为char数组
char charS[s.size() + 1];
strcpy(charS, s.c_str());
// p转换为char数组
char charP[p.size() + 1];
strcpy(charP, p.c_str());
// if(charP[0] == '?') return isMatch(s.substr(1), p.substr(1));
// else if(charP[0] != '*') return (charP[0] == charS[0]) && isMatch(s.substr(1), p.substr(1));
// else return isMatch(s.substr(1), p) || isMatch(s, p.substr(1));
if(p.size() == 0) return s.size() == 0;
if(s.size() == 0) return isTotal(p);
bool isMatchArr[p.size()][s.size()];
if(p.substr(p.size() - 1) == "*"){
for(int i = 0; i < s.size(); i++) isMatchArr[p.size() - 1][i] = true;
}else {
for(int i = 0; i < s.size() - 1; i++) isMatchArr[p.size() - 1][i] = false;
isMatchArr[p.size() - 1][s.size() - 1] = p.substr(p.size() - 1) == "?" || p.substr(p.size() - 1) == s.substr(s.size() - 1);
}
for(int i = p.size() - 2; i >= 0; i--){
if(isMatchArr[i + 1][s.size() - 1] == false) isMatchArr[i][s.size() - 1] = false;
else if(charP[i] == '*') isMatchArr[i][s.size() - 1] = true;
else if(charP[i] == '?') isMatchArr[i][s.size() - 1] = isTotal(p.substr(i + 1));
else isMatchArr[i][s.size() - 1] = charP[i] == charS[s.size() - 1] && isTotal(p.substr(i + 1));
}
for(int i = p.size() -2; i >= 0; i--){
for(int j = s.size() - 2; j >= 0; j--){
if(charP[i] == '?') isMatchArr[i][j] = isMatchArr[i+1][j+1];
else if(charP[i] != '*') isMatchArr[i][j] = (charP[i] == charS[j]) && isMatchArr[i+1][j+1];
else isMatchArr[i][j] = isMatchArr[i+1][j] || isMatchArr[i][j+1];
}
}
return isMatchArr[0][0];
}
};