题目描述

✅ 44. 通配符匹配

image-20260928204309230

image-20260928204309231

题意分析

判断模式串 p 能否完整匹配文本串 s,要求覆盖双方的全部字符,不能只匹配文本中的一段。普通字母只能匹配相同字母,? 恰好匹配一个任意字符,* 可以匹配任意长度的字符序列,包括空串。

两个字符串都可能为空。本题的 * 是独立通配符,不依附在前一个字符上;它的含义与正则表达式中的重复符号不同,转移方式也不同。

解法:一维动态规划匹配前缀

核心思路

[!blue]

先定义二维状态 F[i][j]:模式前 i 个字符能否匹配文本前 j 个字符。初始 F[0][0] = true,空模式不能匹配非空文本。每加入一个模式字符,就计算一行新的匹配状态。

如果当前模式字符是普通字母或 ?,它必须消耗文本最后一个字符。只有这两个字符能够匹配,且更短的两个前缀 F[i - 1][j - 1] 已经匹配,当前状态才为真。普通字符要求相等,? 则不限制文本字符是什么。

如果当前字符是 *,分为两种情况:让它匹配空串,就继承 F[i - 1][j];让它匹配至少一个字符,可以先用当前模式匹配前 j - 1 个字符,再让这个星号多接一个字符,读取 F[i][j - 1]。因此星号的转移是这两个状态取逻辑或,已经覆盖了所有可能的匹配长度。

只保存一行 dp 时,要按依赖选择更新方向。普通字符和 ? 需要上一行的 dp[j - 1],所以从右向左更新,避免它提前被覆盖。星号需要本行已更新的 dp[j - 1],因此从左向右更新;此时原位置的 dp[j] 仍是上一行状态,执行 dp[j] = dp[j] || dp[j - 1] 即可。

dp[0] 表示当前模式前缀能否匹配空文本。遇到星号可以保持原值,遇到其他字符则必须变为假。但清零必须放在该行的内层循环之后,因为更新 j = 1 时还可能需要上一行的 dp[0]。全部模式字符处理完后,返回 dp[n]。

解题步骤

  1. 创建长度为 n + 1 的布尔数组,令 dp[0] = true,其余为假。
  2. 从左到右遍历模式字符。
  3. 遇到 *,从 j = 1 到 n 更新 dp[j] = dp[j] || dp[j - 1],保持 dp[0] 不变。
  4. 遇到普通字符或 ?,从 j = n 到 1,根据旧 dp[j - 1] 与当前字符能否匹配更新状态;循环结束后令 dp[0] = false。
  5. 返回 dp[n],表示完整模式与完整文本是否匹配。

代码实现

class Solution {
    public boolean isMatch(String s, String p) {
        int n = s.length();
        boolean[] dp = new boolean[n + 1];

        dp[0] = true;

        for (int i = 0; i < p.length(); i++) {
            char pattern = p.charAt(i);

            if (pattern == '*') {
                // 星号可多匹配字符,正序读取本轮左侧状态。
                for (int j = 1; j <= n; j++) {
                    dp[j] = dp[j] || dp[j - 1];
                }
            } else {
                // 普通字符或问号只匹配一位,倒序才能读取旧对角状态。
                for (int j = n; j >= 1; j--) {
                    dp[j] = dp[j - 1] && (pattern == '?' || pattern == s.charAt(j - 1));
                }

                // 出现非星号后,非空模式不再匹配空文本。
                dp[0] = false;
            }
        }

        return dp[n];
    }
}
func isMatch(s string, p string) bool {
    n := len(s)
    dp := make([]bool, n+1)
    dp[0] = true

    for i := 0; i < len(p); i++ {
        pattern := p[i]
        if pattern == '*' {
            // 星号可多匹配字符,正序读取本轮左侧状态。
            for j := 1; j <= n; j++ {
                dp[j] = dp[j] || dp[j-1]
            }
        } else {
            // 普通字符或问号只匹配一位,倒序才能读取旧对角状态。
            for j := n; j >= 1; j-- {
                dp[j] = dp[j-1] && (pattern == '?' || pattern == s[j-1])
            }
            // 出现非星号后,非空模式不再匹配空文本。
            dp[0] = false
        }
    }
    return dp[n]
}

复杂度分析

  • 时间复杂度:$O(mn + m + n)$,其中 m、n 分别是模式和文本长度。核心转移为 $O(mn)$,初始化数组及扫描模式另需线性开销;当字符串非空时通常简写为 $O(mn)$。
  • 空间复杂度:$O(n + 1)$,保存长度为 n + 1 的一维状态,不保存全部二维前缀状态。

关键点总结

[!green]

  • 先按前缀定义二维转移,再根据旧行或新行的依赖压缩空间,更新方向不能统一套用。
  • 星号“不匹配字符”和“继续匹配一个字符”两种选择的逻辑或,覆盖任意长度匹配。
  • 空文本只能被全是星号的模式前缀匹配;dp[0] 专门维护这个边界。

易错点总结

[!yellow]

  • 普通字符正序更新,会读取本轮刚改写的左侧状态,错误地让同一模式字符参与多次匹配。
  • 星号逆序更新,无法把本行已经可达的前缀继续向右扩展,限制了星号的匹配长度。
  • 遇到非星号时不清零 dp[0],会把必须消耗字符的模式部分当成能够匹配空串。
  • 在普通字符内层循环之前清零 dp[0],又会丢掉更新第一个文本字符所需的旧状态。
  • ? 不允许匹配空串;最终必须读取完整长度的 dp[n],不能发现某个短前缀匹配就返回真。

相似题目

题目 难度 关联与区别
10. 正则表达式匹配 困难 星号语义不同:本题星号可匹配任意字符串,正则星号只能重复它前面的元素。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/86344261
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!