LeetCode 第10题:正则表达式匹配

LeetCode 第10题:正则表达式匹配

题目描述

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。

  • '.' 匹配任意单个字符
  • '*' 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖 整个 字符串 s 的,而不是部分字符串。

难度

困难

题目链接

https://leetcode.cn/problems/regular-expression-matching/

示例

示例 1:

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

示例 2:

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

示例 3:

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

提示

  • 1 <= s.length <= 20
  • 1 <= p.length <= 20
  • s 只包含从 a-z 的小写字母
  • p 只包含从 a-z 的小写字母,以及字符 . 和 *
  • 保证每次出现字符 * 时,前面都匹配到有效的字符

解题思路

方法一:动态规划

使用动态规划来解决这个问题。定义dp[i][j]表示s的前i个字符和p的前j个字符是否匹配。

关键点:

  1. 状态定义:dp[i][j] 表示 s[0:i] 和 p[0:j] 是否匹配
  2. 初始状态:dp[0][0] = true,空字符串匹配空模式
  3. 状态转移:
    • 如果p[j-1]是普通字符:dp[i][j] = dp[i-1][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]
      • 使用一次或多次:dp[i][j] = dp[i-1][j] && (s[i-1] == p[j-2] || p[j-2] == '.')

时间复杂度:O(mn),其中m和n分别是字符串s和p的长度
空间复杂度:O(mn)

方法二:递归(回溯)

通过递归的方式处理每个字符的匹配情况。

关键点:

  1. 处理'*'的特殊情况:可以匹配0次或多次
  2. 处理'.'的特殊情况:可以匹配任意字符
  3. 递归终止条件的处理

代码实现

C# 实现(动态规划)

public class Solution {
    public bool IsMatch(string s, string p) {
        int m = s.Length;
        int n = p.Length;
      
        // dp[i][j] 表示 s 的前 i 个字符和 p 的前 j 个字符是否匹配
        bool[,] dp = new bool[m + 1, n + 1];
      
        // 空字符串和空模式匹配
        dp[0, 0] = true;
      
        // 处理模式串中的星号匹配空字符串的情况
        for (int j = 1; j <= n; j++) {
            if (p[j - 1] == '*') {
                dp[0, j] = dp[0, j - 2];
            }
        }
      
        // 填充dp数组
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (p[j - 1] == '*') {
                    // 不使用星号
                    dp[i, j] = dp[i, j - 2];
                  
                    // 使用星号匹配一次或多次
                    if (p[j - 2] == '.' || p[j - 2] == s[i - 1]) {
                        dp[i, j] = dp[i, j] || dp[i - 1, j];
                    }
                }
                else if (p[j - 1] == '.' || p[j - 1] == s[i - 1]) {
                    dp[i, j] = dp[i - 1, j - 1];
                }
            }
        }
      
        return dp[m, n];
    }
}

C# 实现(递归回溯)

public class Solution {
    public bool IsMatch(string s, string p) {
        if (string.IsNullOrEmpty(p)) {
            return string.IsNullOrEmpty(s);
        }
      
        // 检查第一个字符是否匹配
        bool firstMatch = !string.IsNullOrEmpty(s) && 
                         (p[0] == s[0] || p[0] == '.');
      
        // 如果模式中有星号
        if (p.Length >= 2 && p[1] == '*') {
            // 不使用星号 或 使用星号匹配一次或多次
            return IsMatch(s, p.Substring(2)) || 
                   (firstMatch && IsMatch(s.Substring(1), p));
        }
      
        // 普通字符匹配
        return firstMatch && IsMatch(s.Substring(1), p.Substring(1));
    }
}

代码详解

动态规划版本:

  1. 状态数组初始化:
    • dp[0,0] = true 表示空字符串匹配
    • 处理模式串中星号匹配空字符串的情况
  2. 状态转移:
    • 处理'*'的情况:可以不使用或使用多次
    • 处理'.'和普通字符的情况
  3. 最终结果:dp[m,n] 表示整个字符串是否匹配

递归版本:

  1. 基本情况处理:
    • 空模式串的情况
    • 第一个字符的匹配情况
  2. 星号处理:
    • 可以选择不使用星号
    • 可以选择使用星号一次或多次
  3. 递归调用:
    • 处理剩余字符串的匹配

执行结果

动态规划版本:

  • 执行用时:80 ms
  • 内存消耗:38.5 MB

递归版本:

  • 执行用时:92 ms
  • 内存消耗:38.7 MB

总结与反思

  1. 这是一道经典的动态规划题目:
    • 需要清晰的状态定义
    • 复杂的状态转移方程
    • 边界情况的处理
  2. 两种解法比较:
    • 动态规划:效率更高,空间复杂度较大
    • 递归:代码简洁,但可能栈溢出
  3. 优化思路:
    • 可以使用滚动数组优化空间
    • 可以添加记忆化搜索优化递归

相关题目

posted @   旧厂街小江  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示