题目
给定一个字符串 (s) 和一个字符模式 § ,实现一个支持 ‘?’ 和 ‘*’ 的通配符匹配。
‘?’ 可以匹配任何单个字符。
‘*’ 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。
示例 1:
输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。
示例 2:
输入:
s = “aa”
p = ""
输出: true
解释: '’ 可以匹配任意字符串。
示例 3:
输入:
s = “cb”
p = “?a”
输出: false
解释: ‘?’ 可以匹配 ‘c’, 但第二个 ‘a’ 无法匹配 ‘b’。
示例 4:
输入:
s = “adceb”
p = “ab”
输出: true
解释: 第一个 ‘’ 可以匹配空字符串, 第二个 '’ 可以匹配字符串 “dce”.
示例 5:
输入:
s = “acdcb”
p = “a*c?b”
输入: false
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/wildcard-matching
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
无官方题解
感想
一开始没用回溯,碰了一鼻子灰,好不容易实现了能回溯的版本,又撞上一个特别长的用例1708 / 1809:“abbabaaabbabbaababbabbbbbabbbabbbabaaaaababababbbabababaabbababaabbbbbbaaaabababbbaabbbbaabbbbababababbaabbaababaabbbababababbbbaaabbbbbabaaaabbababbbbaababaabbababbbbbababbbabaaaaaaaabbbbbaabaaababaaaabb”
“**aa*****ba*a*bb**aa*ab****a*aaaaaa***a*aaaa**bbabb*b*b**aaaaaaaaa*a********ba*bbb***a*ba*bb*bb**a*b*bb”,让我意识到要用dp数组记录结果来剪枝(这应该算是动态规划记录法吧?不是很确定,毕竟自己没有利用状态转移方程。可能只是算剪枝吧)。发现得用这些,陆陆续续加上后代码却显得很丑了,而且巨慢。
执行用时 :86 ms, 在所有Java提交中击败了13.36%的用户
内存消耗 :45.9 MB, 在所有Java提交中击败了57.32%的用户
class Solution {
char[] sChars;
char[] pChars;
short[][] dp;//这个数组是用来记录isMatchChar的结果,来方便回溯剪枝的。一开始用short是因为以为可以存1代表成功,0代表未计算,-1代表失败。后来想想发现其实用boolean就行了,false代表未计算,true代表计算过失败的——成功的就直接return了,不需要存储。
public boolean isMatch(String s, String p) {
if (p == null || p.length() == 0) {
if (s != null && s.length() > 0) return false;
else return true;
}
pChars = p.toCharArray();
if (s == null || s.length() == 0) {
for (int i = 0; i < pChars.length; i++) {
if (pChars[i] != '*') return false;
}
return true;
}
sChars = s.toCharArray();
dp = new short[sChars.length][pChars.length];
return isMatchChar(0, 0);
}
private boolean isMatchChar(int sIndex, int pIndex) {
//System.out.println("isMatchChar():"+sIndex+","+pIndex);
if (pIndex >= pChars.length) {
if (sIndex < sChars.length) {
return false;
} else return true;
}
if (sIndex >= sChars.length) {
for (int i = pIndex; i < pChars.length; i++) {
if (pChars[i] != '*') return false;
}
return true;
}
if (dp[sIndex][pIndex] != 0) return dp[sIndex][pIndex] > 0;
for (int i = pIndex; i < pChars.length; i++) {
if (pChars[i] != '*') {
if (i == pIndex) {
if (pChars[i] == '?' || sChars[sIndex] == pChars[i]) {
//System.out.println("匹配首个s:"+sChars[sIndex]+"和p:"+pChars[i]+",调用下一个");
if (dp[sIndex][pIndex] != 0) return dp[sIndex][pIndex] > 0;
else return isMatchChar(sIndex + 1, pIndex + 1);
} else {
dp[sIndex][pIndex] = -1;
return false;
}
} else {
int count = 0;
int countQuestionMark = 0;
while (i + count < pChars.length && (pChars[i + count] == '?' || pChars[i + count] == '*')) {
if (pChars[i + count] == '?') countQuestionMark++;
count++;
}
if (i + count >= pChars.length) {
return sChars.length - sIndex >= countQuestionMark;
}
int index = findInSChars(pChars[i + count], sIndex + countQuestionMark);
while (index < countQuestionMark) {
if (index < 0) {
dp[sIndex][pIndex] = -1;
return false;
}
index = findInSChars(pChars[i + count], index + 1);
}
while (!isMatchChar(index + 1, i + count + 1)) {
if (index < 0) {
dp[sIndex][pIndex] = -1;
return false;
}
index = findInSChars(pChars[i + count], index + 1);
if (index < 0) {
dp[sIndex][pIndex] = -1;
return false;
}
}
return true;
}
}
}
return true;
}
private int findInSChars(char c, int start) {
for (int i = start; i < sChars.length; i++) {
if (sChars[i] == c) return i;
}
return -1;
}
}
看了一下提交记录的各个时间范例,可以看到有几个区域分布人数比较多,对应的方法也比较典型:
首先是30ms~40ms,这个区域的典型做法是二维数组然后利用dp转移方程推出结果:
class Solution {
public boolean isMatch(String s, String p) {
boolean [][] isMatch=new boolean[s.length()+1][p.length()+1];
isMatch[0][0]=true;
for(int j=1;j<p.length()+1;j++ )
if(p.charAt(j-1)=='*')
isMatch[0][j]=isMatch[0][j-1];
for(int i=1;i<s.length()+1;i++)
for(int j=1;j<p.length()+1;j++){
if(s.charAt(i-1)==p.charAt(j-1) || p.charAt(j-1)=='?'){
isMatch[i][j]=isMatch[i-1][j-1];
}
if(p.charAt(j-1)=='*')
isMatch[i][j]=isMatch[i-1][j] || isMatch[i][j-1];
}
return isMatch[s.length()][p.length()] ;
}
}
然后就是10ms左右的一枝独秀的大佬了:
看了一下提交的4ms范例,大致思路就是用双指针加回溯。回溯是通过startIndex实现的,之所以只用一个值就可以回溯,是因为这道题还有贪心算法的性质:第二个星号出现的时候,前一个星号的匹配就可以确定了,不会出现回溯到第一个星号的情况。
class Solution {
public boolean isMatch(String s, String p) {
// 指定双指针,sIndex指向s字符串,pIndex指向p字符串
int sIndex = 0;
int pIndex = 0;
int match = 0;
int startIndex = -1;
// 从s字符串作为基准进行比对
while (sIndex < s.length()){
// 当pIndex位置的字符和sIndex位置的相同或者pIndex位置的为?时,驱动双指针向前移动一位
if (pIndex < p.length() && (p.charAt(pIndex) == '?' || s.charAt(sIndex) == p.charAt(pIndex))) {
sIndex++;
pIndex++;
}
// 当发现'*'时,对p指针直接移动到下一位,将'*'的index传给startIndex处理,同时将s指针传给match指针
else if (pIndex < p.length() && p.charAt(pIndex) == '*') {
startIndex = pIndex;
match = sIndex;
pIndex++;
}
// 当'*'匹配字符时,pIndex始终保持startIndex+1,即'*'的下一位,然后移动match指针和sIndex指针
// 当出现s和p指针重新匹配时,会被第一个if语句拦截,移动双指针
// 当移动到一个新的'*'时,在第二个else if重新更新sIndex
// 可能会出现匹配短了的情况,比如匹配"adcbeb"和"*a*b",程序会在第一个b就直接跳出,但此时程序检查到后续不匹配的情况
// 此时重新把startIndex+1赋给pIndex,相当于把第一个b包含进*中进行匹配,然后再出现下一个b时再匹配
else if (startIndex != -1) {
pIndex = startIndex + 1;
match++;
sIndex = match;
}
else
return false;
}
// 检查p剩余的子串
while (pIndex < p.length() && p.charAt(pIndex) == '*')
pIndex++;
return pIndex == p.length();
}
}
可以看出自己的代码的最大差距在于dp备忘录如何优化——可以从二维数组优化到一维数组,甚至像最优代码那样再利用贪心算法的性质优化到一个值。。。动态规划的转移方程自己也是一直不怎么会推,看自己回溯和dp杂交的代码感觉自己怕不是学了个假的动态规划。。。