LeetCode 44:通配符匹配

LeetCode 44:通配符匹配

在这里插入图片描述

问题本质与挑战

通配符匹配规则:

  • ? 匹配单个任意字符
  • * 匹配任意长度的字符序列(包括空序列)。
    需判断模式串 p 是否能完全匹配字符串 s(而非部分匹配)。

核心挑战:

  • 处理 *多态性(匹配空、单个或多个字符);
  • 避免暴力枚举,通过动态规划高效推导状态。

动态规划核心思路

1. 状态定义

dp[i][j] 表示:

  • 字符串 si 个字符s[0..i-1]);
  • 模式串 pj 个字符p[0..j-1]);
    是否完全匹配
2. 初始化逻辑
  • 基础casedp[0][0] = true(空字符串匹配空模式)。
  • 前导 * 处理
    p 以连续 * 开头(如 p = "***abc"),这些 * 可匹配空字符串,因此:
    for (int j = 1; j <= m; j++) {
        if (p.charAt(j-1) == '*') {
            dp[0][j] = dp[0][j-1]; // 继承前一状态(匹配空字符)
        } else {
            break; // 非*字符,前导*结束
        }
    }
    
3. 状态转移方程

遍历 sp 的每个字符(i 从 1 到 nj 从 1 到 m),分三种情况:

情况 1:p[j-1] == '*'

* 可匹配空字符多个字符,因此:

  • 匹配空字符:忽略当前 *,状态继承自 dp[i][j-1]
  • 匹配多个字符* 匹配 s 的当前字符,状态继承自 dp[i-1][j](前 i-1 个字符已匹配,* 继续匹配当前字符)。
    公式:
dp[i][j] = dp[i][j-1] || dp[i-1][j];
情况 2:p[j-1] == '?'

? 匹配单个任意字符,因此状态继承自 dp[i-1][j-1]sp 各前进一位,前面的子串已匹配)。
公式:

dp[i][j] = dp[i-1][j-1];
情况 3:p[j-1]普通字符

需满足两个条件:

  1. s[i-1] == p[j-1](字符相等);
  2. dp[i-1][j-1] == true(前面的子串已匹配)。
    公式:
dp[i][j] = (s.charAt(i-1) == p.charAt(j-1)) && dp[i-1][j-1];

算法步骤详解(示例推导)

示例 1:s = "aa", p = "a"
  • 参数n=2s 长度),m=1p 长度),dp 数组维度 3×2
  • 初始化
    • dp[0][0] = true(空字符串匹配空模式)。
    • p[0] = 'a'(非 *),前导处理中断,故 dp[0][1] = false
  • 遍历 i=1s[0]='a'),j=1p[0]='a'
    普通字符匹配,dp[1][1] = (a==a) && dp[0][0] = true
  • 遍历 i=2s[1]='a'),j=1
    普通字符匹配,但 dp[1][0] = false(空模式无法匹配 s 的前1个字符),故 dp[2][1] = false
  • 结果dp[2][1] = false,匹配失败。
示例 2:s = "aa", p = "*"
  • 参数n=2m=1dp 数组维度 3×2
  • 初始化
    • dp[0][0] = true
    • p[0] = '*',故 dp[0][1] = dp[0][0] = true(前导 * 匹配空字符串)。
  • 遍历 i=1s[0]='a'),j=1p[0]='*'
    * 匹配多个字符,dp[1][1] = dp[1][0] (false) || dp[0][1] (true) → true
  • 遍历 i=2s[1]='a'),j=1
    dp[2][1] = dp[2][0] (false) || dp[1][1] (true) → true
  • 结果dp[2][1] = true,匹配成功。

完整代码(Java)

class Solution {
    public boolean isMatch(String s, String p) {
        int n = s.length();   // s的长度
        int m = p.length();   // p的长度
        // dp[i][j]:s前i个字符与p前j个字符是否匹配
        boolean[][] dp = new boolean[n + 1][m + 1];
        
        // 初始化:空字符串匹配空模式
        dp[0][0] = true;
        // 处理p的前导*(这些*可以匹配空字符串)
        for (int j = 1; j <= m; j++) {
            if (p.charAt(j - 1) == '*') {
                dp[0][j] = dp[0][j - 1];
            } else {
                break; // 非*字符,前导*结束
            }
        }
        
        // 动态规划遍历
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                char sChar = s.charAt(i - 1); // s的当前字符
                char pChar = p.charAt(j - 1); // p的当前字符
                
                if (pChar == '*') {
                    // *匹配空字符(dp[i][j-1]) 或 匹配多个字符(dp[i-1][j])
                    dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
                } else if (pChar == '?') {
                    // ?匹配单个字符,继承前一状态
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    // 普通字符:必须相等,且前面的子串匹配
                    dp[i][j] = (sChar == pChar) && dp[i - 1][j - 1];
                }
            }
        }
        
        return dp[n][m]; // 返回s和p全匹配的结果
    }
}

代码解释

  1. 初始化 dp 数组

    • 维度 (n+1)×(m+1),覆盖空字符串的边界情况。
    • dp[0][0] = true:空字符串与空模式匹配。
  2. 前导 * 处理

    • 遍历 p 的前导字符,若为 *,则 dp[0][j] 继承 dp[0][j-1](表示 * 匹配空字符串)。
    • 遇到非 * 字符立即中断,因为后续 * 不再是“前导”(如 p="*a*",仅第一个 * 是前导)。
  3. 状态转移循环

    • 外层遍历 s 的每个前缀(i 从 1 到 n)。
    • 内层遍历 p 的每个前缀(j 从 1 到 m)。
    • 根据 p[j-1] 的类型(*?、普通字符),应用对应的转移规则:
      • *:两种匹配可能(空或多个字符),取或运算。
      • ?:直接继承前一状态(i-1,j-1)。
      • 普通字符:需字符相等且前一状态匹配。
  4. 结果返回

    • dp[n][m] 表示 s 全串与 p 全串是否完全匹配。

复杂度分析

  • 时间复杂度O(n×m)ns 长度,mp 长度,双重遍历。
  • 空间复杂度O(n×m),存储 dp 数组(可优化为 O(m),但为清晰保留二维数组)。

关键优化与扩展

  • 前导 * 优化:提前处理模式串的前导 *,减少无效计算。
  • 与正则匹配的关联:本题是 LeetCode 10(正则表达式匹配)的简化版,核心思想一致,但 * 的语义更简单(无需关联前面字符)。

掌握该方法后,可轻松迁移到更复杂的字符串匹配问题,动态规划的状态定义转移逻辑是解决此类问题的核心。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值