「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」
问题
44. 通配符匹配
给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。
'?' 可以匹配任何单个字符。
'*' 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。
说明:
s可能为空,且只包含从a-z的小写字母。p可能为空,且只包含从a-z的小写字母,以及字符?和*。
尝试解答【1】 - 朴素的递归方式
先确定边界:
- 匹配成功的可能性:匹配一直成功,直到匹配到两个字符串的最后一个字符
- 匹配失败:发生不匹配,或者长度不等的时候匹配提前结束
然后确定过程:
- 如果遇到*:可以匹配若干个字符,也可以不匹配字符
- 这里有一个小case:连续的*也只算一个,因此需要特殊处理
- 如果遇到?:算作比较一个字符
- 字符的话就是直接匹配了
这么分解之后只有第一个是 *的场景需要深入考虑:
- 如果后面有字符,我们需要确定拿字符串的哪一位去和这个字符对齐
- 这里可能有多个可能和字符对齐的情况,那么我们从这里可以开若干次递归,来解决子问题:
- 从*开始顺序遍历下去,每遇到一个匹配的情况就进入一次递归,递归返回成功了就返回成功,否则就继续,直到遍历完,返回了false
根据这个思路,我们就能写出第一版代码:
public static boolean isMatch(String s, String p) {
return isMatch(s, p, 0, 0);
}
public static boolean isMatch(String s, String p, int curL, int curR) {
int l = curL, r = curR;
while (r < p.length()) {
//如果是?,那判断有得匹配的情况下直接往下即可
if (p.charAt(r) == '?') {
if(l>=s.length()) return false;
l++;
r++;
continue;
}
//如果是*,先找到下一个不是*的位置
if (p.charAt(r) == '*') {
while (r < p.length() && p.charAt(r) == '*') r++;
//如果到了最后:那么代表着后面的都不用匹配了
if (r == p.length()) return true;
//没到最后:那么需要退一位,此处的r是最后一位*
r--;
//这里不剪枝会超时,所以需要加上匹配的情况
int j = l;
while (j < s.length()) {
if ((s.charAt(j)==p.charAt(r+1)||p.charAt(r+1)=='?')&&isMatch(s, p, j+1,r+2)) return true;
j++;
}
//如果上面退出了,那么说明后面的都匹配不了,此时只能返回false
return false;
}
if (l >= s.length() || s.charAt(l) != p.charAt(r)) return false;
l++;
r++;
}
//到这里只能说明l或r到达尾部了,此时需要判断
//有没有可能l到最后了,r还是*?
return l == s.length() && r == p.length();
}
当然,这里没有考虑复杂度的问题,因此超时了:
s = "abbabaaabbabbaababbabbbbbabbbabbbabaaaaababababbbabababaabbababaabbbbbbaaaabababbbaabbbbaabbbbababababbaabbaababaabbbababababbbbaaabbbbbabaaaabbababbbbaababaabbababbbbbababbbabaaaaaaaabbbbbaabaaababaaaabb"
`p = "**aa*****ba*a*bb**aa*aba*aaaaaa***a*aaaa**bbabb*b*b**aaaaaaaaa*a********ba*bbb***a*ba*bb*bb**a*b*bb"
【2】分析&优化
哪里导致超时,其实挺明显的:
if ((s.charAt(j)==p.charAt(r+1)||p.charAt(r+1)=='?')&&isMatch(s, p, j+1,r+2)) return true;
套用上面的超时例子,能知道:我们过多地通过递归去判断后面的子问题了,即使相同的子问题在前面的某次递归中已经被处理过了,这里还去处理,就导致了这个问题。
那么如何解决呢?
我们知道,重复子问题的消除我们可以用备忘录,那么按照这里的入参格式,我们可以定义备忘录为:
dp[i][j] = new boolean[s.length()][p.length()]
dp[i][j]的含义,是s子串(i到末尾)和p(j到末尾)是否已访问
虽然因为布尔值只能标识2种状态,我们这里有3种状态:不能匹配,未处理,能匹配,但是,根据逻辑其实不需要记录能匹配的情况,因为能匹配的情况,我们直接返回了,因此我们只要记录哪里走过就可以了。
随后再根据失败的用例加上亿点点细节,AC代码就出来了:
static boolean[][] dp;
public static boolean isMatch(String s, String p) {
//完全没有考虑到这种情况,一个是'',一个是'**********'
if(s.length() == 0){
for (int length = p.length()-1; length >= 0; length--)
if(p.charAt(length)!='*') return false;
return true;
}
dp = new boolean[s.length()][p.length()];
return isMatch(s, p, 0, 0);
}
public static boolean isMatch(String s, String p, int curL, int curR) {
int l = curL, r = curR;
if(l>=s.length() || r>=p.length() ||dp[curL][curR]) return false;
dp[curL][curR] = true;
while (r < p.length()) {
//如果是?,那判断有得匹配的情况下直接往下即可
if (p.charAt(r) == '?') {
if(l>=s.length()) return false;
l++;
r++;
continue;
}
//如果是*,先找到下一个不是*的位置
if (p.charAt(r) == '*') {
while (r < p.length() && p.charAt(r) == '*') r++;
//如果到了最后:那么代表着后面的都不用匹配了
if (r == p.length()) return true;
//没到最后:那么需要退一位,此处的r是最后一位*
r--;
//这里不剪枝会超时,所以需要加上匹配的情况
int j = l;
while (j < s.length()) {
if ((s.charAt(j)==p.charAt(r+1)||p.charAt(r+1)=='?')&&isMatch(s, p, j,r+1)) return true;
j++;
}
//如果上面退出了,那么说明后面的都匹配不了,此时只能返回false
return false;
}
if (l >= s.length() || s.charAt(l) != p.charAt(r)) {
return false;
}
l++;
r++;
}
return l == s.length() && r == p.length();
}
执行用时:24 ms, 在所有 Java 提交中击败了70.56%的用户
内存消耗:39.1 MB, 在所有 Java 提交中击败了18.47%的用户