动态规划 正则表达式匹配

LeetCode 10. 正则表达式匹配

问题描述

给定字符串 s 和模式 p,实现支持 '.''*' 的正则表达式匹配:

  • '.' 匹配任意单个字符
  • '*' 匹配零个或多个前一个元素
    匹配需覆盖整个字符串 s

示例

示例 1:
输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。

示例 2:
输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3:
输入:s = "ab", p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。

方法一:递归(记忆化搜索)

核心思路

  • 递归函数match(s, i, p, j) 表示 s[i..]p[j..] 是否匹配。
  • 终止条件
    • j == p.length:若 i == s.length 返回 true,否则 false
    • i == s.length:剩余模式需能匹配空串(如 a*b*)。
  • 记忆化:用 memo[i][j] 缓存结果。
  • 递归过程
    1. 当前字符匹配:firstMatch = (i < s.length) && (s[i] == p[j] || p[j] == '.')
    2. j+1 < p.lengthp[j+1]=='*'
      • 匹配0次:跳过模式中的 x*match(s, i, p, j+2))。
      • 匹配1+次:若 firstMatch 为真,消耗字符串一个字符(match(s, i+1, p, j))。
    3. 否则:若 firstMatch 为真,继续匹配下一个字符。
class Solution {
    private int[][] memo; // 记忆化数组:0-未计算,1-true,2-false

    public boolean isMatch(String s, String p) {
        memo = new int[s.length() + 1][p.length() + 1];
        return match(s, 0, p, 0);
    }

	/**
	* 表示 s[i..] 和 p[j..] 是否匹配
	*/
    private boolean match(String s, int i, String p, int j) {
        // 已计算过,直接返回结果
        if (memo[i][j] != 0) {
            return memo[i][j] == 1;
        }
        
        boolean res;
        // 模式串已结束:字符串也必须结束才匹配
        if (j == p.length()) {
            res = (i == s.length());
        } else {
            // 当前字符是否匹配
            boolean firstMatch = (i < s.length()) && 
                (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.');
            
            // 下一个字符是'*':考虑匹配0次或多次
            if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
                res = match(s, i, p, j + 2) ||  // 匹配0次(跳过x*)
                    (firstMatch && match(s, i + 1, p, j)); // 匹配1+次(消耗字符)
            } else {
                // 普通匹配:消耗一个字符
                res = firstMatch && match(s, i + 1, p, j + 1);
            }
        }
        
        // 缓存结果
        memo[i][j] = res ? 1 : 2;
        return res;
    }
}

方法二:动态规划(二维DP)

核心思路

  • 状态定义dp[i][j] 表示 s 的前 i 个字符与 p 的前 j 个字符是否匹配,即 s[0..i-1]p[0..j-1] 是否匹配。
  • 状态初始化
    • dp[0][0] = true(空串匹配空串)。
    • dp[0][j]:空串匹配模式 p 仅当模式形如 a*b*dp[0][j] = (p[j-1]=='*') && dp[0][j-2])。
  • 状态转移方程
    1. p[j-1]=='*'
      • 匹配0次:dp[i][j] = dp[i][j-2](跳过 x*)。
      • 匹配1+次:若 s[i-1] 匹配 p[j-2],则 dp[i][j] = dp[i-1][j](消耗一个字符)。
    2. 否则:若当前字符匹配,则 dp[i][j] = dp[i-1][j-1]
class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length(), n = p.length();
        boolean[][] dp = new boolean[m + 1][n + 1];
        dp[0][0] = true; // 空串匹配空串
        
        // 初始化:s为空串时,p需形如 a*b* 才能匹配
        for (int j = 2; j <= n; j++) {
            if (p.charAt(j - 1) == '*') {
                dp[0][j] = dp[0][j - 2]; // 跳过x*
            }
        }
        
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                char sc = s.charAt(i - 1);
                char pc = p.charAt(j - 1);
                
                if (pc == '*') {
                    char prev = p.charAt(j - 2); // '*'前的字符
                    // 匹配0次:跳过x*
                    dp[i][j] = dp[i][j - 2];
                    // 匹配1+次:若prev匹配sc,则消耗一个字符
                    if (prev == '.' || prev == sc) {
                        dp[i][j] |= dp[i - 1][j];
                    }
                } else if (pc == '.' || pc == sc) {
                    // 普通匹配:消耗一个字符
                    dp[i][j] = dp[i - 1][j - 1];
                }
                // 否则默认为false
            }
        }
        return dp[m][n];
    }
}

算法过程(二维DP)

s="aa", p="a*"

  1. 初始化
    dp[0][0] = true
    dp[0][2] = dp[0][0] = true (p="a*" 匹配空字符串)
    
  2. 填表
    • i=1, j=1'a' 匹配 'a'dp[1][1]=true
    • i=1, j=2'*' 匹配:
      • 零个字符:dp[1][0]=false
      • 一个字符:prevP='a' 匹配 s[0]='a'dp[1][2]=dp[0][2]=true
    • i=2, j=1'a' 匹配 'a'dp[2][1]=dp[1][0]=false
    • i=2, j=2'*' 匹配:
      • 零个字符:dp[2][0]=false
      • 一个字符:prevP='a' 匹配 s[1]='a'dp[2][2]=dp[1][2]=true
  3. 结果dp[2][2]=true

算法分析

  • 时间复杂度
    • 递归:O(mn)(记忆化避免重复计算)。
    • 动态规划:O(mn)。
  • 空间复杂度
    • 递归:O(mn)(记忆化数组 + 递归栈)。
    • 二维DP:O(mn)。

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    // 测试用例1: 基础不匹配
    System.out.println(solution.isMatch("aa", "a")); // false
    // 测试用例2: 星号匹配多个
    System.out.println(solution.isMatch("aa", "a*")); // true
    // 测试用例3: 点星号任意匹配
    System.out.println(solution.isMatch("ab", ".*")); // true
    // 测试用例4: 星号匹配零个
    System.out.println(solution.isMatch("aab", "c*a*b")); // true
    // 测试用例5: 复杂匹配
    System.out.println(solution.isMatch("mississippi", "mis*is*p*.")); // false
}

关键点

  1. 递归记忆化
    • 缓存子问题结果,避免重复计算。
  2. 动态规划状态转移
    • '*' 分匹配0次和多次两种情况。
      • 匹配零个字符:忽略 x* 模式
      • 匹配一个或多个字符:消耗一个字符并保持模式
    • 普通字符需精确匹配。
  3. . 的处理:相当于万能字符,匹配任意单个字符
  4. 结果获取dp[m][n] 表示整个字符串是否匹配

常见问题

  1. 为什么需要处理空字符串?
    模式如 a* 可以匹配空字符串,必须单独处理这种特殊情况。

  2. .* 如何实现万能匹配?

    • '.' 匹配任意字符
    • '*' 允许重复任意次数
    • 组合 ".*" 可匹配任意字符串(包括空串)
  3. 如何处理连续的 *
    题目保证 '*' 前必有有效字符,不会出现连续 '*' 或开头 '*'

  4. 空间复杂度如何优化?
    可改用一维数组,只保存当前行和上一行的状态,空间复杂度降为 O(n)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值