(LeetCode-Hot100)10. 正则表达式匹配

正则表达式匹配

问题简介

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
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。

示例 4:

复制代码
输入:s = "aab", p = "c*a*b"
输出:true
解释:因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。

示例 5:

复制代码
输入:s = "mississippi", p = "mis*is*p*."
输出:false

解题思路

方法一:动态规划(推荐)

💡 核心思想: 使用二维 DP 表来记录子问题的解。

步骤分析:
  1. 定义状态: dp[i][j] 表示字符串 s 的前 i 个字符和模式 p 的前 j 个字符是否匹配。

  2. 初始化:

    • dp[0][0] = true:空字符串和空模式匹配
    • 对于模式 p,如果 p[j-1] == '*',则 dp[0][j] = dp[0][j-2]* 可以匹配零个前面的字符)
  3. 状态转移:

    • 如果 p[j-1] 不是 '*'
      • s[i-1] == p[j-1]p[j-1] == '.' 时,dp[i][j] = dp[i-1][j-1]
    • 如果 p[j-1] == '*'
      • 匹配零个: dp[i][j] = dp[i][j-2]
      • 匹配一个或多个: 如果 s[i-1] == p[j-2]p[j-2] == '.',则 dp[i][j] = dp[i-1][j]
  4. 返回结果: dp[m][n],其中 mn 分别是 sp 的长度。

方法二:递归 + 记忆化

💡 核心思想: 直接按照匹配规则递归处理,使用记忆化避免重复计算。

步骤分析:
  1. 基本情况:

    • 如果模式为空,返回字符串是否为空
    • 如果字符串为空,检查模式是否能匹配空字符串(如 a*b*c*
  2. 递归情况:

    • 检查第一个字符是否匹配
    • 如果模式的第二个字符是 '*'
      • 跳过 x* 组合(匹配零个)
      • 或者匹配一个字符并继续匹配剩余字符串(匹配一个或多个)
    • 否则,直接匹配第一个字符并递归处理剩余部分

方法三:纯递归(会超时)

不推荐: 时间复杂度指数级,仅用于理解问题逻辑。

// 方法一:动态规划

java 复制代码
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;
        for (int j = 2; j <= n; j += 2) {
            if (p.charAt(j - 1) == '*') {
                dp[0][j] = dp[0][j - 2];
            }
        }
        
        // 状态转移
        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 != '*') {
                    if (sc == pc || pc == '.') {
                        dp[i][j] = dp[i - 1][j - 1];
                    }
                } else {
                    // '*' 匹配零个前面的字符
                    dp[i][j] = dp[i][j - 2];
                    // '*' 匹配一个或多个前面的字符
                    if (sc == p.charAt(j - 2) || p.charAt(j - 2) == '.') {
                        dp[i][j] = dp[i][j] || dp[i - 1][j];
                    }
                }
            }
        }
        return dp[m][n];
    }
}

// 方法二:递归 + 记忆化

java 复制代码
class Solution2 {
    private Boolean[][] memo;
    public boolean isMatch(String s, String p) {
        memo = new Boolean[s.length() + 1][p.length() + 1];
        return dfs(s, 0, p, 0);
    }
    
    private boolean dfs(String s, int i, String p, int j) {
        if (memo[i][j] != null) {
            return memo[i][j];
        }
        
        boolean result;
        if (j == p.length()) {
            result = i == s.length();
        } else {
            boolean firstMatch = i < s.length() && 
                (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.');
            
            if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
                result = dfs(s, i, p, j + 2) || 
                        (firstMatch && dfs(s, i + 1, p, j));
            } else {
                result = firstMatch && dfs(s, i + 1, p, j + 1);
            }
        }
        
        memo[i][j] = result;
        return result;
    }
}

// 方法一:动态规划

go 复制代码
func isMatch(s string, p string) bool {
    m, n := len(s), len(p)
    dp := make([][]bool, m+1)
    for i := range dp {
        dp[i] = make([]bool, n+1)
    }
    
    // 初始化
    dp[0][0] = true
    for j := 2; j <= n; j += 2 {
        if p[j-1] == '*' {
            dp[0][j] = dp[0][j-2]
        }
    }
    
    // 状态转移
    for i := 1; i <= m; i++ {
        for j := 1; j <= n; j++ {
            sc, pc := s[i-1], p[j-1]
            
            if pc != '*' {
                if sc == pc || pc == '.' {
                    dp[i][j] = dp[i-1][j-1]
                }
            } else {
                // '*' 匹配零个前面的字符
                dp[i][j] = dp[i][j-2]
                // '*' 匹配一个或多个前面的字符
                if sc == p[j-2] || p[j-2] == '.' {
                    dp[i][j] = dp[i][j] || dp[i-1][j]
                }
            }
        }
    }
    
    return dp[m][n]
}

// 方法二:递归 + 记忆化

go 复制代码
func isMatch2(s string, p string) bool {
    memo := make(map[string]bool)
    return dfs(s, 0, p, 0, memo)
}

func dfs(s string, i int, p string, j int, memo map[string]bool) bool {
    key := fmt.Sprintf("%d,%d", i, j)
    if val, exists := memo[key]; exists {
        return val
    }
    
    var result bool
    if j == len(p) {
        result = i == len(s)
    } else {
        firstMatch := i < len(s) && (s[i] == p[j] || p[j] == '.')
        
        if j+1 < len(p) && p[j+1] == '*' {
            result = dfs(s, i, p, j+2, memo) || 
                    (firstMatch && dfs(s, i+1, p, j, memo))
        } else {
            result = firstMatch && dfs(s, i+1, p, j+1, memo)
        }
    }
    
    memo[key] = result
    return result
}

示例演示

📌 s = "aab", p = "c*a*b" 为例:

i\j "" c * a * b
"" T F T F T F
a F F F T T F
a F F F T T F
b F F F F F T

✅ 最终结果:dp[3][5] = true

答案有效性证明

正确性分析:

  1. 基础情况正确:

    • 空字符串与空模式匹配 ✅
    • 模式中 x* 可以匹配零个字符 ✅
  2. 状态转移完整:

    • 处理了普通字符匹配(包括 '.'
    • 处理了 '*' 的两种情况:匹配零个和匹配多个
    • 所有可能的匹配路径都被考虑
  3. 数学归纳法:

    • 假设对于所有 i' < ij' < jdp[i'][j'] 计算正确
    • 根据状态转移方程,dp[i][j] 也能正确计算

复杂度分析

方法 时间复杂度 空间复杂度 说明
动态规划 O(m×n) O(m×n) m, n 分别为字符串和模式长度
递归+记忆化 O(m×n) O(m×n) 每个状态只计算一次
纯递归 O(2^(m+n)) O(m+n) 指数级,会超时 ❌

💡 优化空间: 动态规划可以优化到 O(n) 空间复杂度,因为每次只依赖前一行的状态。

问题总结

关键要点:

  • '*' 是最复杂的部分,需要考虑匹配零个和匹配多个两种情况
  • 动态规划的状态定义要清晰:dp[i][j] 表示前缀匹配
  • 初始化很重要,特别是处理模式开头的 x* 组合

常见陷阱:

  • 忘记 '*' 必须和前面的字符一起考虑
  • 边界条件处理不当(空字符串、空模式)
  • 没有考虑 '.' 的特殊匹配规则

适用场景:

  • 字符串模式匹配问题
  • 需要处理通配符的场景
  • 正则表达式引擎的基础实现

github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions

相关推荐
2501_926978331 小时前
分形时空理论框架:从破缺悖论到意识宇宙的物理学新范式引言(理论概念版)--AGI理论系统基础1.1
java·服务器·前端·人工智能·经验分享·agi
西门吹雪分身1 小时前
K8S之Pod调度
java·容器·kubernetes·k8s
弹简特1 小时前
【JavaEE08-后端部分】SpringMVC03-SpringMVC第二大核心处理请求之Cookie/Session和获取header
java·spring boot·spring·java-ee
We་ct2 小时前
LeetCode 146. LRU缓存:题解+代码详解
前端·算法·leetcode·链表·缓存·typescript
烟花落o2 小时前
【数据结构系列03】链表的回文解构、相交链表
数据结构·算法·链表·刷题
努力学算法的蒟蒻2 小时前
day87(2.16)——leetcode面试经典150
数据结构·leetcode·面试
追随者永远是胜利者2 小时前
(LeetCode-Hot100)17. 电话号码的字母组合
java·算法·leetcode·职场和发展·go
不想看见4042 小时前
Shortest Bridge -- 广度优先搜索 --力扣101算法题解笔记
算法·leetcode·宽度优先
流云鹤2 小时前
2026牛客寒假算法基础集训营5(B D G J F )
算法