算法题解【leetCode44 通配符匹配】

275 阅读3分钟

「这是我参与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%的用户